feat: Implement token refresh mechanism and infinite scroll for code pedimento regimens
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
* Cliente API para comunicación con el backend
|
||||
*/
|
||||
import { getToken } from './auth';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
@@ -11,13 +12,121 @@ export interface ApiResponse<T = any> {
|
||||
status: number;
|
||||
}
|
||||
|
||||
let isRefreshing = false;
|
||||
let refreshSubscribers: ((token: string) => void)[] = [];
|
||||
|
||||
/**
|
||||
* Realiza una petición al API
|
||||
* Agrega una petición a la cola de espera mientras se refresca el token
|
||||
*/
|
||||
function subscribeTokenRefresh(callback: (token: string) => void) {
|
||||
refreshSubscribers.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifica a todas las peticiones en espera que el token se ha refrescado
|
||||
*/
|
||||
function onTokenRefreshed(token: string) {
|
||||
refreshSubscribers.forEach((callback) => callback(token));
|
||||
refreshSubscribers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Intenta refrescar el token usando el refresh token
|
||||
*/
|
||||
async function refreshToken(): Promise<string | null> {
|
||||
if (!browser) return null;
|
||||
|
||||
const refreshTokenValue = localStorage.getItem('refresh_token');
|
||||
if (!refreshTokenValue) {
|
||||
console.warn('🔄 No refresh token available');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🔄 [API] Intentando refrescar token...');
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/v1/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshTokenValue }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('❌ [API] Refresh token expirado o inválido');
|
||||
// Si el refresh token también está expirado, limpiar todo
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
// Limpiar cookies también
|
||||
document.cookie = 'access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
|
||||
document.cookie = 'refresh_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC';
|
||||
// Redirigir al login después de un pequeño delay para que el usuario vea el mensaje
|
||||
setTimeout(() => {
|
||||
if (browser) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
}, 2000);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Guardar los nuevos tokens
|
||||
if (data.access_token) {
|
||||
console.log('✅ [API] Token refrescado exitosamente');
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
|
||||
if (data.refresh_token) {
|
||||
localStorage.setItem('refresh_token', data.refresh_token);
|
||||
}
|
||||
|
||||
// Actualizar también las cookies
|
||||
const isSecure = window.location.protocol === 'https:';
|
||||
const secureFlag = isSecure ? '; Secure' : '';
|
||||
|
||||
document.cookie = `access_token=${data.access_token}; path=/; max-age=${60 * 60 * 24 * 7}; SameSite=Lax${secureFlag}`;
|
||||
if (data.refresh_token) {
|
||||
document.cookie = `refresh_token=${data.refresh_token}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax${secureFlag}`;
|
||||
}
|
||||
|
||||
// Actualizar el authStore si está disponible
|
||||
try {
|
||||
const { authStore } = await import('./auth');
|
||||
authStore.setToken(data.access_token);
|
||||
} catch (e) {
|
||||
// Si no se puede importar authStore, no es crítico
|
||||
console.warn('⚠️ [API] No se pudo actualizar authStore:', e);
|
||||
}
|
||||
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('❌ [API] Error refreshing token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Realiza una petición al API con manejo automático de refresh token
|
||||
*/
|
||||
async function fetchApi<T = any>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
options: RequestInit = {},
|
||||
retryCount = 0
|
||||
): Promise<ApiResponse<T>> {
|
||||
// Si ya estamos refrescando el token, esperar
|
||||
if (isRefreshing && retryCount === 0) {
|
||||
return new Promise((resolve) => {
|
||||
subscribeTokenRefresh((newToken) => {
|
||||
resolve(fetchApi<T>(endpoint, options, 1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const token = getToken();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
@@ -36,6 +145,41 @@ async function fetchApi<T = any>(
|
||||
credentials: 'include' // Importante: envía cookies con cada request
|
||||
});
|
||||
|
||||
// Si recibimos 401 o 403 y no es el endpoint de refresh, intentar refrescar el token
|
||||
if ((response.status === 401 || response.status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) {
|
||||
console.warn(`⚠️ [API] ${response.status} recibido en ${endpoint}, intentando refrescar token...`);
|
||||
console.log(`⚠️ [API] Token actual disponible:`, token ? 'Sí (parcial: ' + token.substring(0, 20) + '...)' : 'No');
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const newToken = await refreshToken();
|
||||
|
||||
if (newToken) {
|
||||
// Token refrescado exitosamente
|
||||
console.log(`✅ [API] Reintentando petición a ${endpoint} con nuevo token`);
|
||||
onTokenRefreshed(newToken);
|
||||
isRefreshing = false;
|
||||
// Reintentar la petición original con el nuevo token
|
||||
return await fetchApi<T>(endpoint, options, 1);
|
||||
} else {
|
||||
console.error(`❌ [API] No se pudo refrescar el token para ${endpoint}`);
|
||||
isRefreshing = false;
|
||||
// Retornar error 401 para que la capa superior lo maneje
|
||||
return {
|
||||
error: 'Sesión expirada. Por favor, inicia sesión nuevamente.',
|
||||
status: 401
|
||||
};
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error(`❌ [API] Error al refrescar token:`, refreshError);
|
||||
isRefreshing = false;
|
||||
return {
|
||||
error: 'Error al refrescar la sesión',
|
||||
status: 401
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -50,6 +194,7 @@ async function fetchApi<T = any>(
|
||||
status: response.status
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`❌ [API] Error de conexión en ${endpoint}:`, error);
|
||||
return {
|
||||
error: 'Error de conexión con el servidor',
|
||||
status: 0
|
||||
@@ -79,6 +224,8 @@ export const api = {
|
||||
auth: {
|
||||
login: (credentials: { username: string; password: string; tenant_slug: string }) =>
|
||||
api.post('/v1/auth/login', credentials),
|
||||
refresh: (refreshToken: string) =>
|
||||
api.post('/v1/auth/refresh', { refresh_token: refreshToken }),
|
||||
logout: (data: { refresh_token: string }) => api.post('/v1/auth/logout', data),
|
||||
me: () => api.get('/v1/auth/me'),
|
||||
health: () => api.get('/health')
|
||||
|
||||
@@ -289,6 +289,9 @@ export const login = async (credentials: {
|
||||
|
||||
// Guardar en cookies para que el servidor pueda acceder
|
||||
setCookie('access_token', loginData.access_token);
|
||||
if (loginData.refresh_token) {
|
||||
setCookie('refresh_token', loginData.refresh_token);
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar información del usuario
|
||||
@@ -362,6 +365,8 @@ export const logout = async () => {
|
||||
authStore.reset();
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
deleteCookie('access_token');
|
||||
deleteCookie('refresh_token');
|
||||
|
||||
// Si hay instancia de Keycloak, hacer logout de Keycloak
|
||||
if (keycloakInstance?.authenticated) {
|
||||
@@ -411,6 +416,60 @@ export const getToken = (): string | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Refresca el access token usando el refresh token
|
||||
*/
|
||||
export const refreshAccessToken = async (): Promise<boolean> => {
|
||||
if (!browser) return false;
|
||||
|
||||
const refreshToken = localStorage.getItem('refresh_token');
|
||||
if (!refreshToken) {
|
||||
console.warn('No refresh token available');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { api } = await import('./api');
|
||||
const response = await api.auth.refresh(refreshToken);
|
||||
|
||||
if (response.error || !response.data) {
|
||||
console.error('Failed to refresh token:', response.error);
|
||||
// Si falla el refresh, hacer logout
|
||||
await logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Actualizar tokens
|
||||
const newAccessToken = response.data.access_token;
|
||||
const newRefreshToken = response.data.refresh_token;
|
||||
|
||||
authStore.setToken(newAccessToken);
|
||||
localStorage.setItem('access_token', newAccessToken);
|
||||
|
||||
if (newRefreshToken) {
|
||||
localStorage.setItem('refresh_token', newRefreshToken);
|
||||
}
|
||||
|
||||
// Actualizar también la cookie
|
||||
setCookie('access_token', newAccessToken);
|
||||
|
||||
console.log('✅ Token refreshed successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error);
|
||||
await logout();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtiene el refresh token
|
||||
*/
|
||||
export const getRefreshToken = (): string | null => {
|
||||
if (!browser) return null;
|
||||
return localStorage.getItem('refresh_token');
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtiene la instancia de Keycloak
|
||||
*/
|
||||
|
||||
@@ -1,78 +1,68 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel
|
||||
getCoreRowModel
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
totalItems: number;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
onPageChange: (page: number) => void;
|
||||
loading: boolean;
|
||||
hasMore: boolean;
|
||||
loadMore: () => void;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
totalItems,
|
||||
currentPage,
|
||||
pageSize,
|
||||
onPageChange
|
||||
loading,
|
||||
hasMore,
|
||||
loadMore
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
let pagination = $state<PaginationState>({
|
||||
pageIndex: currentPage - 1,
|
||||
pageSize: pageSize
|
||||
});
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: Math.ceil(totalItems / pageSize),
|
||||
state: {
|
||||
get pagination() {
|
||||
return pagination;
|
||||
}
|
||||
}
|
||||
getCoreRowModel: getCoreRowModel()
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
pagination = {
|
||||
pageIndex: currentPage - 1,
|
||||
pageSize: pageSize
|
||||
let scrollContainer = $state<HTMLDivElement>();
|
||||
let loadingTrigger = $state<HTMLDivElement>();
|
||||
|
||||
// Intersection Observer para detectar cuando el usuario llega al final
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const [entry] = entries;
|
||||
if (entry.isIntersecting && hasMore && !loading) {
|
||||
loadMore();
|
||||
}
|
||||
},
|
||||
{
|
||||
root: scrollContainer,
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
|
||||
if (loadingTrigger) {
|
||||
observer.observe(loadingTrigger);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
|
||||
function handlePreviousPage() {
|
||||
if (currentPage > 1) {
|
||||
onPageChange(currentPage - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function handleNextPage() {
|
||||
const totalPages = Math.ceil(totalItems / pageSize);
|
||||
if (currentPage < totalPages) {
|
||||
onPageChange(currentPage + 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="rounded-md border">
|
||||
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Header class="sticky top-0 bg-background z-10">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
@@ -107,33 +97,27 @@
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
|
||||
<!-- Loading Trigger - Se activa cuando es visible -->
|
||||
{#if hasMore}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-20 text-center">
|
||||
<div bind:this={loadingTrigger}>
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
<span class="text-muted-foreground text-sm">Cargando más...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-muted-foreground text-sm">
|
||||
Desplázate para cargar más
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
<div class="flex items-center justify-between space-x-2 py-4">
|
||||
<div class="text-muted-foreground text-sm">
|
||||
Mostrando {data.length} de {totalItems} registro(s)
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="text-muted-foreground text-sm">
|
||||
Página {currentPage} de {Math.ceil(totalItems / pageSize)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={handlePreviousPage}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={handleNextPage}
|
||||
disabled={currentPage >= Math.ceil(totalItems / pageSize)}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => {
|
||||
// Verificar si existe el token en las cookies
|
||||
const token = cookies.get('access_token');
|
||||
let token = cookies.get('access_token');
|
||||
const refreshToken = cookies.get('refresh_token');
|
||||
|
||||
// Si no hay token, redirigir al login
|
||||
if (!token) {
|
||||
@@ -11,20 +12,19 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => {
|
||||
throw redirect(303, `/login?redirect=${encodeURIComponent(url.pathname)}`);
|
||||
}
|
||||
|
||||
// Configurar la URL de la API
|
||||
let apiUrl = process.env.INTERNAL_API_URL;
|
||||
if (!apiUrl) {
|
||||
apiUrl = import.meta.env.VITE_API_URL;
|
||||
// Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR)
|
||||
apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend');
|
||||
}
|
||||
|
||||
// Normalizar la URL: asegurar que termine con '/'
|
||||
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
|
||||
|
||||
// Validar el token con el backend para asegurar que sea válido
|
||||
try {
|
||||
// En Docker, el servidor debe usar el nombre del servicio 'backend' en lugar de 'localhost'
|
||||
// VITE_API_URL ya incluye '/api/' al final (ej: http://localhost:8000/api/)
|
||||
let apiUrl = process.env.INTERNAL_API_URL;
|
||||
if (!apiUrl) {
|
||||
apiUrl = import.meta.env.VITE_API_URL;
|
||||
// Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR)
|
||||
apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend');
|
||||
}
|
||||
|
||||
// Normalizar la URL: asegurar que termine con '/'
|
||||
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
|
||||
|
||||
console.log('🔐 [Dashboard] Validando token con:', `${baseUrl}v1/auth/me`);
|
||||
|
||||
const response = await fetch(`${baseUrl}v1/auth/me`, {
|
||||
@@ -33,9 +33,76 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => {
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Token inválido, limpiar y redirigir
|
||||
// Si el token está expirado (401) y tenemos refresh token, intentar refrescar
|
||||
if (response.status === 401 && refreshToken) {
|
||||
console.log('🔄 [Dashboard] Token expirado, intentando refrescar...');
|
||||
|
||||
try {
|
||||
const refreshResponse = await fetch(`${baseUrl}v1/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ refresh_token: refreshToken })
|
||||
});
|
||||
|
||||
if (refreshResponse.ok) {
|
||||
const refreshData = await refreshResponse.json();
|
||||
|
||||
// Actualizar las cookies con los nuevos tokens
|
||||
cookies.set('access_token', refreshData.access_token, {
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24 * 7 // 7 días
|
||||
});
|
||||
|
||||
if (refreshData.refresh_token) {
|
||||
cookies.set('refresh_token', refreshData.refresh_token, {
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24 * 30 // 30 días
|
||||
});
|
||||
}
|
||||
|
||||
// Usar el nuevo token para obtener la info del usuario
|
||||
token = refreshData.access_token;
|
||||
console.log('✅ [Dashboard] Token refrescado exitosamente');
|
||||
|
||||
// Reintentar la validación con el nuevo token
|
||||
const retryResponse = await fetch(`${baseUrl}v1/auth/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (retryResponse.ok) {
|
||||
const userData = await retryResponse.json();
|
||||
return {
|
||||
authenticated: true,
|
||||
user: userData
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.log('❌ [Dashboard] Refresh token también está expirado');
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error('🔐 [Dashboard] Error al refrescar token:', refreshError);
|
||||
}
|
||||
|
||||
// Si llegamos aquí, el refresh falló
|
||||
cookies.delete('access_token', { path: '/' });
|
||||
cookies.delete('refresh_token', { path: '/' });
|
||||
throw redirect(303, `/login?redirect=${encodeURIComponent(url.pathname)}`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
// Token inválido y no se pudo refrescar, limpiar y redirigir
|
||||
cookies.delete('access_token', { path: '/' });
|
||||
cookies.delete('refresh_token', { path: '/' });
|
||||
throw redirect(303, `/login?redirect=${encodeURIComponent(url.pathname)}`);
|
||||
}
|
||||
|
||||
@@ -54,6 +121,7 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => {
|
||||
// Para cualquier otro error (conexión, etc), limpiar token y redirigir
|
||||
console.error('🔐 [Dashboard] Error validando token:', error);
|
||||
cookies.delete('access_token', { path: '/' });
|
||||
cookies.delete('refresh_token', { path: '/' });
|
||||
throw redirect(303, `/login?redirect=${encodeURIComponent(url.pathname)}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
|
||||
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
// Esperar a que el layout padre valide/refresque el token
|
||||
await parent();
|
||||
|
||||
const token = cookies.get('access_token');
|
||||
|
||||
if (!token) {
|
||||
|
||||
@@ -1,32 +1,107 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import type { CodePedimentoRegimen } from '$lib/api/dashboard/refrence_data/code_pedimento_regimens';
|
||||
import { onMount } from 'svelte';
|
||||
import { codePedimentoRegimensApi, type CodePedimentoRegimen } from '$lib/api/dashboard/refrence_data/code_pedimento_regimens';
|
||||
import DataTable from '$lib/components/dashboard/reference_data/code_pedimento_regimens/data-table.svelte';
|
||||
import { columns } from '$lib/components/dashboard/reference_data/code_pedimento_regimens/columns.js';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import type { PageData } from './$types';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// Los datos vienen del servidor a través del +page.server.ts
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Desestructurar los datos
|
||||
let items: CodePedimentoRegimen[] = $derived(data.items || []);
|
||||
let totalItems = $derived(data.total || 0);
|
||||
let currentPage = $derived(data.page || 1);
|
||||
let pageSize = $derived(data.page_size || 50);
|
||||
let error = $derived(data.error || null);
|
||||
// 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;
|
||||
};
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
// Navegar a la misma página con los parámetros de paginación
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('page', page.toString());
|
||||
url.searchParams.set('page_size', pageSize.toString());
|
||||
goto(url.toString());
|
||||
// Verificar si hay token en las cookies
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
|
||||
if (cookieToken && cookieToken !== localToken) {
|
||||
console.log('🔄 [Page] Sincronizando token de cookies a localStorage');
|
||||
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) {
|
||||
console.log('🔄 [Page] Sincronizando refresh_token de cookies a localStorage');
|
||||
localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll
|
||||
let allItems = $state<CodePedimentoRegimen[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
console.log(`📊 [Page] Cargando página ${currentPage + 1}...`);
|
||||
console.log(`📊 [Page] Token disponible:`, localStorage.getItem('access_token') ? 'Sí' : 'No');
|
||||
|
||||
const response = await codePedimentoRegimensApi.list(currentPage + 1, pageSize);
|
||||
|
||||
console.log(`📊 [Page] Respuesta recibida:`, {
|
||||
error: response.error,
|
||||
status: response.status,
|
||||
hasData: !!response.data
|
||||
});
|
||||
|
||||
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);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
console.log(`📊 [Page] Cargados ${response.data.items.length} items adicionales`);
|
||||
// Agregar los nuevos items al array existente
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('📊 [Page] Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reloadData() {
|
||||
// Forzar recarga de la página para obtener datos frescos
|
||||
// Reset y recargar desde el principio
|
||||
window.location.reload();
|
||||
}
|
||||
</script>
|
||||
@@ -76,7 +151,9 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Listado de Relaciones</Card.Title>
|
||||
<Card.Description>Total de registros: {totalItems}</Card.Description>
|
||||
<Card.Description>
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
</Card.Description>
|
||||
</div>
|
||||
<Button variant="outline" onclick={reloadData}>
|
||||
<svg
|
||||
@@ -99,14 +176,13 @@
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<!-- TanStack DataTable -->
|
||||
<!-- TanStack DataTable con Infinite Scroll -->
|
||||
<DataTable
|
||||
data={items}
|
||||
data={allItems}
|
||||
{columns}
|
||||
{totalItems}
|
||||
{currentPage}
|
||||
{pageSize}
|
||||
onPageChange={handlePageChange}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
Reference in New Issue
Block a user