Merge pull request 'fix/trailers' (#387) from fix/trailers into development

Reviewed-on: ADUANASOFT/anexo76#387
This commit is contained in:
2026-05-12 14:06:26 +00:00
9 changed files with 173 additions and 25 deletions

View File

@@ -40,11 +40,14 @@ export const statesApi = {
* Lista todos los estados con paginación y búsqueda
* 🛡️ CORREGIDO: Ahora requiere companyId
*/
list: (companyId: number, page = 1, pageSize = 50, search?: string) => {
list: (companyId: number, page = 1, pageSize = 50, search?: string, countryId?: string) => {
let url = `/v1/public/reference_data/states/?company_id=${companyId}&page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
if (countryId) {
url += `&country_id=${encodeURIComponent(countryId)}`;
}
return api.get<StateListResponse>(url);
},

View File

@@ -16,8 +16,14 @@ export interface TrailerTypeListResponse {
}
export const trailerTypesApi = {
list: (page = 1, pageSize = 100) =>
/**
* Lista los tipos de trailer con paginación
* @param companyId - ID de la empresa (requerido por RBAC)
* @param page - Número de página
* @param pageSize - Tamaño de página
*/
list: (companyId: number, page = 1, pageSize = 100) =>
api.get<TrailerTypeListResponse>(
`/v1/public/reference_data/trailer-types/?page=${page}&page_size=${pageSize}`
`/v1/public/reference_data/trailer-types/?company_id=${companyId}&page=${page}&page_size=${pageSize}`
)
};

View File

@@ -4,6 +4,7 @@
import { Input } from '$lib/components/ui/input';
import { Loader2, Search } from 'lucide-svelte';
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
import { companyStore } from '$lib/stores/company.svelte';
import { m } from '$lib/i18n/messages';
let {
@@ -28,7 +29,8 @@
loading = true;
error = '';
try {
const response = await statesApi.list(1, 100);
const companyId = companyStore.activeCompany?.id || 1;
const response = await statesApi.list(companyId, 1, 100);
if (response.data?.items) {
items = response.data.items;
filteredItems = items;

View File

@@ -5,6 +5,7 @@
import * as Table from '$lib/components/ui/table';
import { Search, Loader2, MapPin } from 'lucide-svelte';
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
import { companyStore } from '$lib/stores/company.svelte';
import { toast } from 'svelte-sonner';
// --- PROPS ---
@@ -102,7 +103,8 @@
// Note: statesApi.list takes page, pageSize, and searchTerm?
// Wait, let me check statesApi.list signature again.
// It only takes page and pageSize! I need to check if it supports search.
const response = await statesApi.list(page, pageSize);
const companyId = companyStore.activeCompany?.id || 1;
const response = await statesApi.list(companyId, page, pageSize);
if (response.error) {
toast.error(`Error: ${response.error}`);

View File

@@ -11,6 +11,7 @@
import { companyStore } from '$lib/stores/company.svelte';
import { browser } from '$app/environment';
import { onMount, onDestroy } from 'svelte';
import { getTrailerTypeDescription } from '$lib/i18n/trailer-types';
let {
open = $bindable(false),
@@ -53,7 +54,7 @@
];
const countryM3 = $derived(
countries.find((c) => c.ame_key === formData.country)?.m3_key ?? null
countries.find((c) => c.ame_key === formData.country || c.m3_key === formData.country)?.m3_key ?? null
);
const statesFiltered = $derived(
@@ -74,19 +75,31 @@
};
}
async function loadReferenceData() {
async function loadReferenceData(companyId: number) {
if (!browser) return;
refsLoading = true;
try {
const [tt, cc, ss] = await Promise.all([
trailerTypesApi.list(1, 100),
countriesApi.list(1, 100),
statesApi.list(1, 100)
const [tt, cc] = await Promise.all([
trailerTypesApi.list(companyId, 1, 100),
countriesApi.list(companyId, 1, 100)
]);
if (tt.data?.items) trailerTypes = tt.data.items;
if (cc.data?.items) countries = cc.data.items;
if (ss.data?.items) states = ss.data.items;
} catch {
if (tt.data?.items) trailerTypes = [...tt.data.items];
if (cc.data?.items) countries = [...cc.data.items];
// Si ya tenemos país, cargar sus estados
if (formData.country) {
const cM3 = countries.find(c => c.ame_key === formData.country || c.m3_key === formData.country)?.m3_key;
if (cM3) {
const ss = await statesApi.list(companyId, 1, 100, undefined, cM3);
if (ss.data?.items) states = [...ss.data.items];
}
} else {
// Cargar algunos estados por defecto (opcional)
const ss = await statesApi.list(companyId, 1, 100);
if (ss.data?.items) states = [...ss.data.items];
}
} catch (e) {
console.error('Error loading reference data:', e);
trailerTypes = [];
countries = [];
states = [];
@@ -95,18 +108,43 @@
}
}
// Efecto para recargar estados cuando cambia el país
$effect(() => {
const companyId = companyStore.activeCompany?.id;
if (!browser || !companyId || !formData.country) return;
const cM3 = countries.find(c => c.ame_key === formData.country || c.m3_key === formData.country)?.m3_key;
if (cM3) {
void statesApi.list(companyId, 1, 100, undefined, cM3).then(res => {
if (res.data?.items) {
states = [...res.data.items];
}
});
}
});
$effect(() => {
if (!open) {
error = null;
loading = false;
return;
}
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
if (item) {
formData = { ...item };
formData = {
...item,
state: item.state ?? '',
country: item.country ?? '',
trailer_type_key: item.trailer_type_key ?? ''
};
} else {
formData = emptyTrailerForm();
}
void loadReferenceData();
void loadReferenceData(companyId);
});
async function handleSubmit() {
@@ -222,7 +260,7 @@
{refsLoading
? 'Cargando tipos...'
: formData.trailer_type_key
? `${formData.trailer_type_key} ${trailerTypes.find((t) => t.trailer_type_key === formData.trailer_type_key)?.description ?? ''}`
? `${formData.trailer_type_key} ${getTrailerTypeDescription(formData.trailer_type_key, trailerTypes.find((t) => t.trailer_type_key === formData.trailer_type_key)?.description ?? '')}`
: '— Sin tipo (opcional) —'}
</Select.Trigger>
<Select.Content>
@@ -231,7 +269,9 @@
<Select.Item value={t.trailer_type_key} label={t.trailer_type_key}>
{t.trailer_type_key}
{#if t.description}
<span class="text-muted-foreground"> — {t.description}</span>
<span class="text-muted-foreground">
— {getTrailerTypeDescription(t.trailer_type_key, t.description)}</span
>
{/if}
</Select.Item>
{/each}

View File

@@ -66,11 +66,12 @@
async function loadReferenceData() {
if (!browser) return;
const companyId = companyStore.activeCompany?.id || 1;
refsLoading = true;
try {
const [cc, ss] = await Promise.all([
countriesApi.list(1, 100),
statesApi.list(1, 100)
countriesApi.list(companyId, 1, 100),
statesApi.list(companyId, 1, 100)
]);
if (cc.data?.items) countries = cc.data.items;
if (ss.data?.items) states = ss.data.items;

View File

@@ -90,16 +90,26 @@
refsLoading = true;
const cid = companyStore.activeCompany.id;
try {
const [tr, tt, cc, ss] = await Promise.all([
const [tr, tt, cc] = await Promise.all([
transportersApi.list(cid, { page: 1, page_size: 100 }),
transportTypesApi.list(cid, 1, 100),
countriesApi.list(1, 100),
statesApi.list(1, 100)
countriesApi.list(cid, 1, 100)
]);
if (tr.data?.items) transporters = tr.data.items;
if (tt.data?.items) transportTypes = tt.data.items;
if (cc.data?.items) countries = cc.data.items;
if (ss.data?.items) states = ss.data.items;
// Si ya tenemos país, cargar sus estados
if (formData.country) {
const cM3 = countries.find(c => c.ame_key === formData.country)?.m3_key;
if (cM3) {
const ss = await statesApi.list(cid, 1, 100, undefined, cM3);
if (ss.data?.items) states = ss.data.items;
}
} else {
const ss = await statesApi.list(cid, 1, 100);
if (ss.data?.items) states = ss.data.items;
}
} catch {
transporters = [];
transportTypes = [];
@@ -110,6 +120,21 @@
}
}
// Efecto para recargar estados cuando cambia el país
$effect(() => {
const cid = companyStore.activeCompany?.id;
if (!browser || !cid || !formData.country) return;
const cM3 = countries.find(c => c.ame_key === formData.country)?.m3_key;
if (cM3) {
void statesApi.list(cid, 1, 100, undefined, cM3).then(res => {
if (res.data?.items) {
states = res.data.items;
}
});
}
});
function emptyVehicleForm(): Vehicle {
return {
vehicle_key: '',

View File

@@ -0,0 +1,65 @@
import { getLocale } from '$lib/paraglide/runtime';
/**
* Mapeo de traducciones para el catálogo de Tipos de Trailer (GTipoTrailer).
* Se usa la clave (trailer_type_key) para obtener la descripción en español.
*/
const TRAILER_TYPE_ES: Record<string, string> = {
'20': 'Contenedor marítimo de 20 pies - Techo abierto',
'2B': 'Contenedor marítimo de 20 pies - Techo cerrado',
'40': 'Contenedor marítimo de 40 pies - Techo abierto',
'4B': 'Contenedor marítimo de 40 pies - Techo cerrado',
'BI': 'Remolque para bebidas',
'CB': 'Remolque de cuello de ganso',
'CH': 'Chasis',
'CL': 'Contenedor marítimo de otra longitud - Techo cerrado',
'CU': 'Contenedor marítimo de otra longitud - Techo abierto',
'CZ': 'Contenedor refrigerado',
'DD': 'Remolque de doble caída',
'DT': 'Remolque de caída trasera',
'FR': 'Remolque flat rack',
'FT': 'Plataforma / Cama plana',
'HC': 'Remolque tolva (cubierto)',
'HE': 'Remolque para caballos',
'HO': 'Remolque tolva (abierto)',
'HP': 'Remolque tolva (descarga neumática cubierto)',
'L1': 'Pipa / Tanque (líquidos) no caldeado / no aislado',
'L2': 'Pipa / Tanque (líquidos) caldeado / no aislado',
'L3': 'Pipa / Tanque (líquidos) no caldeado / aislado',
'L4': 'Pipa / Tanque (líquidos) caldeado / aislado',
'LP': 'Remolque para troncos / tubería / postes',
'LT': 'Remolque para ganado',
'NC': 'Sin equipo',
'OE': 'Otro',
'RD': 'Remolque de rack fijo / doble caída',
'RG': 'Góndola cerrada',
'RO': 'Góndola abierta',
'RS': 'Remolque de rack fijo / caída simple',
'SD': 'Remolque de caída simple',
'T1': 'Pipa / Tanque (gas) no caldeado / no aislado',
'T2': 'Pipa / Tanque (gas) caldeado / no aislado',
'T3': 'Pipa / Tanque (gas) no caldeado / aislado',
'T4': 'Pipa / Tanque (gas) caldeado / aislado',
'T5': 'Pipa / Tanque (químicos) no caldeado / no aislado',
'T6': 'Pipa / Tanque (químicos) caldeado / no aislado',
'T7': 'Pipa / Tanque (químicos) no caldeado / aislado',
'T8': 'Pipa / Tanque (químicos) caldeado / aislado',
'TC': 'Portavehículos / Nodriza',
'TK': 'Pipa / Tanque (líquidos grado alimenticio)',
'TL': 'Semirremolque',
'TW': 'Remolque de temperatura controlada'
};
/**
* Obtiene la descripción traducida de un tipo de trailer.
* @param key Clave del tipo de trailer (ej: '20', 'FT')
* @param fallback Descripción original por si no hay traducción
* @returns La descripción en el idioma activo
*/
export function getTrailerTypeDescription(key: string, fallback: string = ''): string {
const locale = getLocale();
if (locale.startsWith('es')) {
return TRAILER_TYPE_ES[key] || fallback;
}
return fallback;
}