diff --git a/backend/api/v1/modules/public/reference_data/states/routes.py b/backend/api/v1/modules/public/reference_data/states/routes.py index 79595d33..165e4d0e 100644 --- a/backend/api/v1/modules/public/reference_data/states/routes.py +++ b/backend/api/v1/modules/public/reference_data/states/routes.py @@ -22,12 +22,16 @@ async def list_states( 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"), 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), current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(State) + if country_id: + query = query.filter(State.m3_key == country_id) + if search: search_filter = f"%{search}%" query = query.filter( diff --git a/frontend/src/lib/api/dashboard/reference_data/states.ts b/frontend/src/lib/api/dashboard/reference_data/states.ts index e501ded0..8773460e 100644 --- a/frontend/src/lib/api/dashboard/reference_data/states.ts +++ b/frontend/src/lib/api/dashboard/reference_data/states.ts @@ -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(url); }, diff --git a/frontend/src/lib/api/dashboard/reference_data/trailer_types.ts b/frontend/src/lib/api/dashboard/reference_data/trailer_types.ts index 116eb183..c0ec9929 100644 --- a/frontend/src/lib/api/dashboard/reference_data/trailer_types.ts +++ b/frontend/src/lib/api/dashboard/reference_data/trailer_types.ts @@ -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( - `/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}` ) }; diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/state-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/state-dialog.svelte index 6d9f8b47..59019343 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/state-dialog.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/state-dialog.svelte @@ -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; diff --git a/frontend/src/lib/components/dashboard/shared/modals/state-selector-dialog.svelte b/frontend/src/lib/components/dashboard/shared/modals/state-selector-dialog.svelte index 9fe0b478..2403b9c1 100644 --- a/frontend/src/lib/components/dashboard/shared/modals/state-selector-dialog.svelte +++ b/frontend/src/lib/components/dashboard/shared/modals/state-selector-dialog.svelte @@ -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}`); diff --git a/frontend/src/lib/components/dashboard/transportation/trailers/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/transportation/trailers/create-edit-dialog.svelte index 939cd3d0..1a368eac 100644 --- a/frontend/src/lib/components/dashboard/transportation/trailers/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/transportation/trailers/create-edit-dialog.svelte @@ -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) —'} @@ -231,7 +269,9 @@ {t.trailer_type_key} {#if t.description} - — {t.description} + + — {getTrailerTypeDescription(t.trailer_type_key, t.description)} {/if} {/each} diff --git a/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte index 2446ea29..f5653cc7 100644 --- a/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/transportation/transporters/create-edit-dialog.svelte @@ -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; diff --git a/frontend/src/lib/components/dashboard/transportation/vehicles/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/transportation/vehicles/create-edit-dialog.svelte index c0c45e1e..9ddbdd2e 100644 --- a/frontend/src/lib/components/dashboard/transportation/vehicles/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/transportation/vehicles/create-edit-dialog.svelte @@ -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: '', diff --git a/frontend/src/lib/i18n/trailer-types.ts b/frontend/src/lib/i18n/trailer-types.ts new file mode 100644 index 00000000..28889cdc --- /dev/null +++ b/frontend/src/lib/i18n/trailer-types.ts @@ -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 = { + '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; +}