feat: Implement reference data management for states, transport modes, transport types, and valuation methods

- Added server-side loading logic for states, transport modes, transport types, and valuation methods with pagination support.
- Created Svelte components for displaying and managing states, transport modes, transport types, and valuation methods.
- Implemented infinite scroll functionality for loading more data as the user scrolls.
- Added error handling and user feedback for API interactions.
- Included dialogs for creating and editing entries in each reference data category.
This commit is contained in:
2025-11-02 16:54:27 -06:00
parent 27b5880524
commit ab57c6cd79
141 changed files with 13989 additions and 2 deletions

View File

@@ -0,0 +1,81 @@
import type { PageServerLoad } from './$types';
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) {
return {
error: 'No authenticated',
items: [],
total: 0,
page: 1,
page_size: 50
};
}
try {
// Obtener parámetros de paginación de la URL
const page = parseInt(url.searchParams.get('page') || '1');
const pageSize = parseInt(url.searchParams.get('page_size') || '50');
// Configurar la URL de la API para SSR
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
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
const response = await fetch(
`${baseUrl}v1/payment-methods?page=${page}&page_size=${pageSize}`,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
const errorText = await response.text();
console.error('📊 [Payment Methods] API Error:', {
status: response.status,
statusText: response.statusText,
error: errorText
});
return {
error: `Error ${response.status}: ${response.statusText}`,
items: [],
total: 0,
page: page,
page_size: pageSize
};
}
const data = await response.json();
return {
items: data.items || [],
total: data.total || 0,
page: data.page || page,
page_size: data.page_size || pageSize,
error: null
};
} catch (error) {
console.error('📊 [Payment Methods] Load error:', error);
return {
error: 'Error loading data',
items: [],
total: 0,
page: 1,
page_size: 50
};
}
};

View File

@@ -0,0 +1,196 @@
<script lang="ts">
import { onMount } from 'svelte';
import { paymentMethodsApi, type PaymentMethod } from '$lib/api/dashboard/refrence_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 CreateEditDialog from '$lib/components/dashboard/reference_data/payment_methods/create-edit-dialog.svelte';
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 iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// 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<PaymentMethod[]>(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 {
const response = await paymentMethodsApi.list(currentPage + 1, pageSize);
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) {
// 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() {
// Reset y recargar desde el principio
window.location.reload();
}
function handleCreateClick() {
showCreateDialog = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
// Crear columnas con el callback onSuccess
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl 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>
<Button onclick={handleCreateClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>
Nuevo Método de Pago
</Button>
</div>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Métodos de Pago</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
</svg>
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />