feat: Enhance Pedimentos CRUD operations with related data handling

- Updated PedimentosService to create and update related tables for Pedimentos.
- Added company_id filtering in queries for Pedimentos.
- Improved error handling and logging during creation and update processes.
- Refactored router tags for consistency and clarity.
- Adjusted API endpoints for customs brokers to include company_id in requests.
- Enhanced company selection logic in various components to prioritize active company.
- Implemented event listeners for company changes to reload data dynamically.
- Updated frontend components to handle loading states and errors more effectively.
- Ensured all relevant routes and API calls are aligned with the new company context.
This commit is contained in:
2025-11-14 18:14:33 -06:00
parent aa35635397
commit ba40123333
19 changed files with 739 additions and 149 deletions

View File

@@ -88,32 +88,40 @@ export interface CreateCustomsBrokerData {
* API para Agentes Aduanales
*/
export const customsBrokersApi = {
/**
* Lista todos los agentes aduanales
*/
list: (companyId: string) => {
return api.get<CustomsBroker[]>(`/v1/a76/customs-brokers?company_id=${companyId}`);
},
/**
* Obtiene un agente aduanal por su clave
*/
get: (brokerKey: string) => {
return api.get<CustomsBroker>(`/api/v1/a76/customs-broker/${brokerKey}`);
return api.get<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}`);
},
/**
* Crea un nuevo agente aduanal
*/
create: (data: CreateCustomsBrokerData) => {
return api.post<CustomsBroker>('/api/v1/a76/customs-broker', data);
const companyId = data.company_id;
return api.post<CustomsBroker>(`/v1/a76/customs-brokers?company_id=${companyId}`, data);
},
/**
* Elimina un agente aduanal
*/
delete: (brokerKey: string) => {
return api.delete<CustomsBroker>(`/api/v1/a76/customs-broker/${brokerKey}`);
delete: (brokerKey: string, companyId: string) => {
return api.delete<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`);
},
/**
* Actualiza la información de VU de un agente aduanal
*/
updateVU: (brokerKey: string, data: CustomsBrokerVU) => {
return api.put<CustomsBrokerVU>(`/api/v1/a76/customs-broker-vu/${brokerKey}`, data);
return api.put<CustomsBrokerVU>(`/v1/a76/customs-broker-vu/${brokerKey}`, data);
},
/**
@@ -121,7 +129,7 @@ export const customsBrokersApi = {
*/
updatePersonnel: (brokerKey: string, line: number, data: CustomsBrokerPersonnel) => {
return api.put<CustomsBrokerPersonnel>(
`/api/v1/a76/customs-broker-personnel/${brokerKey}/${line}`,
`/v1/a76/customs-broker-personnel/${brokerKey}/${line}`,
data
);
}

View File

