fix: solución de bloqueos y estandarización de permisos

This commit is contained in:
2026-04-27 10:47:30 -05:00
parent fa3cbb4d0a
commit 5c2a84e95d
312 changed files with 13889 additions and 7911 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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