Merge pull request 'fix/trailers' (#387) from fix/trailers into development
Reviewed-on: ADUANASOFT/anexo76#387
This commit is contained in:
@@ -22,12 +22,16 @@ async def list_states(
|
|||||||
page: int = Query(1, ge=1, description="Número de página"),
|
page: int = Query(1, ge=1, description="Número de página"),
|
||||||
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
|
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
|
||||||
search: str = Query(None, description="Término de búsqueda"),
|
search: str = Query(None, description="Término de búsqueda"),
|
||||||
|
country_id: str = Query(None, description="Filtrar por país (m3_key)"),
|
||||||
db: Session = Depends(get_core_db),
|
db: Session = Depends(get_core_db),
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
skip = (page - 1) * page_size
|
skip = (page - 1) * page_size
|
||||||
query = db.query(State)
|
query = db.query(State)
|
||||||
|
|
||||||
|
if country_id:
|
||||||
|
query = query.filter(State.m3_key == country_id)
|
||||||
|
|
||||||
if search:
|
if search:
|
||||||
search_filter = f"%{search}%"
|
search_filter = f"%{search}%"
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
|
|||||||
@@ -40,11 +40,14 @@ export const statesApi = {
|
|||||||
* Lista todos los estados con paginación y búsqueda
|
* Lista todos los estados con paginación y búsqueda
|
||||||
* 🛡️ CORREGIDO: Ahora requiere companyId
|
* 🛡️ 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}`;
|
let url = `/v1/public/reference_data/states/?company_id=${companyId}&page=${page}&page_size=${pageSize}`;
|
||||||
if (search) {
|
if (search) {
|
||||||
url += `&search=${encodeURIComponent(search)}`;
|
url += `&search=${encodeURIComponent(search)}`;
|
||||||
}
|
}
|
||||||
|
if (countryId) {
|
||||||
|
url += `&country_id=${encodeURIComponent(countryId)}`;
|
||||||
|
}
|
||||||
return api.get<StateListResponse>(url);
|
return api.get<StateListResponse>(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,14 @@ export interface TrailerTypeListResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const trailerTypesApi = {
|
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>(
|
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}`
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Loader2, Search } from 'lucide-svelte';
|
import { Loader2, Search } from 'lucide-svelte';
|
||||||
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
|
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
|
||||||
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
import { m } from '$lib/i18n/messages';
|
import { m } from '$lib/i18n/messages';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -28,7 +29,8 @@
|
|||||||
loading = true;
|
loading = true;
|
||||||
error = '';
|
error = '';
|
||||||
try {
|
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) {
|
if (response.data?.items) {
|
||||||
items = response.data.items;
|
items = response.data.items;
|
||||||
filteredItems = items;
|
filteredItems = items;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
import { Search, Loader2, MapPin } from 'lucide-svelte';
|
import { Search, Loader2, MapPin } from 'lucide-svelte';
|
||||||
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
|
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
|
||||||
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
// --- PROPS ---
|
// --- PROPS ---
|
||||||
@@ -102,7 +103,8 @@
|
|||||||
// Note: statesApi.list takes page, pageSize, and searchTerm?
|
// Note: statesApi.list takes page, pageSize, and searchTerm?
|
||||||
// Wait, let me check statesApi.list signature again.
|
// Wait, let me check statesApi.list signature again.
|
||||||
// It only takes page and pageSize! I need to check if it supports search.
|
// 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) {
|
if (response.error) {
|
||||||
toast.error(`Error: ${response.error}`);
|
toast.error(`Error: ${response.error}`);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import { companyStore } from '$lib/stores/company.svelte';
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import { getTrailerTypeDescription } from '$lib/i18n/trailer-types';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
open = $bindable(false),
|
open = $bindable(false),
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const countryM3 = $derived(
|
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(
|
const statesFiltered = $derived(
|
||||||
@@ -74,19 +75,31 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadReferenceData() {
|
async function loadReferenceData(companyId: number) {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
refsLoading = true;
|
refsLoading = true;
|
||||||
try {
|
try {
|
||||||
const [tt, cc, ss] = await Promise.all([
|
const [tt, cc] = await Promise.all([
|
||||||
trailerTypesApi.list(1, 100),
|
trailerTypesApi.list(companyId, 1, 100),
|
||||||
countriesApi.list(1, 100),
|
countriesApi.list(companyId, 1, 100)
|
||||||
statesApi.list(1, 100)
|
|
||||||
]);
|
]);
|
||||||
if (tt.data?.items) trailerTypes = tt.data.items;
|
if (tt.data?.items) trailerTypes = [...tt.data.items];
|
||||||
if (cc.data?.items) countries = cc.data.items;
|
if (cc.data?.items) countries = [...cc.data.items];
|
||||||
if (ss.data?.items) states = ss.data.items;
|
|
||||||
} catch {
|
// 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 = [];
|
trailerTypes = [];
|
||||||
countries = [];
|
countries = [];
|
||||||
states = [];
|
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(() => {
|
$effect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
error = null;
|
error = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const companyId = companyStore.activeCompany?.id;
|
||||||
|
if (!companyId) return;
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
formData = { ...item };
|
formData = {
|
||||||
|
...item,
|
||||||
|
state: item.state ?? '',
|
||||||
|
country: item.country ?? '',
|
||||||
|
trailer_type_key: item.trailer_type_key ?? ''
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
formData = emptyTrailerForm();
|
formData = emptyTrailerForm();
|
||||||
}
|
}
|
||||||
void loadReferenceData();
|
|
||||||
|
void loadReferenceData(companyId);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
@@ -222,7 +260,7 @@
|
|||||||
{refsLoading
|
{refsLoading
|
||||||
? 'Cargando tipos...'
|
? 'Cargando tipos...'
|
||||||
: formData.trailer_type_key
|
: 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) —'}
|
: '— Sin tipo (opcional) —'}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
@@ -231,7 +269,9 @@
|
|||||||
<Select.Item value={t.trailer_type_key} label={t.trailer_type_key}>
|
<Select.Item value={t.trailer_type_key} label={t.trailer_type_key}>
|
||||||
{t.trailer_type_key}
|
{t.trailer_type_key}
|
||||||
{#if t.description}
|
{#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}
|
{/if}
|
||||||
</Select.Item>
|
</Select.Item>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -66,11 +66,12 @@
|
|||||||
|
|
||||||
async function loadReferenceData() {
|
async function loadReferenceData() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
const companyId = companyStore.activeCompany?.id || 1;
|
||||||
refsLoading = true;
|
refsLoading = true;
|
||||||
try {
|
try {
|
||||||
const [cc, ss] = await Promise.all([
|
const [cc, ss] = await Promise.all([
|
||||||
countriesApi.list(1, 100),
|
countriesApi.list(companyId, 1, 100),
|
||||||
statesApi.list(1, 100)
|
statesApi.list(companyId, 1, 100)
|
||||||
]);
|
]);
|
||||||
if (cc.data?.items) countries = cc.data.items;
|
if (cc.data?.items) countries = cc.data.items;
|
||||||
if (ss.data?.items) states = ss.data.items;
|
if (ss.data?.items) states = ss.data.items;
|
||||||
|
|||||||
@@ -90,16 +90,26 @@
|
|||||||
refsLoading = true;
|
refsLoading = true;
|
||||||
const cid = companyStore.activeCompany.id;
|
const cid = companyStore.activeCompany.id;
|
||||||
try {
|
try {
|
||||||
const [tr, tt, cc, ss] = await Promise.all([
|
const [tr, tt, cc] = await Promise.all([
|
||||||
transportersApi.list(cid, { page: 1, page_size: 100 }),
|
transportersApi.list(cid, { page: 1, page_size: 100 }),
|
||||||
transportTypesApi.list(cid, 1, 100),
|
transportTypesApi.list(cid, 1, 100),
|
||||||
countriesApi.list(1, 100),
|
countriesApi.list(cid, 1, 100)
|
||||||
statesApi.list(1, 100)
|
|
||||||
]);
|
]);
|
||||||
if (tr.data?.items) transporters = tr.data.items;
|
if (tr.data?.items) transporters = tr.data.items;
|
||||||
if (tt.data?.items) transportTypes = tt.data.items;
|
if (tt.data?.items) transportTypes = tt.data.items;
|
||||||
if (cc.data?.items) countries = cc.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 {
|
} catch {
|
||||||
transporters = [];
|
transporters = [];
|
||||||
transportTypes = [];
|
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 {
|
function emptyVehicleForm(): Vehicle {
|
||||||
return {
|
return {
|
||||||
vehicle_key: '',
|
vehicle_key: '',
|
||||||
|
|||||||
65
frontend/src/lib/i18n/trailer-types.ts
Normal file
65
frontend/src/lib/i18n/trailer-types.ts
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user