Refactor invoice edit flow: enhance error handling, load reference data, and improve token management

This commit is contained in:
AlexeerCT
2025-12-29 12:53:06 -06:00
parent b939526302
commit 8d0a895c8d
6 changed files with 421 additions and 93 deletions

View File

@@ -134,48 +134,63 @@ export async function authenticatedFetch(
fetch: typeof globalThis.fetch,
redirectUrl?: string
): Promise<Response> {
const baseUrl = getServerApiUrl();
let { accessToken } = getAuthTokens(cookies);
try {
const baseUrl = getServerApiUrl();
let { accessToken } = getAuthTokens(cookies);
// Si no hay token, redirigir o lanzar error
if (!accessToken) {
if (redirectUrl) {
throw redirect(303, redirectUrl);
}
throw new Error('No access token available');
}
// Construir URL completa
const url = endpoint.startsWith('http') ? endpoint : `${baseUrl}${endpoint}`;
// Realizar la petición inicial
const headers = createAuthHeaders(accessToken, options.headers as Record<string, string>);
let response = await fetch(url, {
...options,
headers
});
// Si es 401, intentar refrescar el token
if (response.status === 401) {
const newToken = await refreshAccessToken(cookies, fetch);
if (newToken) {
// Reintentar la petición con el nuevo token
const newHeaders = createAuthHeaders(newToken, options.headers as Record<string, string>);
response = await fetch(url, {
...options,
headers: newHeaders
});
} else {
// No se pudo refrescar, limpiar y redirigir
clearAuthTokens(cookies);
// Si no hay token, redirigir o lanzar error
if (!accessToken) {
if (redirectUrl) {
throw redirect(303, redirectUrl);
}
throw new Error('No access token available');
}
}
return response;
// Construir URL completa
const url = endpoint.startsWith('http') ? endpoint : `${baseUrl}${endpoint}`;
// Realizar la petición inicial
const headers = createAuthHeaders(accessToken, options.headers as Record<string, string>);
let response = await fetch(url, {
...options,
headers
});
// Si es 401, intentar refrescar el token
if (response.status === 401) {
const newToken = await refreshAccessToken(cookies, fetch);
if (newToken) {
// Reintentar la petición con el nuevo token
const newHeaders = createAuthHeaders(newToken, options.headers as Record<string, string>);
response = await fetch(url, {
...options,
headers: newHeaders
});
} else {
// No se pudo refrescar, limpiar y redirigir
clearAuthTokens(cookies);
if (redirectUrl) {
throw redirect(303, redirectUrl);
}
}
}
return response;
} catch (error) {
// Si es un redirect, re-lanzarlo
if (error && typeof error === 'object' && 'status' in error && 'location' in error) {
throw error;
}
console.error('🔴 [API] Error en authenticatedFetch:', endpoint, error);
// Retornar una respuesta de error simulada en lugar de lanzar
return new Response(JSON.stringify({ error: 'Network error', details: String(error) }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
/**

View File

@@ -0,0 +1,80 @@
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
import { getAuthTokens, getActiveCompanyId, authenticatedFetch } from '$lib/server/api';
export const GET: RequestHandler = async ({ params, cookies, fetch }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw error(401, 'Not authenticated');
}
const companyId = await getActiveCompanyId(cookies, fetch);
if (!companyId) {
throw error(400, 'No company selected');
}
const invoiceId = parseInt(params.id);
if (isNaN(invoiceId)) {
throw error(400, 'Invalid invoice ID');
}
try {
// Cargar la factura y datos de referencia en paralelo
const [
invoiceResponse,
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
sealsResponse,
incotermsResponse,
pedimentosResponse
] = await Promise.all([
authenticatedFetch(`v1/a76/invoices/${invoiceId}?company_id=${companyId}`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/invoice-types/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch(`v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`, {}, cookies, fetch),
authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch(`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch(`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch)
]);
if (!invoiceResponse.ok) {
throw error(invoiceResponse.status, 'Error loading invoice');
}
const invoice = await invoiceResponse.json();
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
return json({
invoice,
invoiceTypes: invoiceTypes.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
pedimentos: pedimentos.items || []
});
} catch (err) {
console.error('Error loading invoice edit data:', err);
throw error(500, 'Error loading invoice');
}
};

View File

@@ -0,0 +1,67 @@
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { getAuthTokens, getActiveCompanyId, authenticatedFetch } from '$lib/server/api';
export const GET: RequestHandler = async ({ cookies, fetch }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
return json({ error: 'Not authenticated' }, { status: 401 });
}
const companyId = await getActiveCompanyId(cookies, fetch);
if (!companyId) {
return json({ error: 'No company selected' }, { status: 400 });
}
try {
// Cargar todos los datos de referencia en paralelo
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
sealsResponse,
incotermsResponse,
pedimentosResponse
] = await Promise.all([
authenticatedFetch('v1/public/refrence_data/invoice-types/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch(`v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`, {}, cookies, fetch),
authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch(`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
authenticatedFetch('v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch),
authenticatedFetch(`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch)
]);
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
return json({
invoiceTypes: invoiceTypes.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
pedimentos: pedimentos.items || []
});
} catch (err) {
console.error('Error loading reference data:', err);
return json({ error: 'Error loading reference data' }, { status: 500 });
}
};

View File

@@ -97,57 +97,80 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
// Si el ID es "new", es una creación
if (params.id === 'new') {
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
sealsResponse,
incotermsResponse,
pedimentosResponse
] = await Promise.all([
invoiceTypesPromise,
customsBrokersPromise,
clientsPromise,
providersPromise,
currencyTypesPromise,
transportTypesPromise,
sealsPromise,
incotermsPromise,
pedimentosPromise
]);
try {
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
sealsResponse,
incotermsResponse,
pedimentosResponse
] = await Promise.all([
invoiceTypesPromise,
customsBrokersPromise,
clientsPromise,
providersPromise,
currencyTypesPromise,
transportTypesPromise,
sealsPromise,
incotermsPromise,
pedimentosPromise
]);
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
return {
invoice: null,
invoiceId: null,
isCreate: true,
invoiceTypes: invoiceTypes.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
pedimentos: pedimentos.items || [],
// Filtros desde query parameters para preselección
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
return {
invoice: null,
invoiceId: null,
isCreate: true,
invoiceTypes: invoiceTypes.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
pedimentos: pedimentos.items || [],
// Filtros desde query parameters para preselección
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
} catch (err) {
console.error('Error loading data for new invoice:', err);
// En caso de error, devolver estructura mínima para que la página pueda cargar
return {
invoice: null,
invoiceId: null,
isCreate: true,
invoiceTypes: [],
customsBrokers: [],
clients: [],
providers: [],
currencyTypes: [],
transportTypes: [],
seals: [],
incoterms: [],
pedimentos: [],
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
}
}
const invoiceId = parseInt(params.id);

View File

@@ -1,4 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import * as Tabs from '$lib/components/ui/tabs';
import * as Alert from '$lib/components/ui/alert';
@@ -17,8 +19,6 @@
LoaderCircle,
Save
} from 'lucide-svelte';
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte';
import { companyStore } from '$lib/stores/company.svelte';
// Importar los componentes de cada pestaña
import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte';
@@ -34,8 +34,20 @@
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
// Get sidebar context
const sidebar = useSidebar();
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
let companyStore: any = $state(undefined);
let mounted = $state(false);
onMount(async () => {
try {
const companyStoreModule = await import('$lib/stores/company.svelte');
companyStore = companyStoreModule.companyStore;
mounted = true;
} catch (err) {
console.error('Error loading client modules:', err);
mounted = true;
}
});
interface ExtendedPageData {
invoiceId?: number | null;
@@ -471,7 +483,7 @@
if (data.isCreate) {
// Crear nueva factura con todos sus sub-recursos
const response = await invoicesApi.create(companyStore.activeCompany?.id || 0, payload as CreateInvoiceData);
const response = await invoicesApi.create(companyStore?.activeCompany?.id || 0, payload as CreateInvoiceData);
if (response.error) {
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
throw new Error(errorMsg);
@@ -484,7 +496,7 @@
return;
} else {
// Actualizar factura existente con todos sus sub-recursos
const response = await invoicesApi.update(invoiceId!, companyStore.activeCompany?.id || 0, payload as UpdateInvoiceData);
const response = await invoicesApi.update(invoiceId!, companyStore?.activeCompany?.id || 0, payload as UpdateInvoiceData);
if (response.error) throw new Error(response.error);
}
@@ -635,8 +647,7 @@
<!-- Footer fijo en la parte inferior -->
<div
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] transition-[left] duration-200 ease-linear"
style:left={sidebar.isMobile ? '0' : (sidebar.open ? 'var(--sidebar-width)' : '0')}
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5]"
>
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
<!-- Tabs Navigation -->

View File

@@ -0,0 +1,132 @@
import type { PageLoad } from './$types';
import { error } from '@sveltejs/kit';
// Deshabilitar SSR para esta página debido al layout de dashboard que usa stores del cliente
export const ssr = false;
export const load: PageLoad = async ({ params, url, parent }) => {
// Obtener datos del layout padre
const parentData = await parent();
const operationTypeParam = url.searchParams.get('operation_type');
const invoiceTypeParam = url.searchParams.get('invoice_type');
let parsedOperationType: number | null = null;
if (operationTypeParam) {
const parsed = parseInt(operationTypeParam, 10);
if (!isNaN(parsed)) {
parsedOperationType = parsed;
}
}
// Si el ID es "new", cargar datos de referencia
if (params.id === 'new') {
try {
// Llamar a la ruta de servidor que ya existe
const response = await fetch(`/api/invoices/reference-data`);
if (!response.ok) {
console.error('Error loading reference data:', response.status);
// Retornar estructura vacía en caso de error
return {
invoice: null,
invoiceId: null,
isCreate: true,
invoiceTypes: [],
customsBrokers: [],
clients: [],
providers: [],
currencyTypes: [],
transportTypes: [],
seals: [],
incoterms: [],
pedimentos: [],
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
}
const data = await response.json();
return {
invoice: null,
invoiceId: null,
isCreate: true,
invoiceTypes: data.invoiceTypes || [],
customsBrokers: data.customsBrokers || [],
clients: data.clients || [],
providers: data.providers || [],
currencyTypes: data.currencyTypes || [],
transportTypes: data.transportTypes || [],
seals: data.seals || [],
incoterms: data.incoterms || [],
pedimentos: data.pedimentos || [],
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
} catch (err) {
console.error('Error loading data for new invoice:', err);
return {
invoice: null,
invoiceId: null,
isCreate: true,
invoiceTypes: [],
customsBrokers: [],
clients: [],
providers: [],
currencyTypes: [],
transportTypes: [],
seals: [],
incoterms: [],
pedimentos: [],
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
}
}
// Si es un ID numérico, cargar la factura
const invoiceId = parseInt(params.id);
if (isNaN(invoiceId)) {
throw error(400, 'ID de factura inválido');
}
try {
const response = await fetch(`/api/invoices/${invoiceId}/edit-data`);
if (!response.ok) {
throw error(response.status, 'Error al cargar la factura');
}
const data = await response.json();
return {
invoice: data.invoice,
invoiceId,
isCreate: false,
invoiceTypes: data.invoiceTypes || [],
customsBrokers: data.customsBrokers || [],
clients: data.clients || [],
providers: data.providers || [],
currencyTypes: data.currencyTypes || [],
transportTypes: data.transportTypes || [],
seals: data.seals || [],
incoterms: data.incoterms || [],
pedimentos: data.pedimentos || [],
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
} catch (err) {
console.error('Error loading invoice:', err);
throw error(500, 'Error al cargar la factura');
}
};