@@ -23,7 +23,7 @@
error = null;
try {
const response = await customsBrokersApi.delete(broker.broker_key);
const response = await customsBrokersApi.delete(broker.broker_key, broker.company_id);
if (response.error) {
error = response.error;

View File

@@ -55,13 +55,13 @@ class CompanyStore {
if (savedId) {
const company = this._companies.find(c => c.id === parseInt(savedId));
if (company) {
this.setActiveCompany(company);
this.setActiveCompany(company, true); // silent=true para inicialización
return;
}
}
}
// Si no hay guardada, seleccionar la primera
this.setActiveCompany(this._companies[0]);
this.setActiveCompany(this._companies[0], true); // silent=true para inicialización
}
return;
}
@@ -90,7 +90,7 @@ class CompanyStore {
// Si hay compañías y no hay una activa, seleccionar la primera
if (this._companies.length > 0 && !this._activeCompany) {
this.setActiveCompany(this._companies[0]);
this.setActiveCompany(this._companies[0], true); // silent=true para inicialización
}
} else {
console.error('Error loading companies:', response.statusText);
@@ -108,8 +108,11 @@ class CompanyStore {
/**
* Establece la compañía activa
* @param company - La compañía a establecer como activa
* @param silent - Si es true, no dispara el evento companyChanged (para inicialización)
*/
setActiveCompany(company: Company) {
setActiveCompany(company: Company, silent: boolean = false) {
const previousCompanyId = this._activeCompany?.id;
this._activeCompany = company;
// Guardar en localStorage para persistencia
@@ -122,8 +125,10 @@ class CompanyStore {
document.cookie = `active_company_id=${company.id}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
}
// Despachar evento personalizado para que otros componentes reaccionen
if (typeof window !== 'undefined') {
// Despachar evento personalizado solo si:
// 1. No es silent (no es inicialización)
// 2. Y realmente cambió la compañía (el ID es diferente)
if (!silent && typeof window !== 'undefined' && previousCompanyId !== company.id) {
window.dispatchEvent(new CustomEvent('companyChanged', {
detail: { companyId: company.id }
}));

View File

@@ -38,13 +38,11 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => {
cookies.delete('active_company_id', { path: '/' });
}
return json({ error: 'Failed to fetch companies' }, { status: response.status });
}
console.log('✅ Token', token);
}
const companies = await response.json();
return json(companies);
} catch (error) {
console.error('Error fetching companies:', error);
console.log('✅ Token', token);
console.error('Error fetching companies:', error);
return json({ error: 'Internal server error' }, { status: 500 });
}
};

View File

@@ -23,11 +23,18 @@ 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');
// Obtener company_id de la URL o de las companies del usuario
// Obtener company_id de múltiples fuentes (en orden de prioridad):
// 1. URL query param (permite cambiar vía navegación)
// 2. Cookie active_company_id (setted por el team-switcher)
// 3. Primera compañía del usuario (fallback)
const companyIdParam = url.searchParams.get('company_id');
const cookieCompanyId = cookies.get('active_company_id');
const companyId = companyIdParam
? parseInt(companyIdParam)
: parentData.companies?.[0]?.id; // Usar la primera compañía por defecto
: cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {

View File

@@ -42,6 +42,19 @@
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
// Escuchar cambios de compañía
const handleCompanyChange = (event: CustomEvent) => {
// Recargar la página para obtener datos de la nueva compañía
reloadData();
};
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
// Cleanup
return () => {
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
};
}
});
@@ -97,9 +110,45 @@
}
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
async function reloadData() {
// Reset y recargar desde el principio usando la API
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const response = await clientsProvidersApi.list(
companyStore.activeCompany.id,
1,
pageSize
);
if (response.error) {
console.error('📊 [Page] Error en reloadData:', 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) {
// Reemplazar todos los items con los nuevos datos
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error recargando datos';
console.error('📊 [Page] Error reloading:', e);
} finally {
loading = false;
}
}
function handleCreateClick() {

View File

@@ -1,7 +1,7 @@
import type { PageServerLoad } from './$types';
import { getAuthTokens } from '$lib/server/api';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, parent }) => {
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
// Esperar a que el layout padre valide/refresque el token
const parentData = await parent();
@@ -10,12 +10,75 @@ export const load: PageServerLoad = async ({ cookies, parent }) => {
if (!accessToken) {
return {
error: 'No authenticated',
brokers: [],
companies: parentData.companies || []
};
}
return {
companies: parentData.companies || [],
error: null
};
try {
// Obtener company_id de múltiples fuentes (en orden de prioridad):
// 1. URL query param (permite cambiar vía navegación)
// 2. Cookie active_company_id (setted por el team-switcher)
// 3. Primera compañía del usuario (fallback)
const companyIdParam = url.searchParams.get('company_id');
const cookieCompanyId = cookies.get('active_company_id');
const companyId = companyIdParam
? parseInt(companyIdParam)
: cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
error: 'No company selected',
brokers: [],
companies: parentData.companies || []
};
}
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/a76/customs-brokers?company_id=${companyId}`,
{},
cookies,
fetch
);
if (!response.ok) {
const errorText = await response.text();
console.error('📊 [CustomsBrokers] API Error:', {
status: response.status,
statusText: response.statusText,
error: errorText
});
return {
error: `Error ${response.status}: ${response.statusText}`,
brokers: [],
companies: parentData.companies || [],
currentCompanyId: companyId
};
}
const data = await response.json();
// El endpoint devuelve un objeto con items, total, page, page_size
// Extraer el array de items
const brokers = Array.isArray(data) ? data : (data.items || []);
return {
brokers: brokers,
error: null,
companies: parentData.companies || [],
currentCompanyId: companyId
};
} catch (error) {
console.error('📊 [CustomsBrokers] Load error:', error);
return {
error: 'Error loading data',
brokers: [],
companies: parentData.companies || []
};
}
};

View File

@@ -18,6 +18,11 @@
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Estado para la lista de agentes aduanales (inicializar con datos del servidor)
let brokersList = $state<CustomsBroker[]>(data.brokers || []);
let listLoading = $state(false);
let listError = $state<string | null>(data.error || null);
// Estado para búsqueda
let searchKey = $state('');
let searchedBroker = $state<CustomsBroker | null>(null);
@@ -50,6 +55,19 @@
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
// Escuchar cambios de compañía
const handleCompanyChange = (event: CustomEvent) => {
// Recargar la página para obtener datos de la nueva compañía
reloadData();
};
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
// Cleanup
return () => {
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
};
}
});
@@ -91,11 +109,44 @@
}
}
function reloadData() {
async function reloadData() {
// Limpiar búsqueda
searchKey = '';
searchedBroker = null;
searchError = null;
// Recargar lista de brokers desde la API
if (!companyStore.activeCompany) return;
listLoading = true;
listError = null;
try {
const response = await customsBrokersApi.list(companyStore.activeCompany.id.toString());
if (response.error) {
console.error('📊 [CustomsBrokers] Error en reloadData:', response.error);
if (response.status === 401 || response.status === 403) {
listError = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
listError = response.error;
}
return;
}
if (response.data) {
// Reemplazar la lista con los nuevos datos
brokersList = response.data;
}
} catch (e) {
listError = 'Error recargando datos';
console.error('📊 [CustomsBrokers] Error reloading:', e);
} finally {
listLoading = false;
}
}
function handleCreateClick() {
@@ -110,8 +161,8 @@
// Crear columnas con el callback onSuccess
const columns = createColumns(handleSuccess);
// Array para mostrar en la tabla (vacío o con el broker buscado)
const brokers = $derived(searchedBroker ? [searchedBroker] : []);
// Array para mostrar en la tabla (búsqueda o lista completa)
const brokers = $derived(searchedBroker ? [searchedBroker] : brokersList);
</script>
<div class="space-y-6">
@@ -227,26 +278,49 @@
</Card.Root>
<!-- Resultados -->
{#if searchedBroker}
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Resultado de la Búsqueda</Card.Title>
<Card.Description>
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>
{#if searchedBroker}
Resultado de la Búsqueda
{:else}
Agentes Aduanales
{/if}
</Card.Title>
<Card.Description>
{#if searchedBroker}
Se encontró 1 agente aduanal
</Card.Description>
{:else if listLoading}
Cargando agentes aduanales...
{:else}
Total: {brokersList.length} agente{brokersList.length !== 1 ? 's' : ''} aduanal{brokersList.length !== 1 ? 'es' : ''}
{/if}
</Card.Description>
</div>
</div>
</Card.Header>
<Card.Content>
{#if listError}
<div class="rounded-lg border border-destructive bg-destructive/10 p-4 text-sm text-destructive">
{listError}
</div>
{:else if listLoading}
<div class="flex items-center justify-center py-8">
<div class="flex items-center gap-2 text-muted-foreground">
<div class="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
Cargando agentes aduanales...
</div>
</div>
</Card.Header>
<Card.Content>
{:else}
<DataTable
data={brokers}
{columns}
/>
</Card.Content>
</Card.Root>
{/if}
{/if}
</Card.Content>
</Card.Root>
</div>
<!-- Diálogo de crear -->

View File

@@ -2,11 +2,13 @@ import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import {
getAuthTokens,
getActiveCompanyId,
authenticatedFetch
} from '$lib/server/api';
export const load: PageServerLoad = async ({ fetch, cookies }) => {
export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
// Esperar a que el layout padre valide/refresque el token
const parentData = await parent();
// Verificar autenticación
const { accessToken } = getAuthTokens(cookies);
@@ -15,8 +17,18 @@ export const load: PageServerLoad = async ({ fetch, cookies }) => {
}
try {
// Obtener el company_id de la cookie o usar la primera disponible
const companyId = await getActiveCompanyId(cookies, fetch);
// Obtener company_id de múltiples fuentes (en orden de prioridad):
// 1. URL query param (permite cambiar vía navegación)
// 2. Cookie active_company_id (setted por el team-switcher)
// 3. Primera compañía del usuario (fallback)
const companyIdParam = url.searchParams.get('company_id');
const cookieCompanyId = cookies.get('active_company_id');
const companyId = companyIdParam
? parseInt(companyIdParam)
: cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
// Si aún no hay companyId, mostrar error
if (!companyId) {
@@ -25,7 +37,8 @@ export const load: PageServerLoad = async ({ fetch, cookies }) => {
total: 0,
page: 1,
page_size: 50,
error: 'No se encontró una compañía seleccionada'
error: 'No se encontró una compañía seleccionada',
companies: parentData.companies || []
};
}
@@ -44,7 +57,9 @@ export const load: PageServerLoad = async ({ fetch, cookies }) => {
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar pedimentos'
error: 'Error al cargar pedimentos',
companies: parentData.companies || [],
currentCompanyId: companyId
};
}
@@ -54,7 +69,9 @@ export const load: PageServerLoad = async ({ fetch, cookies }) => {
items: data.items || [],
total: data.total || 0,
page: data.page || 1,
page_size: data.page_size || 50
page_size: data.page_size || 50,
companies: parentData.companies || [],
currentCompanyId: companyId
};
} catch (error) {
console.error('Error loading pedimentos:', error);
@@ -63,7 +80,8 @@ export const load: PageServerLoad = async ({ fetch, cookies }) => {
total: 0,
page: 1,
page_size: 50,
error: 'Error al cargar pedimentos'
error: 'Error al cargar pedimentos',
companies: parentData.companies || []
};
}
};

View File

@@ -9,6 +9,7 @@
import { Label } from '$lib/components/ui/label';
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
@@ -46,12 +47,25 @@
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
// Escuchar cambios de compañía
const handleCompanyChange = (event: CustomEvent) => {
// Recargar los datos sin recargar la página completa
reloadData();
};
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
// Cleanup
return () => {
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
};
}
});
// Estado para infinite scroll
let allItems = $state<Pedimento[]>(data.items || []);
let currentPage = $state(data.page || 1);
let currentPage = $state(data.page);
let pageSize = $state(50);
let totalItems = $state(data.total || 0);
let loading = $state(false);
@@ -65,13 +79,15 @@
error = null;
try {
const companyId = companyStore.activeCompany?.id;
const filterParams = {
status: filters.status || undefined,
client_id: filters.client_id ? parseInt(filters.client_id) : undefined,
year: filters.year || undefined
};
const response = await pedimentosApi.list(currentPage + 1, pageSize, filterParams);
const response = await pedimentosApi.list(currentPage + 1, pageSize, filterParams, companyId);
if (response.error) {
console.error('📊 [Page] Error en loadMore:', response.error, 'Status:', response.status);
@@ -109,13 +125,15 @@
error = null;
try {
const companyId = companyStore.activeCompany?.id || 1;
const filterParams = {
status: filters.status || undefined,
client_id: filters.client_id ? parseInt(filters.client_id) : undefined,
year: filters.year || undefined
};
const response = await pedimentosApi.list(1, pageSize, filterParams);
const response = await pedimentosApi.list(1, pageSize, filterParams, companyId);
if (response.error) {
console.error('📊 [Page] Error aplicando filtros:', response.error);
@@ -150,12 +168,52 @@
client_id: '',
year: ''
};
window.location.reload();
applyFilters();
}
function reloadData() {
// Reset y recargar desde el principio
window.location.reload();
async function reloadData() {
// Reset y recargar desde el principio usando la API
if (!companyStore.activeCompany) return;
loading = true;
error = null;
try {
const companyId = companyStore.activeCompany.id;
const filterParams = {
status: filters.status || undefined,
client_id: filters.client_id ? parseInt(filters.client_id) : undefined,
year: filters.year || undefined
};
const response = await pedimentosApi.list(1, pageSize, filterParams, companyId);
if (response.error) {
console.error('📊 [Pedimentos] Error en reloadData:', 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) {
// Reemplazar todos los items con los nuevos datos
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error recargando datos';
console.error('📊 [Pedimentos] Error reloading:', e);
} finally {
loading = false;
}
}
function handleCreateClick() {

View File

@@ -1,6 +1,6 @@
import type { PageServerLoad } from './$types';
import { error, redirect } from '@sveltejs/kit';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
import { getAuthTokens, getActiveCompanyId, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
const { accessToken } = getAuthTokens(cookies);
@@ -24,9 +24,16 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
}
try {
// Obtener el company_id de la cookie
const companyId = await getActiveCompanyId(cookies, fetch);
if (!companyId) {
throw error(400, 'No se encontró una compañía seleccionada');
}
// Cargar el pedimento desde el backend usando authenticatedFetch
const response = await authenticatedFetch(
`v1/a76/pedimentos/${pedimentoId}`,
`v1/a76/pedimentos/${pedimentoId}?company_id=${companyId}`,
{},
cookies,
fetch