feat: Implement consolidated catalog services and endpoints for pedimento and invoice creation/edition.

This commit is contained in:
Galindo97
2026-02-12 13:35:48 -06:00
parent 9d27066da1
commit ab5181f803
16 changed files with 1323 additions and 879 deletions

View File

@@ -101,5 +101,21 @@ export const classesApi = {
*/
createFA: (data: any, company_id: number): Promise<ApiResponse<any>> => {
return api.post(`/v1/a76/classes/fa?company_id=${company_id}`, data);
},
/**
* Get all classes with FA data in a single query (optimized, eliminates N+1)
*/
getWithFAData: (params: {
company_id: number;
page?: number;
page_size?: number;
}): Promise<ApiResponse<A76Class[]>> => {
const query = new URLSearchParams({
company_id: params.company_id.toString(),
page: (params.page || 1).toString(),
page_size: (params.page_size || 1000).toString()
});
return api.get(`/v1/a76/classes/with-fa-data?${query.toString()}`);
}
};

View File

@@ -392,6 +392,26 @@ export const invoicesApi = {
return api.delete(`/v1/a76/invoices/${invoiceId}?${params.toString()}`);
},
/**
* Obtiene datos para la creación de una factura (catálogos consolidados)
*/
getCreationData: (companyId: number) => {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.get<any>(`/v1/a76/invoices/creation-data?${params.toString()}`);
},
/**
* Obtiene datos para la edición de una factura (catálogos consolidados + factura)
*/
getEditionData: (invoiceId: number, companyId: number) => {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.get<any>(`/v1/a76/invoices/${invoiceId}/edition-data?${params.toString()}`);
},
// --- Nested Resources ---
/**

View File

@@ -26,358 +26,110 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
parsedOperationType = operationTypeParam;
}
// Cargar datos de referencia necesarios
const invoiceTypesPromise = authenticatedFetch(
'v1/public/reference_data/invoice-types/?page=1&page_size=100',
{},
cookies,
fetch
);
try {
let invoiceData: any = null;
let catalogsData: any = {};
let defaultSettings: any = null;
let isCreate = false;
const customsBrokersPromise = authenticatedFetch(
`v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`,
{},
cookies,
fetch
);
// Fetch Default Settings separately if applicable (for creation)
const settingsPromise = (params.id === 'new' && parsedOperationType && invoiceTypeParam && companyId)
? authenticatedFetch(
`v1/a76/invoice-settings/${invoiceTypeParam}?operation_type=${parsedOperationType}&company_id=${companyId}`,
{},
cookies,
fetch
).then(res => res.ok ? res.json() : null).catch(err => {
console.error('Error fetching defaults:', err);
return null;
})
: Promise.resolve(null);
// Cargar clientes y proveedores
const clientsPromise = authenticatedFetch(
`v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`,
{},
cookies,
fetch
);
const providersPromise = authenticatedFetch(
`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`,
{},
cookies,
fetch
);
// Cargar tipos de moneda, transporte, etc.
const currencyTypesPromise = authenticatedFetch(
'v1/public/reference_data/currency-types/?page=1&page_size=100',
{},
cookies,
fetch
);
const transportTypesPromise = authenticatedFetch(
'v1/public/reference_data/transport-types/?page=1&page_size=100',
{},
cookies,
fetch
);
const transportersPromise = authenticatedFetch(
`v1/a76/transportation/transporters/?company_id=${companyId}&page=1&page_size=100`,
{},
cookies,
fetch
);
const vehiclesPromise = authenticatedFetch(
`v1/a76/transportation/vehicles/?company_id=${companyId}&page=1&page_size=100`,
{},
cookies,
fetch
);
const driversPromise = authenticatedFetch(
`v1/a76/transportation/drivers/?company_id=${companyId}`,
{},
cookies,
fetch
);
const trailersPromise = authenticatedFetch(
`v1/a76/transportation/trailers/?company_id=${companyId}&page=1&page_size=100`,
{},
cookies,
fetch
);
const customsSectionsPromise = authenticatedFetch(
'v1/public/reference_data/customs-sections/?page=1&page_size=100',
{},
cookies,
fetch
);
const codePedimentoRegimensPromise = authenticatedFetch(
'v1/public/reference_data/code-pedimento-regimens/?page=1&page_size=1000',
{},
cookies,
fetch
);
const sealsPromise = authenticatedFetch(
`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`,
{},
cookies,
fetch
);
const incotermsPromise = authenticatedFetch(
'v1/public/reference_data/incoterms/?page=1&page_size=100',
{},
cookies,
fetch
);
const pedimentosPromise = authenticatedFetch(
`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`,
{},
cookies,
fetch
);
const transportModesPromise = authenticatedFetch(
'v1/public/reference_data/transport-modes/?page=1&page_size=100',
{},
cookies,
fetch
);
// Si el ID es "new", es una creación
if (params.id === 'new') {
try {
// Fetch default settings if type, operation and company are present
const settingsPromise = (parsedOperationType && invoiceTypeParam && companyId)
? authenticatedFetch(
`v1/a76/invoice-settings/${invoiceTypeParam}?operation_type=${parsedOperationType}&company_id=${companyId}`,
if (params.id === 'new') {
isCreate = true;
// Call Consolidated Creation Endpoint
const [creationResponse, settingsResult] = await Promise.all([
authenticatedFetch(
`v1/a76/invoices/creation-data?company_id=${companyId}`,
{},
cookies,
fetch
).catch((err) => {
console.error('Error fetching defaults in server load:', err);
return null;
})
: Promise.resolve(null);
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
transportersResponse,
vehiclesResponse,
driversResponse,
trailersResponse,
customsSectionsResponse,
codePedimentoRegimensResponse,
sealsResponse,
incotermsResponse,
pedimentosResponse,
transportModesResponse,
settingsResponse
] = await Promise.all([
invoiceTypesPromise,
customsBrokersPromise,
clientsPromise,
providersPromise,
currencyTypesPromise,
transportTypesPromise,
transportersPromise,
vehiclesPromise,
driversPromise,
trailersPromise,
customsSectionsPromise,
codePedimentoRegimensPromise,
sealsPromise,
incotermsPromise,
pedimentosPromise,
transportModesPromise,
),
settingsPromise
]);
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 transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] };
const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] };
const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] };
const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.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 transportModes = transportModesResponse.ok ? await transportModesResponse.json() : { items: [] };
let defaultSettings = null;
if (settingsResponse && settingsResponse.ok) {
const settingsData = await settingsResponse.json();
defaultSettings = settingsData.settings || null;
if (creationResponse.ok) {
catalogsData = await creationResponse.json();
} else {
console.error('Error fetching creation data:', creationResponse.status);
// We continuing with empty catalogs might be better than crashing?
// But UI will likely be broken. Let's rely on empty arrays initialization below.
}
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 || [],
transporters: transporters.items || [],
vehicles: vehicles.items || [],
drivers: drivers.items || [],
trailers: trailers.items || [],
customsSections: customsSections.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
pedimentos: pedimentos.items || [],
transportModes: transportModes.items || [],
defaultSettings,
// Filtros desde query parameters para preselección
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
if (settingsResult && settingsResult.settings) {
defaultSettings = settingsResult.settings;
}
} else {
// Edition Mode
const invoiceId = parseInt(params.id);
if (isNaN(invoiceId)) {
throw error(400, 'ID de factura inválido');
}
// Call Consolidated Edition Endpoint
const editionResponse = await authenticatedFetch(
`v1/a76/invoices/${invoiceId}/edition-data?company_id=${companyId}`,
{},
cookies,
fetch
);
if (!editionResponse.ok) {
if (editionResponse.status === 404) {
throw error(404, 'Factura no encontrada');
}
};
} 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: [],
transporters: [],
vehicles: [],
drivers: [],
trailers: [],
customsSections: [],
codePedimentoRegimens: [],
seals: [],
incoterms: [],
pedimentos: [],
transportModes: [],
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
}
}
throw error(editionResponse.status, 'Error al cargar la factura');
}
const invoiceId = parseInt(params.id);
if (isNaN(invoiceId)) {
throw error(400, 'ID de factura inválido');
}
try {
// Cargar la factura desde el backend
const response = await authenticatedFetch(
`v1/a76/invoices/${invoiceId}?company_id=${companyId}`,
{},
cookies,
fetch
);
if (!response.ok) {
throw error(response.status, 'Error al cargar la factura');
const editionData = await editionResponse.json();
catalogsData = editionData; // It includes catalogs + invoice
invoiceData = editionData.invoice;
}
const invoice = await response.json();
// Cargar también los datos de referencia para edición
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
transportersResponse,
vehiclesResponse,
driversResponse,
trailersResponse,
customsSectionsResponse,
codePedimentoRegimensResponse,
sealsResponse,
incotermsResponse,
pedimentosResponse,
transportModesResponse
] = await Promise.all([
invoiceTypesPromise,
customsBrokersPromise,
clientsPromise,
providersPromise,
currencyTypesPromise,
transportTypesPromise,
transportersPromise,
vehiclesPromise,
driversPromise,
trailersPromise,
customsSectionsPromise,
codePedimentoRegimensPromise,
sealsPromise,
incotermsPromise,
pedimentosPromise,
transportModesPromise
]);
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 transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] };
const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] };
const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] };
const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.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 transportModes = transportModesResponse.ok ? await transportModesResponse.json() : { items: [] };
// Map snake_case response to camelCase props expected by Svelte Page
// Providing empty arrays as defaults if something is missing
return {
invoice,
invoiceId,
isCreate: false,
invoiceTypes: invoiceTypes.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
transporters: transporters.items || [],
vehicles: vehicles.items || [],
drivers: drivers.items || [],
trailers: trailers.items || [],
customsSections: customsSections.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
pedimentos: pedimentos.items || [],
transportModes: transportModes.items || [],
// Filtros desde query parameters para preselección
invoice: invoiceData,
invoiceId: params.id === 'new' ? null : parseInt(params.id),
isCreate: isCreate,
invoiceTypes: catalogsData.invoice_types || [],
customsBrokers: catalogsData.customs_brokers || [],
clients: catalogsData.clients || [],
providers: catalogsData.providers || [],
currencyTypes: catalogsData.currency_types || [],
transportTypes: catalogsData.transport_types || [],
transporters: catalogsData.transporters || [],
vehicles: catalogsData.vehicles || [],
drivers: catalogsData.drivers || [],
trailers: catalogsData.trailers || [],
customsSections: catalogsData.customs_sections || [],
codePedimentoRegimens: catalogsData.code_pedimento_regimens || [],
seals: catalogsData.seals || [],
incoterms: catalogsData.incoterms || [],
pedimentos: catalogsData.pedimentos || [],
transportModes: catalogsData.transport_modes || [],
defaultSettings: defaultSettings,
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
} catch (err) {
console.error('Error loading invoice:', err);
throw error(500, 'Error al cargar la factura');
} catch (err: any) {
console.error('Error in load function:', err);
if (err && err.status === 404) throw err; // Propagate 404
throw error(500, 'Error interno al cargar la página');
}
};

View File

@@ -38,6 +38,7 @@
import ExchangeRateDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosEdicionFactura } from '$lib/config/shortcuts/dashboard/invoices/edit';
import { api } from '$lib/api';
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
let companyStore: any = $state(undefined);
@@ -241,7 +242,7 @@
// Estados para saber si existen datos previos
let observationExists = $state(!!data.defaultSettings?.observationFormData);
let itemsExists = $state(!!(data.defaultSettings?.itemsFormData?.items?.length));
let itemsExists = $state(!!data.defaultSettings?.itemsFormData?.items?.length);
let othersExists = $state(!!data.defaultSettings?.othersFormData);
let continuationExists = $state(!!data.defaultSettings?.continuationFormData);
@@ -525,7 +526,7 @@
}
let isLoadingDefaults = $state(false);
// Compute initial key from source data to avoid state reference warning
const initialLoadedKey = data.defaultSettings?.InvoiceTopFieldsFormData
? `${data.defaultSettings.InvoiceTopFieldsFormData.invoice_type || ''}-${data.defaultSettings.InvoiceTopFieldsFormData.operation_type || ''}`
@@ -552,22 +553,12 @@
isLoadingDefaults = true;
try {
const token = data.user?.token;
const headers: HeadersInit = {
'Content-Type': 'application/json'
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const res = await fetch(
`/api/v1/a76/invoice-settings/${InvoiceTopFieldsFormData.invoice_type}?operation_type=${InvoiceTopFieldsFormData.operation_type}&company_id=${companyStore.activeCompany.id}`,
{ headers }
const res = await api.get(
`/v1/a76/invoice-settings/${InvoiceTopFieldsFormData.invoice_type}?operation_type=${InvoiceTopFieldsFormData.operation_type}&company_id=${companyStore.activeCompany.id}`
);
if (res.ok) {
const settingsData = await res.json();
const settings = settingsData.settings || {};
if (res.success && res.data) {
const settings = res.data.settings || {};
// Mark as loaded even if empty to prevent retries for the same combination
lastLoadedKey = currentKey;
@@ -593,7 +584,7 @@
InvoiceTopFieldsFormData.operation_type || data.filters?.operation_type;
observationExists = !!settings.observationFormData;
itemsExists = !!(itemsFormData.items?.length);
itemsExists = !!itemsFormData.items?.length;
othersExists = !!settings.othersFormData;
continuationExists = !!settings.continuationFormData;

View File

@@ -16,72 +16,44 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
throw error(400, 'No se encontró una compañía seleccionada');
}
// Cargar pedimento_codes (datos de referencia)
const pedimentoCodesPromise = authenticatedFetch(
'v1/public/reference_data/pedimento-codes?page=1&page_size=100',
{},
cookies,
fetch
);
// Cargar customs_sections (datos de referencia)
const customsSectionsPromise = authenticatedFetch(
'v1/public/reference_data/customs-sections?page=1&page_size=100',
{},
cookies,
fetch
);
// Cargar customs_brokers (datos de referencia)
const customsBrokersPromise = authenticatedFetch(
`v1/a76/customs-brokers?company_id=${companyId}`,
{},
cookies,
fetch
);
// Cargar clientes (para el select de client_id)
const clientsPromise = authenticatedFetch(
`v1/a76/clients-providers?company_id=${companyId}&type=client&page=1&page_size=1000`,
{},
cookies,
fetch
);
// Cargar code-pedimento-regimens (para interdependencia de campos)
const codePedimentoRegimensPromise = authenticatedFetch(
'v1/public/reference_data/code-pedimento-regimens?page=1&page_size=100',
{},
cookies,
fetch
);
// Si el ID es "new", es una creación
if (params.id === 'new') {
try {
const [pedimentoCodesResponse, customsSectionsResponse, customsBrokersResponse, clientsResponse, codePedimentoRegimensResponse] = await Promise.all([
pedimentoCodesPromise,
customsSectionsPromise,
customsBrokersPromise,
clientsPromise,
codePedimentoRegimensPromise
]);
// Call consolidated creation endpoint
const response = await authenticatedFetch(
`v1/a76/pedimentos/creation-data?company_id=${companyId}`,
{},
cookies,
fetch
);
const pedimentoCodes = pedimentoCodesResponse.ok ? await pedimentoCodesResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
if (!response.ok) {
console.error('Error fetching creation data:', response.status);
// Graceful degradation: return empty data
return {
pedimento: null,
pedimentoId: null,
isCreate: true,
pedimentoCodes: [],
customsSections: [],
customsBrokers: [],
clients: [],
codePedimentoRegimens: [],
error: 'Error al cargar catálogos. Verifique la conexión con el backend.'
};
}
const data = await response.json();
return {
pedimento: null,
pedimentoId: null,
isCreate: true,
pedimentoCodes: pedimentoCodes.items || [],
customsSections: customsSections.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || []
pedimentoCodes: data.pedimento_codes || [],
customsSections: data.customs_sections || [],
customsBrokers: data.customs_brokers || [],
clients: data.clients || [],
codePedimentoRegimens: data.code_pedimento_regimens || []
};
} catch (e) {
console.error('❌ Error loading new pedimento data:', e);
@@ -106,11 +78,9 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
}
try {
// Cargar el pedimento desde el backend usando authenticatedFetch
// Call consolidated edition endpoint
const response = await authenticatedFetch(
`v1/a76/pedimentos/${pedimentoId}?company_id=${companyId}`,
`v1/a76/pedimentos/${pedimentoId}/edition-data?company_id=${companyId}`,
{},
cookies,
fetch
@@ -126,32 +96,17 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
throw error(response.status, 'Error al cargar el pedimento');
}
const pedimento = await response.json();
// Cargar pedimento_codes y customs_sections
const [pedimentoCodesResponse, customsSectionsResponse, customsBrokersResponse, clientsResponse, codePedimentoRegimensResponse] = await Promise.all([
pedimentoCodesPromise,
customsSectionsPromise,
customsBrokersPromise,
clientsPromise,
codePedimentoRegimensPromise
]);
const pedimentoCodes = pedimentoCodesResponse.ok ? await pedimentoCodesResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
const data = await response.json();
return {
pedimento,
pedimentoId,
pedimento: data.pedimento,
pedimentoId: data.pedimento_id,
isCreate: false,
pedimentoCodes: pedimentoCodes.items || [],
customsSections: customsSections.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
codePedimentoRegimens: codePedimentoRegimens.items || []
pedimentoCodes: data.pedimento_codes || [],
customsSections: data.customs_sections || [],
customsBrokers: data.customs_brokers || [],
clients: data.clients || [],
codePedimentoRegimens: data.code_pedimento_regimens || []
};
} catch (e) {
console.error('Error loading pedimento:', e);