fix: solución de bloqueos y estandarización de permisos
This commit is contained in:
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/code-pedimento-regimens?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/code-pedimento-regimens/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { codePedimentoRegimensApi, type CodePedimentoRegimen } from '$lib/api/dashboard/reference_data/code_pedimento_regimens';
|
||||
import {
|
||||
codePedimentoRegimensApi,
|
||||
type CodePedimentoRegimen
|
||||
} from '$lib/api/dashboard/reference_data/code_pedimento_regimens';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/code_pedimento_regimens/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/code_pedimento_regimens/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -12,10 +15,12 @@
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/code_pedimento_regimens/list';
|
||||
import type { PageData } from './$types';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
'Lista Códigos',
|
||||
@@ -24,29 +29,22 @@
|
||||
})
|
||||
);
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<CodePedimentoRegimen[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_pedimento_regimens.view'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_pedimento_regimens.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_pedimento_regimens.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -57,6 +55,7 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await codePedimentoRegimensApi.list(1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
@@ -69,9 +68,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -98,51 +99,80 @@
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [CodePedimentoRegimens] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Pedimento - Regímenes
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Relación entre códigos de pedimento y regímenes aduaneros
|
||||
</p>
|
||||
<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)]"
|
||||
>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_code_pedimento_regimens.view' : error || ''}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex flex-none items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Pedimento - Regímenes</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Relación entre códigos de pedimento y regímenes aduaneros
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
{#if error}
|
||||
<div
|
||||
class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Pedimento - Regímenes</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Buscar"
|
||||
bind:value={searchQuery}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex min-h-0 flex-1 flex-col overflow-hidden p-0">
|
||||
<div class="h-full flex-1 overflow-hidden rounded-md border bg-background">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Pedimento - Regímenes</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/containers?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/containers/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +71,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
status: response.status,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,34 +2,21 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { containersApi, type Container } from '$lib/api/dashboard/reference_data/containers';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/containers/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/containers/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/containers/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<Container[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
@@ -38,6 +25,15 @@
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_containers.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'ref_containers.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_containers.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_containers.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,6 +44,7 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await containersApi.list(1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
@@ -60,9 +57,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -71,7 +70,7 @@
|
||||
if (loading || !hasMore) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
try {
|
||||
const response = await containersApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
@@ -82,69 +81,85 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.items) {
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Containers] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
|
||||
useShortcuts('Contenedores', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Contenedores
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los tipos de contenedores disponibles en el sistema
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Contenedores</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_containers.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Contenedores</h1>
|
||||
<p class="text-muted-foreground">Gestiona los tipos de contenedores disponibles en el sistema</p>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
<Button class="h-9" onclick={() => (alert('Módulo de creación no disponible para Catálogos Públicos'))}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Contenedor
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Contenedores</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/countries?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/countries/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +71,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
status: response.status,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { countriesApi, type Country } from '$lib/api/dashboard/reference_data/countries';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/countries/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/countries/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/countries/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/countries/list';
|
||||
import { browser } from '$app/environment';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
@@ -27,7 +29,6 @@
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
// Función para obtener el valor de una cookie
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
@@ -35,7 +36,6 @@
|
||||
return null;
|
||||
};
|
||||
|
||||
// Verificar si hay token en las cookies
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
|
||||
// También sincronizar refresh_token si existe
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
|
||||
@@ -61,6 +60,15 @@
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_countries.view') || userHasPermission($currentUser, 'cat_countries.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'ref_countries.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_countries.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_countries.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -71,6 +79,7 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await countriesApi.list(1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
@@ -84,7 +93,6 @@
|
||||
loading = false;
|
||||
}
|
||||
|
||||
// Actualizar URL silenciosamente para mantener estado
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
@@ -101,14 +109,10 @@
|
||||
|
||||
try {
|
||||
const response = await countriesApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
|
||||
if (response.error) {
|
||||
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
|
||||
|
||||
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
// Recargar automáticamente después de 2 segundos
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
@@ -119,7 +123,6 @@
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
// Agregar los nuevos items al array existente
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
@@ -133,48 +136,71 @@
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
// Reset y recargar desde el principio
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
// Recargar datos después de crear/editar/eliminar
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas con el callback onSuccess
|
||||
const columns = createColumns(handleSuccess);
|
||||
// Crear columnas con el callback onSuccess y permisos
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Países
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los países disponibles en el sistema
|
||||
</p>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_countries.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Países
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los países disponibles en el sistema
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
<Button class="h-9" onclick={() => (alert('Módulo de creación no disponible para Catálogos Públicos'))}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo País
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Países</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Países</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/currency-types?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/currency-types/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +71,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
status: response.status,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,20 +2,31 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { currencyTypesApi, type CurrencyType } from '$lib/api/dashboard/reference_data/currency_types';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/currency_types/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/currency_types/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/currency_types/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/currency_types/list';
|
||||
import { browser } from '$app/environment';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
'Lista Tipos de Moneda',
|
||||
obtenerAtajosLista({
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
// Sincronizar token de cookies
|
||||
// Sincronizar tokens
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
@@ -24,9 +35,18 @@
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
if (cookieToken && cookieToken !== localToken) {
|
||||
localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
|
||||
localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,6 +58,15 @@
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_currency_types.view') || userHasPermission($currentUser, 'cat_currency.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'ref_currency_types.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_currency_types.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_currency_types.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,6 +77,7 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await currencyTypesApi.list(1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
@@ -60,9 +90,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -71,7 +103,7 @@
|
||||
if (loading || !hasMore) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
try {
|
||||
const response = await currencyTypesApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
@@ -82,62 +114,80 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.items) {
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [CurrencyTypes] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
useShortcuts('Tipos de Moneda', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Tipos de Moneda
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los tipos de moneda disponibles en el sistema
|
||||
</p>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_currency_types.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Tipos de Moneda</h1>
|
||||
<p class="text-muted-foreground">Gestiona los tipos de moneda disponibles en el sistema</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
<Button class="h-9" onclick={() => (alert('Módulo de creación no disponible para Catálogos Públicos'))}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Tipo de Moneda
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Tipos de Moneda</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Tipos de Moneda</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/customs-sections?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/customs-sections/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +71,8 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
companyId,
|
||||
status: response.status,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,35 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { customsSectionsApi, type CustomsSection } from '$lib/api/dashboard/reference_data/customs_sections';
|
||||
import {
|
||||
customsSectionsApi,
|
||||
type CustomsSection
|
||||
} from '$lib/api/dashboard/reference_data/customs_sections';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/customs_sections/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/customs_sections/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/customs_sections/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<CustomsSection[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
@@ -38,6 +29,15 @@
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_customs_sections.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'ref_customs_sections.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_customs_sections.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_customs_sections.delete'));
|
||||
|
||||
const isError = $derived(status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,8 +48,9 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await customsSectionsApi.list(1, pageSize, searchQuery);
|
||||
const response = await customsSectionsApi.list(data.companyId, 1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
@@ -60,9 +61,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -72,7 +75,12 @@
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await customsSectionsApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
const response = await customsSectionsApi.list(
|
||||
data.companyId,
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
searchQuery
|
||||
);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
@@ -89,55 +97,92 @@
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [CustomsSections] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
|
||||
useShortcuts('Secciones Aduanales', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Secciones Aduanales
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona las secciones aduanales del sistema
|
||||
</p>
|
||||
<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)]"
|
||||
>
|
||||
{#if !canView || isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? "ref_customs_sections.view" : (error || "")}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Header Section -->
|
||||
<div class="flex flex-none items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Secciones Aduanales</h1>
|
||||
<p class="text-muted-foreground">Gestiona las secciones aduanales del sistema</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
<Button
|
||||
class="h-9"
|
||||
onclick={() => alert('Módulo de creación no disponible para Catálogos Públicos')}
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Sección
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
{#if error}
|
||||
<div
|
||||
class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Secciones Aduanales</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Buscar"
|
||||
bind:value={searchQuery}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex min-h-0 flex-1 flex-col overflow-hidden p-0">
|
||||
<div class="h-full flex-1 overflow-hidden rounded-md border bg-background">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Secciones Aduanales</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/customs-warehouses?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/customs-warehouses/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +71,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
status: response.status,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,33 +2,29 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { customsWarehousesApi, type CustomsWarehouse } from '$lib/api/dashboard/reference_data/customs_warehouses';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/customs_warehouses/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/customs_warehouses/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/customs_warehouses/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/customs_warehouses/list';
|
||||
import { browser } from '$app/environment';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
'Lista Recintos Fiscalizados',
|
||||
obtenerAtajosLista({
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<CustomsWarehouse[]>(data.items || []);
|
||||
@@ -38,6 +34,15 @@
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_customs_warehouses.view') || userHasPermission($currentUser, 'cat_warehouses.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'ref_customs_warehouses.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_customs_warehouses.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_customs_warehouses.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,6 +53,7 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await customsWarehousesApi.list(1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
@@ -60,9 +66,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -71,7 +79,7 @@
|
||||
if (loading || !hasMore) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
try {
|
||||
const response = await customsWarehousesApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
@@ -82,62 +90,80 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.items) {
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [CustomsWarehouses] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
useShortcuts('Almacenes Aduanales', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Recintos Fiscalizados
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los recintos fiscalizados del sistema aduanal
|
||||
</p>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_customs_warehouses.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Recintos Fiscalizados</h1>
|
||||
<p class="text-muted-foreground">Gestiona los recintos fiscalizados aduanales en el sistema</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
<Button class="h-9" onclick={() => (alert('Módulo de creación no disponible para Catálogos Públicos'))}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Recinto
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Recintos Fiscalizados</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Recintos Fiscalizados</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -24,8 +24,24 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const code = url.searchParams.get('code') || '';
|
||||
const description = url.searchParams.get('description') || '';
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Construir URL con filtros
|
||||
let endpoint = `v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}`;
|
||||
let endpoint = `v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}&company_id=${companyId}`;
|
||||
if (code) endpoint += `&code=${encodeURIComponent(code)}`;
|
||||
if (description) endpoint += `&description=${encodeURIComponent(description)}`;
|
||||
|
||||
@@ -46,6 +62,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -61,6 +78,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
status: response.status,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,155 +1,174 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { incotermsApi, type Incoterm } from '$lib/api/dashboard/reference_data/incoterms';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/incoterms/columns';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/incoterms/data-table.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/incoterms/list';
|
||||
import type { PageData } from './$types';
|
||||
import { onMount } from 'svelte';
|
||||
import { incotermsApi, type Incoterm } from '$lib/api/dashboard/reference_data/incoterms';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/incoterms/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/incoterms/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { page } from '$app/stores';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/incoterms/list';
|
||||
import { browser } from '$app/environment';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
'Lista Incoterms',
|
||||
obtenerAtajosLista({
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
'Lista Incoterms',
|
||||
obtenerAtajosLista({
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<Incoterm[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<Incoterm[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_incoterms.view') || userHasPermission($currentUser, 'cat_incoterms.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'ref_incoterms.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_incoterms.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_incoterms.delete'));
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
try {
|
||||
const response = await incotermsApi.list(currentPage + 1, pageSize, searchCode, searchDesc);
|
||||
// Filtros de búsqueda (Código y Descripción)
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDescription = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
if (response.error) {
|
||||
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await incotermsApi.list(1, pageSize, searchCode, searchDescription);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error aplicando filtros:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchDescription) url.searchParams.set('description', searchDescription);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Page] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await incotermsApi.list(currentPage + 1, pageSize, searchCode, searchDescription);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Incoterms] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
function reloadData() {
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
try {
|
||||
const response = await incotermsApi.list(1, pageSize, searchCode, searchDesc);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error aplicando filtros:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
// Actualizar URL silenciosamente para mantener estado
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
const url = new URL($page.url);
|
||||
goto(url, { invalidateAll: true });
|
||||
}
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Incoterms
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Catálogo de términos internacionales de comercio
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_incoterms.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Incoterms</h1>
|
||||
<p class="text-muted-foreground">Gestiona los términos de comercio internacional en el sistema</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
<Button class="h-9" onclick={() => (alert('Módulo de creación no disponible para Catálogos Públicos'))}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Incoterm
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Incoterms</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Clave" bind:value={searchCode} oninput={handleSearch} class="h-9 w-36 bg-card lg:w-44" />
|
||||
<Input placeholder="Descripción" bind:value={searchDesc} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable columns={createColumns(handleSuccess)} data={allItems} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Incoterms</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Código" bind:value={searchCode} oninput={handleSearch} class="h-9 w-32 bg-card lg:w-40" />
|
||||
<Input placeholder="Descripción" bind:value={searchDescription} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/invoice-types?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/invoice-types/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +71,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
status: response.status,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,42 +2,47 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { invoiceTypesApi, type InvoiceType } from '$lib/api/dashboard/reference_data/invoice_types';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/invoice_types/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/invoice_types/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/invoice_types/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/invoice_types/list';
|
||||
import { browser } from '$app/environment';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
'Lista Tipos de Factura',
|
||||
obtenerAtajosLista({
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<InvoiceType[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_invoice_types.view') || userHasPermission($currentUser, 'cat_inv_types.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'ref_invoice_types.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_invoice_types.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_invoice_types.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,8 +53,9 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await invoiceTypesApi.list(1, pageSize, undefined, searchQuery);
|
||||
const response = await invoiceTypesApi.list(1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
@@ -60,9 +66,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -71,8 +79,8 @@
|
||||
if (loading || !hasMore) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await invoiceTypesApi.list(currentPage + 1, pageSize, undefined, searchQuery);
|
||||
try {
|
||||
const response = await invoiceTypesApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
@@ -82,62 +90,80 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.items) {
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [InvoiceTypes] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
useShortcuts('Tipos de Factura', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Tipos de Factura
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los tipos de facturas disponibles en el sistema
|
||||
</p>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_invoice_types.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Tipos de Factura</h1>
|
||||
<p class="text-muted-foreground">Gestiona los tipos de factura en el sistema</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
<Button class="h-9" onclick={() => (alert('Módulo de creación no disponible para Catálogos Públicos'))}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Tipo
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Tipos de Factura</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Tipos de Factura</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/material-types?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/material-types/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { materialTypesApi, type MaterialType } from '$lib/api/dashboard/reference_data/material_types';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/material_types/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/material_types/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/material_types/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -11,33 +11,29 @@
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<MaterialType[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_material_types.view'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_material_types.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_material_types.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,8 +44,11 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await materialTypesApi.list(1, pageSize, undefined, searchQuery);
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
const response = await materialTypesApi.list(companyId, 1, pageSize, undefined, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
@@ -60,9 +59,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -72,7 +73,9 @@
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await materialTypesApi.list(currentPage + 1, pageSize, undefined, searchQuery);
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
const response = await materialTypesApi.list(companyId, currentPage + 1, pageSize, undefined, searchQuery);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
@@ -82,62 +85,79 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.items) {
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [MaterialTypes] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
|
||||
useShortcuts('Tipos de Material', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Tipos de Material
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los tipos de materiales disponibles en el sistema
|
||||
</p>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_material_types.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Tipos de Material</h1>
|
||||
<p class="text-muted-foreground">Gestiona los tipos de materiales disponibles en el sistema</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Tipos de Material</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Tipos de Material</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/payment-methods?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/payment-methods/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +71,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
companyId,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { paymentMethodsApi, type PaymentMethod } from '$lib/api/dashboard/reference_data/payment_methods';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/payment_methods/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/payment_methods/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/payment_methods/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -11,33 +11,29 @@
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<PaymentMethod[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'pedimentos_payments.view'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'pedimentos_payments.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'pedimentos_payments.delete'));
|
||||
|
||||
const isError = $derived(status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,8 +44,9 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await paymentMethodsApi.list(1, pageSize, searchQuery);
|
||||
const response = await paymentMethodsApi.list(data.companyId, 1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
@@ -60,9 +57,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -71,8 +70,8 @@
|
||||
if (loading || !hasMore) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await paymentMethodsApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
try {
|
||||
const response = await paymentMethodsApi.list(data.companyId, currentPage + 1, pageSize, searchQuery);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
@@ -82,62 +81,81 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.items) {
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [PaymentMethods] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
|
||||
useShortcuts('Métodos de Pago', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Métodos de Pago
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona las formas de pago disponibles en el sistema
|
||||
</p>
|
||||
{#if !canView}
|
||||
<ErrorState status={403} />
|
||||
{:else if isError}
|
||||
<ErrorState
|
||||
status={status}
|
||||
error={error || ''}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Métodos de Pago</h1>
|
||||
<p class="text-muted-foreground">Gestiona las formas de pago disponibles en el sistema</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Métodos de Pago</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Métodos de Pago</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/pedimento-codes?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/pedimento-codes/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +71,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
companyId,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { pedimentoCodesApi, type PedimentoCode } from '$lib/api/dashboard/reference_data/pedimento_codes';
|
||||
import {
|
||||
pedimentoCodesApi,
|
||||
type PedimentoCode
|
||||
} from '$lib/api/dashboard/reference_data/pedimento_codes';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/pedimento_codes/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/pedimento_codes/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/pedimento_codes/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -11,33 +14,28 @@
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<PedimentoCode[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_pedimento_codes.view'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_pedimento_codes.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_pedimento_codes.delete'));
|
||||
|
||||
const isError = $derived(status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,8 +46,9 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await pedimentoCodesApi.list(1, pageSize, searchQuery);
|
||||
const response = await pedimentoCodesApi.list(data.companyId, 1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
@@ -60,9 +59,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -72,7 +73,12 @@
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await pedimentoCodesApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
const response = await pedimentoCodesApi.list(
|
||||
data.companyId,
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
searchQuery
|
||||
);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
@@ -89,55 +95,79 @@
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [PedimentoCodes] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
useShortcuts('Claves de Pedimento', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Claves de Pedimento
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona las claves de pedimento del sistema aduanero
|
||||
</p>
|
||||
<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)]"
|
||||
>
|
||||
{#if !canView || isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'ref_pedimento_codes.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Header Section -->
|
||||
<div class="flex flex-none items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Claves de Pedimento</h1>
|
||||
<p class="text-muted-foreground">Gestiona las claves de pedimento del sistema aduanero</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
{#if error}
|
||||
<div
|
||||
class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Claves de Pedimento</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Buscar"
|
||||
bind:value={searchQuery}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex min-h-0 flex-1 flex-col overflow-hidden p-0">
|
||||
<div class="h-full flex-1 overflow-hidden rounded-md border bg-background">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Claves de Pedimento</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,25 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
items: [],
|
||||
total: 0,
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/pedimento-regimens?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/pedimento-regimens/?page=${page}&page_size=${pageSize}&company_id=${companyId}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -39,6 +55,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +71,8 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
companyId,
|
||||
status: response.status,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,43 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { pedimentoRegimensApi, type PedimentoRegimen } from '$lib/api/dashboard/reference_data/pedimento_regimens';
|
||||
import {
|
||||
pedimentoRegimensApi,
|
||||
type PedimentoRegimen
|
||||
} from '$lib/api/dashboard/reference_data/pedimento_regimens';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/pedimento_regimens/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/pedimento_regimens/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/pedimento_regimens/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/pedimento_regimens/list';
|
||||
import { browser } from '$app/environment';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
'Lista Regímenes de Pedimento',
|
||||
obtenerAtajosLista({
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<PedimentoRegimen[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_pedimento_regimens.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'ref_pedimento_regimens.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_pedimento_regimens.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_pedimento_regimens.delete'));
|
||||
|
||||
const isError = $derived(status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,8 +57,9 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await pedimentoRegimensApi.list(1, pageSize, searchQuery);
|
||||
const response = await pedimentoRegimensApi.list(data.companyId, 1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
@@ -60,9 +70,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -72,7 +84,12 @@
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await pedimentoRegimensApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
const response = await pedimentoRegimensApi.list(
|
||||
data.companyId,
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
searchQuery
|
||||
);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
@@ -89,55 +106,87 @@
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [PedimentoRegimens] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
useShortcuts('Regímenes', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Regímenes
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los regímenes aduaneros de pedimento
|
||||
</p>
|
||||
<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)]"
|
||||
>
|
||||
{#if !canView || isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? "ref_pedimento_regimens.view" : (error || "")}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex flex-none items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Regímenes de Pedimento</h1>
|
||||
<p class="text-muted-foreground">Gestiona los regímenes de pedimento en el sistema</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
<Button
|
||||
class="h-9"
|
||||
onclick={() => alert('Módulo de creación no disponible para Catálogos Públicos')}
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Régimen
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
{#if error}
|
||||
<div
|
||||
class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Regímenes</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Buscar"
|
||||
bind:value={searchQuery}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex min-h-0 flex-1 flex-col overflow-hidden p-0">
|
||||
<div class="h-full flex-1 overflow-hidden rounded-md border bg-background">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Regímenes</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -58,6 +58,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { sectorsApi, type Sector } from '$lib/api/dashboard/reference_data/sectors';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/sectors/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/sectors/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/sectors/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -12,33 +12,28 @@
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<Sector[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_sectors.view') || userHasPermission($currentUser, 'cat_sectors.view'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_sectors.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_sectors.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -62,6 +57,7 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const companyId = getActiveCompanyId();
|
||||
if (!companyId) {
|
||||
@@ -79,9 +75,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -106,62 +104,79 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.items) {
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Sectors] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
|
||||
useShortcuts('Sectores', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Sectores
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los sectores económicos del sistema
|
||||
</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Sectores</h1>
|
||||
<p class="text-muted-foreground">Gestiona los sectores económicos del sistema</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_sectors.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Sectores</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Sectores</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,9 @@ import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
// Esperar a que el layout padre valide/refresque el token
|
||||
await parent();
|
||||
|
||||
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
|
||||
if (!accessToken) {
|
||||
return {
|
||||
error: 'No authenticated',
|
||||
@@ -22,9 +22,29 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
// 🛡️ Buscamos el ID de la compañía (indispensable para el backend)
|
||||
const companyIdStr = url.searchParams.get('company_id') ||
|
||||
cookies.get('active_company_id') ||
|
||||
cookies.get('activeCompanyId') ||
|
||||
cookies.get('company_id');
|
||||
|
||||
// Si no hay ID, devolvemos estructura vacía y evitamos el 422 de FastAPI
|
||||
if (!companyIdStr) {
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: page,
|
||||
page_size: pageSize,
|
||||
error: null,
|
||||
status: 200
|
||||
};
|
||||
}
|
||||
|
||||
const companyId = parseInt(companyIdStr);
|
||||
|
||||
// 🛡️ Inyectamos el company_id en la Query String
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/states?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/states/?company_id=${companyId}&page=${page}&page_size=${pageSize}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -37,8 +57,9 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
statusText: response.statusText,
|
||||
error: errorText
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -66,4 +87,4 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
page_size: 50
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -2,65 +2,40 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/states/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/states/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/states/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { browser } from '$app/environment';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
// 🛡️ Importaciones de Seguridad y Estado Global
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Keyboard Shortcuts
|
||||
useShortcuts('Estados', [
|
||||
{
|
||||
key: 'Alt+Shift+R',
|
||||
description: 'Actualizar Lista',
|
||||
action: reloadData
|
||||
}
|
||||
]);
|
||||
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
// Función para obtener el valor de una cookie
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
|
||||
// Verificar si hay token en las cookies
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
|
||||
if (cookieToken && cookieToken !== localToken) {
|
||||
localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
|
||||
// También sincronizar refresh_token si existe
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
|
||||
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
|
||||
localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<State[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// 🛡️ Permisos (Runas derivadas)
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_states.view'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_states.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_states.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -70,9 +45,19 @@
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
// 🛡️ Validar compañía activa antes de la petición
|
||||
if (!companyStore.activeCompany) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await statesApi.list(1, pageSize, searchQuery);
|
||||
// ✅ Pasar company_id como primer argumento
|
||||
const response = await statesApi.list(
|
||||
companyStore.activeCompany.id,
|
||||
1,
|
||||
pageSize,
|
||||
searchQuery
|
||||
);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
@@ -83,95 +68,121 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await statesApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
|
||||
try {
|
||||
// ✅ Pasar company_id como primer argumento
|
||||
const response = await statesApi.list(
|
||||
companyStore.activeCompany.id,
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
searchQuery
|
||||
);
|
||||
if (response.error) {
|
||||
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
|
||||
|
||||
// Si es un error de autenticación (401 o 403) y no se pudo refrescar, mostrar mensaje específico
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
// Recargar automáticamente después de 2 segundos
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
// Agregar los nuevos items al array existente
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Page] Error loading more:', e);
|
||||
console.error('📊 [States] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
// Reset y recargar desde el principio
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
// Recargar datos después de crear/editar/eliminar
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas con el callback onSuccess
|
||||
const columns = createColumns(handleSuccess);
|
||||
// 🚨 Función para obtener columnas frescas con permisos actualizados
|
||||
function getFreshColumns() {
|
||||
return createColumns(handleSuccess, { canEdit: canEdit, canDelete: canDelete });
|
||||
}
|
||||
|
||||
useShortcuts('Estados', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Estados
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los estados y sus claves de identificación
|
||||
</p>
|
||||
<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)]"
|
||||
>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_states.view' : error || ''}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex flex-none items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Estados</h1>
|
||||
<p class="text-muted-foreground">Gestiona los estados y regiones del sistema aduanero</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
{#if error}
|
||||
<div
|
||||
class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Estados</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Buscar"
|
||||
bind:value={searchQuery}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex min-h-0 flex-1 flex-col overflow-hidden p-0">
|
||||
<div class="h-full flex-1 overflow-hidden rounded-md border bg-background">
|
||||
<DataTable data={allItems} columns={getFreshColumns()} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Estados</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,9 @@ import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
// Esperar a que el layout padre valide/refresque el token
|
||||
await parent();
|
||||
|
||||
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
|
||||
if (!accessToken) {
|
||||
return {
|
||||
error: 'No authenticated',
|
||||
@@ -22,9 +22,30 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
|
||||
const companyIdStr = url.searchParams.get('company_id') ||
|
||||
cookies.get('active_company_id') ||
|
||||
cookies.get('company_id') ||
|
||||
cookies.get('activeCompanyId');
|
||||
|
||||
|
||||
if (!companyIdStr) {
|
||||
console.log('⏳ [Transport Modes] SSR: Sin company_id, delegando fetch al cliente.');
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: page,
|
||||
page_size: pageSize,
|
||||
status: 200,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
const companyId = parseInt(companyIdStr);
|
||||
|
||||
// 🛡️ Ahora SÍ le mandamos el company_id a FastAPI
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/transport-modes?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/transport-modes/?company_id=${companyId}&page=${page}&page_size=${pageSize}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -37,8 +58,9 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
statusText: response.statusText,
|
||||
error: errorText
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +76,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
status: response.status,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -66,4 +89,4 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
page_size: 50
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,45 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { transportModesApi, type TransportMode } from '$lib/api/dashboard/reference_data/transport_modes';
|
||||
import {
|
||||
transportModesApi,
|
||||
type TransportMode
|
||||
} from '$lib/api/dashboard/reference_data/transport_modes';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/transport_modes/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/transport_modes/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/transport_modes/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
// 🛡️ Seguridad y Compañía
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<TransportMode[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_transport_modes.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'ref_transport_modes.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_transport_modes.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_transport_modes.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
@@ -47,9 +46,17 @@
|
||||
if (!browser) return;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
if (!companyStore.activeCompany) return; // 🛡️ Evitar llamada sin compañía
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await transportModesApi.list(1, pageSize, searchQuery);
|
||||
// 🛡️ Pasamos el company_id
|
||||
const response = await transportModesApi.list(
|
||||
companyStore.activeCompany.id,
|
||||
1,
|
||||
pageSize,
|
||||
searchQuery
|
||||
);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
@@ -60,19 +67,29 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
if (!companyStore.activeCompany) return; // 🛡️ Evitar llamada sin compañía
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await transportModesApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
// 🛡️ Pasamos el company_id
|
||||
const response = await transportModesApi.list(
|
||||
companyStore.activeCompany.id,
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
searchQuery
|
||||
);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
@@ -89,55 +106,95 @@
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [TransportModes] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// 🚨 Usamos una función para forzar la evaluación fresca de las columnas (evitando el bug de caché de Svelte 5)
|
||||
function getFreshColumns() {
|
||||
return createColumns(handleSuccess, { canEdit: canEdit, canDelete: canDelete });
|
||||
}
|
||||
|
||||
useShortcuts('Modos de Transporte', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Modos de Transporte
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los modos de transporte disponibles en el sistema
|
||||
</p>
|
||||
<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)]"
|
||||
>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_transport_modes.view' : error || ''}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex flex-none items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Modos de Transporte</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los modos de transporte disponibles en el sistema
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
<Button
|
||||
class="h-9"
|
||||
onclick={() => alert('Módulo de creación no disponible para Catálogos Públicos')}
|
||||
>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Modo
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
{#if error}
|
||||
<div
|
||||
class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="flex min-h-0 flex-1 flex-col overflow-hidden border bg-background">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Modos de Transporte</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
placeholder="Buscar"
|
||||
bind:value={searchQuery}
|
||||
oninput={handleSearch}
|
||||
class="h-9 w-44 bg-card lg:w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex min-h-0 flex-1 flex-col overflow-hidden p-0">
|
||||
<div class="h-full flex-1 overflow-hidden rounded-md border bg-background">
|
||||
<DataTable data={allItems} columns={getFreshColumns()} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Modos de Transporte</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,9 @@ import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
// Esperar a que el layout padre valide/refresque el token
|
||||
await parent();
|
||||
|
||||
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
|
||||
if (!accessToken) {
|
||||
return {
|
||||
error: 'No authenticated',
|
||||
@@ -22,9 +22,29 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
// 🛡️ Buscamos el ID de la compañía (indispensable para el backend)
|
||||
const companyIdStr = url.searchParams.get('company_id') ||
|
||||
cookies.get('active_company_id') ||
|
||||
cookies.get('activeCompanyId') ||
|
||||
cookies.get('company_id');
|
||||
|
||||
// Si no hay ID, devolvemos estructura vacía y evitamos el 422
|
||||
if (!companyIdStr) {
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: page,
|
||||
page_size: pageSize,
|
||||
error: null,
|
||||
status: 200
|
||||
};
|
||||
}
|
||||
|
||||
const companyId = parseInt(companyIdStr);
|
||||
|
||||
// 🛡️ Inyectamos el company_id en la Query String
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/transport-types?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/transport-types/?company_id=${companyId}&page=${page}&page_size=${pageSize}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -37,8 +57,9 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
statusText: response.statusText,
|
||||
error: errorText
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -66,4 +87,4 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
page_size: 50
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { transportTypesApi, type TransportType } from '$lib/api/dashboard/reference_data/transport_types';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/transport_types/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/transport_types/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/transport_types/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
@@ -11,33 +11,29 @@
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<TransportType[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_transport_types.view'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_transport_types.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_transport_types.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,8 +44,10 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await transportTypesApi.list(1, pageSize, searchQuery);
|
||||
if (!companyStore.activeCompany) return;
|
||||
const response = await transportTypesApi.list(companyStore.activeCompany.id, 1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
@@ -60,9 +58,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -71,8 +71,9 @@
|
||||
if (loading || !hasMore) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await transportTypesApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
try {
|
||||
if (!companyStore.activeCompany) return;
|
||||
const response = await transportTypesApi.list(companyStore.activeCompany.id, currentPage + 1, pageSize, searchQuery);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
@@ -82,62 +83,79 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.items) {
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [TransportTypes] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
|
||||
useShortcuts('Tipos de Transporte', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Tipos de Transporte
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los tipos de transporte según código SAT
|
||||
</p>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_transport_types.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Tipos de Transporte</h1>
|
||||
<p class="text-muted-foreground">Gestiona los tipos de transporte del sistema aduanero</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Tipos de Transporte</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Tipos de Transporte</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
@@ -4,9 +4,9 @@ import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
// Esperar a que el layout padre valide/refresque el token
|
||||
await parent();
|
||||
|
||||
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
|
||||
if (!accessToken) {
|
||||
return {
|
||||
error: 'No authenticated',
|
||||
@@ -22,9 +22,29 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
// 🛡️ Buscamos el ID de la compañía en URL o Cookies
|
||||
const companyIdStr = url.searchParams.get('company_id') ||
|
||||
cookies.get('active_company_id') ||
|
||||
cookies.get('activeCompanyId') ||
|
||||
cookies.get('company_id');
|
||||
|
||||
// 🛡️ Si no hay ID, abortamos elegantemente para evitar el 422
|
||||
if (!companyIdStr) {
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: page,
|
||||
page_size: pageSize,
|
||||
status: 200,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
const companyId = parseInt(companyIdStr);
|
||||
|
||||
// 🛡️ Inyectamos el company_id en la petición al backend
|
||||
const response = await authenticatedFetch(
|
||||
`v1/public/reference_data/valuation-methods?page=${page}&page_size=${pageSize}`,
|
||||
`v1/public/reference_data/valuation-methods/?company_id=${companyId}&page=${page}&page_size=${pageSize}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch
|
||||
@@ -37,8 +57,9 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
statusText: response.statusText,
|
||||
error: errorText
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
error: `Error ${response.status}: ${response.statusText}`,
|
||||
items: [],
|
||||
total: 0,
|
||||
@@ -54,6 +75,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
total: data.total || 0,
|
||||
page: data.page || page,
|
||||
page_size: data.page_size || pageSize,
|
||||
status: response.status,
|
||||
error: null
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -66,4 +88,4 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
page_size: 50
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -2,42 +2,48 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { valuationMethodsApi, type ValuationMethod } from '$lib/api/dashboard/reference_data/valuation_methods';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/valuation_methods/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/valuation_methods/columns.js';
|
||||
import { createColumns } from '$lib/components/dashboard/reference_data/valuation_methods/columns';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { RefreshCw, Plus } from 'lucide-svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
import { RefreshCw } from 'lucide-svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import type { PageData } from './$types';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosLista } from '$lib/config/shortcuts/dashboard/reference_data/valuation_methods/list';
|
||||
import { browser } from '$app/environment';
|
||||
import { currentUser, userHasPermission } from '$lib/auth';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Sincronizar token de cookies
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Atajos
|
||||
useShortcuts(
|
||||
'Lista Métodos de Valoración',
|
||||
obtenerAtajosLista({
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<ValuationMethod[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
let status = $state<number>(data.status || 200);
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(userHasPermission($currentUser, 'ref_valuation_methods.view') || userHasPermission($currentUser, 'cat_valuation.view'));
|
||||
const canCreate = $derived(userHasPermission($currentUser, 'ref_valuation_methods.create'));
|
||||
const canEdit = $derived(userHasPermission($currentUser, 'ref_valuation_methods.edit'));
|
||||
const canDelete = $derived(userHasPermission($currentUser, 'ref_valuation_methods.delete'));
|
||||
|
||||
const isError = $derived(!canView || status >= 400 || error);
|
||||
|
||||
// Filtros
|
||||
let searchQuery = $state($page.url.searchParams.get('search') || '');
|
||||
@@ -48,8 +54,10 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(async () => {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await valuationMethodsApi.list(1, pageSize, searchQuery);
|
||||
if (!companyStore.activeCompany) return;
|
||||
const response = await valuationMethodsApi.list(companyStore.activeCompany.id, 1, pageSize, searchQuery);
|
||||
if (!response.error && response.data) {
|
||||
allItems = response.data.items;
|
||||
currentPage = 1;
|
||||
@@ -60,9 +68,11 @@
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
const url = new URL($page.url);
|
||||
if (searchQuery) url.searchParams.set('search', searchQuery);
|
||||
else url.searchParams.delete('search');
|
||||
|
||||
history.replaceState(history.state, '', url);
|
||||
}, 500);
|
||||
}
|
||||
@@ -71,8 +81,9 @@
|
||||
if (loading || !hasMore) return;
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const response = await valuationMethodsApi.list(currentPage + 1, pageSize, searchQuery);
|
||||
try {
|
||||
if (!companyStore.activeCompany) return;
|
||||
const response = await valuationMethodsApi.list(companyStore.activeCompany.id, currentPage + 1, pageSize, searchQuery);
|
||||
if (response.error) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
@@ -82,62 +93,80 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (response.data?.items) {
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [ValuationMethods] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
window.location.reload();
|
||||
if (browser) window.location.reload();
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
useShortcuts('Métodos de Valoración', [
|
||||
{ key: 'Alt+Shift+R', description: 'Actualizar Lista', action: reloadData }
|
||||
]);
|
||||
|
||||
const columns = createColumns(handleSuccess);
|
||||
// Crear columnas
|
||||
const columns = $derived(createColumns(handleSuccess, { canEdit, canDelete }));
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
|
||||
<!-- Header Section -->
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">
|
||||
Métodos de Valoración
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona los métodos de valoración aduanera autorizados
|
||||
</p>
|
||||
{#if isError}
|
||||
<ErrorState
|
||||
status={!canView ? 403 : status}
|
||||
error={!canView ? 'Permission denied: ref_valuation_methods.view' : (error || '')}
|
||||
onRetry={reloadData}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-none flex items-center justify-between">
|
||||
<div class="space-y-1">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Métodos de Valoración</h1>
|
||||
<p class="text-muted-foreground">Gestiona los métodos de valoración en el sistema</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4 {loading ? 'animate-spin' : ''}" />
|
||||
Actualizar
|
||||
</Button>
|
||||
{#if canCreate}
|
||||
<Button class="h-9" onclick={() => (alert('Módulo de creación no disponible para Catálogos Públicos'))}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo Método
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Button variant="outline" size="sm" class="h-9" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{#if error}
|
||||
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<Card.Header>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<Card.Title>Listado de Métodos de Valoración</Card.Title>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" />
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content class="min-h-0 p-0 flex-1 overflow-hidden flex flex-col">
|
||||
<div class="rounded-md border bg-background overflow-hidden flex-1 h-full">
|
||||
<DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} />
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
{/if}
|
||||
|
||||
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
|
||||
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Métodos de Valoración</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Buscar" bind:value={searchQuery} oninput={handleSearch} class="h-9 w-44 bg-card lg:w-64" /></div></div></Card.Header>
|
||||
<Card.Content class="min-h-0 p-0"><div class="rounded-md border bg-background overflow-hidden flex min-h-0 flex-1 flex-col"><DataTable data={allItems} {columns} {loading} {hasMore} {loadMore} /></div></Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="flex-none text-sm text-muted-foreground">Mostrando {allItems.length} de {totalItems} registros</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user