fix/transportes-traduccion-erronea

This commit is contained in:
2026-04-29 08:13:29 -06:00
parent 8e3f7d2dd8
commit 5f8bc3492e
7 changed files with 414 additions and 118 deletions

View File

@@ -259,7 +259,29 @@
"transporters": "Carriers",
"drivers": "Drivers",
"trailers": "Trailers",
"vehicles": "Vehicles"
"vehicles": "Vehicles",
"vehicle_transport_types": {
"ar": "Armored Truck",
"au": "Automobiles",
"bt": "Box Truck",
"bu": "Bus",
"bv": "Beverage Truck (Refer or not)",
"by": "Bicycle",
"co": "Construction Vehicle (general)",
"ev": "Emergency Vehicle (general)",
"fe": "Ferry",
"fm": "Farm Tractor",
"gb": "Garbage Truck",
"mc": "Motorcycle",
"oc": "Other",
"pm": "Pick-up Truck w/camper",
"pn": "Panel Truck",
"pu": "Pickup Truck",
"pv": "Passenger",
"rv": "Recreation Vehicle (RV)",
"tr": "Semi Tracker",
"tv": "Van"
}
},
"reports": {
"title": "Reports",

View File

@@ -258,7 +258,29 @@
"transporters": "Transportistas",
"drivers": "Conductores",
"trailers": "Trailers",
"vehicles": "Vehículos"
"vehicles": "Vehículos",
"vehicle_transport_types": {
"ar": "Camión Blindado",
"au": "Automóviles",
"bt": "Camión de Caja",
"bu": "Autobús",
"bv": "Camión de Bebidas",
"by": "Bicicleta",
"co": "Vehículo de Construcción",
"ev": "Vehículo de Emergencia",
"fe": "Ferry",
"fm": "Tractor Agrícola",
"gb": "Camión de Basura",
"mc": "Motocicleta",
"oc": "Otro",
"pm": "Camioneta con cabina",
"pn": "Camión Panel",
"pu": "Camioneta (Pick-up)",
"pv": "Pasajero",
"rv": "Vehículo Recreativo (RV)",
"tr": "Tractocamión",
"tv": "Van"
}
},
"reports": {
"title": "Reportes",

View File

@@ -5,6 +5,7 @@ import type { Vehicle } from '$lib/api/dashboard/a76/vehicles';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
import { tv, tvTransportType } from '$lib/i18n/vehicles';
export function createColumns(
onSuccess: () => void,
@@ -13,49 +14,50 @@ export function createColumns(
return [
{
accessorKey: 'vehicle_key',
header: 'Clave',
header: tv('col_key'),
cell: ({ row }) => {
return row.original.vehicle_key;
}
},
{
accessorKey: 'brand',
header: 'Marca',
header: tv('col_brand'),
cell: ({ row }) => {
return row.original.brand || '-';
}
},
{
accessorKey: 'year',
header: 'Año',
header: tv('col_year'),
cell: ({ row }) => {
return row.original.year || '-';
}
},
{
accessorKey: 'plate_number',
header: 'Placas',
header: tv('col_plate'),
cell: ({ row }) => {
return row.original.plate_number || '-';
}
},
{
accessorKey: 'transporter_key',
header: 'Transportista',
header: tv('col_transporter'),
cell: ({ row }) => {
return row.original.transporter_key || '-';
}
},
{
accessorKey: 'transport_type',
header: 'Tipo Transporte',
header: tv('col_transport_type'),
cell: ({ row }) => {
return row.original.transport_type || '-';
if (!row.original.transport_type) return '-';
return `${row.original.transport_type} - ${tvTransportType(row.original.transport_type, row.original.transport_type)}`;
}
},
{
id: 'actions',
header: 'Acciones',
header: tv('col_actions'),
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,

View File

@@ -11,6 +11,7 @@
import { countriesApi, type Country } from '$lib/api/dashboard/reference_data/countries';
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
import { companyStore } from '$lib/stores/company.svelte';
import { tv, tvTransportType } from '$lib/i18n/vehicles';
import { browser } from '$app/environment';
let {
@@ -25,7 +26,7 @@
// Determinar si es modo edición o creación
const isEdit = $derived(!!item);
const title = $derived(isEdit ? 'Editar Vehículo' : 'Nuevo Vehículo');
const title = $derived(isEdit ? tv('dialog_edit_title') : tv('dialog_new_title'));
// Estado del formulario
let formData = $state<Vehicle>({
@@ -66,35 +67,16 @@
let refsLoading = $state(false);
const ENTITY_OPTS = [
{ value: '', label: '— Sin código —' },
{ value: '', label: tv('no_code') },
{ value: 'C', label: 'C' },
{ value: 'I', label: 'I' },
{ value: 'A', label: 'A' },
{ value: 'B', label: 'B' }
];
const TRANSPORT_TYPE_TRANSLATIONS: Record<string, string> = {
AR: 'Camión Blindado',
AU: 'Automóviles',
BT: 'Camión de Caja',
BU: 'Autobús',
BV: 'Camión de Bebidas',
BY: 'Bicicleta',
CO: 'Vehículo de Construcción',
EV: 'Vehículo de Emergencia',
FE: 'Ferry',
FM: 'Tractor Agrícola',
GB: 'Camión de Basura',
MC: 'Motocicleta',
OC: 'Otro',
PM: 'Camioneta con cabina',
PN: 'Camión Panel',
PU: 'Camioneta (Pick-up)',
PV: 'Pasajero',
RV: 'Vehículo Recreativo (RV)',
TR: 'Tractocamión',
TV: 'Van'
};
function getTransportTypeText(transportCode?: string, description?: string): string {
return tvTransportType(transportCode, description);
}
const countryM3 = $derived(
countries.find((c) => c.ame_key === formData.country)?.m3_key ?? null
@@ -110,7 +92,7 @@
try {
const [tr, tt, cc, ss] = await Promise.all([
transportersApi.list(cid, { page: 1, page_size: 100 }),
transportTypesApi.list(1, 100),
transportTypesApi.list(cid, 1, 100),
countriesApi.list(1, 100),
statesApi.list(1, 100)
]);
@@ -181,12 +163,12 @@
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
throw new Error('No hay una compañía seleccionada');
throw new Error(tv('no_company_selected'));
}
// Validación básica
if (!formData.vehicle_key.trim()) {
throw new Error('La clave del vehículo es requerida');
throw new Error(tv('vehicle_key_required'));
}
let response;
@@ -210,7 +192,7 @@
onSuccess();
}
} catch (e) {
error = e instanceof Error ? e.message : 'Error al guardar el vehículo';
error = e instanceof Error ? e.message : tv('save_error');
} finally {
loading = false;
}
@@ -238,8 +220,8 @@
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>
{isEdit
? 'Modifica los datos del vehículo'
: 'Completa los datos para crear un nuevo vehículo de transporte'}
? tv('dialog_edit_description')
: tv('dialog_new_description')}
</Dialog.Description>
</Dialog.Header>
@@ -258,21 +240,21 @@
<Tabs.Root value="general" class="w-full">
<Tabs.List class="grid w-full grid-cols-2">
<Tabs.Trigger value="general">Información General</Tabs.Trigger>
<Tabs.Trigger value="details">Seguro y Detalles</Tabs.Trigger>
<Tabs.Trigger value="general">{tv('tab_general')}</Tabs.Trigger>
<Tabs.Trigger value="details">{tv('tab_details')}</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="general" class="mt-6 space-y-8">
<!-- Información del Vehículo -->
<section class="space-y-4">
<h3 class="border-b pb-2 text-sm font-bold uppercase tracking-wider text-muted-foreground">
Identificación del Vehículo
{tv('section_vehicle_identification')}
</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid gap-2 md:col-span-2">
<Label for="vehicle_key"
>Clave del Vehículo <span class="text-destructive">*</span></Label
>{tv('label_vehicle_key')} <span class="text-destructive">*</span></Label
>
<Input
id="vehicle_key"
@@ -280,27 +262,27 @@
disabled={isEdit}
required
maxlength={14}
placeholder="Ej: VH001"
placeholder={tv('placeholder_vehicle_key')}
/>
</div>
<div class="grid gap-2">
<Label for="brand">Marca</Label>
<Input id="brand" bind:value={formData.brand} maxlength={20} placeholder="Ej: Kenworth" />
<Label for="brand">{tv('label_brand')}</Label>
<Input id="brand" bind:value={formData.brand} maxlength={20} placeholder={tv('placeholder_brand')} />
</div>
<div class="grid gap-2">
<Label for="year">Año</Label>
<Input id="year" bind:value={formData.year} maxlength={4} placeholder="YYYY" />
<Label for="year">{tv('label_year')}</Label>
<Input id="year" bind:value={formData.year} maxlength={4} placeholder={tv('placeholder_year')} />
</div>
<div class="grid gap-2">
<Label for="plate_number">Placas</Label>
<Input id="plate_number" bind:value={formData.plate_number} maxlength={17} placeholder="Placas actuales" />
<Label for="plate_number">{tv('label_plate_number')}</Label>
<Input id="plate_number" bind:value={formData.plate_number} maxlength={17} placeholder={tv('placeholder_plate_number')} />
</div>
<div class="grid gap-2">
<Label for="series">Serie / VIN</Label>
<Input id="series" bind:value={formData.series} maxlength={30} placeholder="Número de serie" />
<Label for="series">{tv('label_series')}</Label>
<Input id="series" bind:value={formData.series} maxlength={30} placeholder={tv('placeholder_series')} />
</div>
</div>
</section>
@@ -308,22 +290,22 @@
<!-- Datos de Transporte -->
<section class="space-y-4">
<h3 class="border-b pb-2 text-sm font-bold uppercase tracking-wider text-muted-foreground">
Datos de Transporte
{tv('section_transport_data')}
</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid gap-2 md:col-span-2">
<Label for="transporter_key">Transportista</Label>
<Label for="transporter_key">{tv('label_transporter')}</Label>
<Select.Root type="single" bind:value={formData.transporter_key} disabled={refsLoading}>
<Select.Trigger class="w-full">
{refsLoading
? '...'
: formData.transporter_key
? `${formData.transporter_key} ${transporters.find((t) => t.transporter_key === formData.transporter_key)?.name ?? ''}`
: '— Seleccionar transportista —'}
: tv('select_transporter')}
</Select.Trigger>
<Select.Content class="max-h-60">
<Select.Item value="" label="Vacío">— Vacío —</Select.Item>
<Select.Item value="" label={tv('empty')}>{tv('empty')}</Select.Item>
{#each transporters as t}
<Select.Item value={t.transporter_key} label={t.transporter_key}>
{t.transporter_key} — {t.name || t.short_name || ''}
@@ -334,30 +316,30 @@
</div>
<div class="grid gap-2">
<Label for="transport_identifier">ID Transporte</Label>
<Label for="transport_identifier">{tv('label_transport_identifier')}</Label>
<Input
id="transport_identifier"
bind:value={formData.transport_identifier}
maxlength={30}
placeholder="Identificador único"
placeholder={tv('placeholder_transport_identifier')}
/>
</div>
<div class="grid gap-2">
<Label for="transport_type">Tipo de Transporte</Label>
<Label for="transport_type">{tv('label_transport_type')}</Label>
<Select.Root type="single" bind:value={formData.transport_type} disabled={refsLoading}>
<Select.Trigger class="w-full" id="transport_type">
{refsLoading
? '...'
: formData.transport_type
? `${formData.transport_type} ${TRANSPORT_TYPE_TRANSLATIONS[formData.transport_type] || transportTypes.find((x) => x.transport_code === formData.transport_type)?.description || ''}`
: '— Opcional'}
? `${formData.transport_type} ${getTransportTypeText(formData.transport_type, transportTypes.find((x) => (x.transport_code || '').trim().toUpperCase() === (formData.transport_type || '').trim().toUpperCase())?.description)}`
: tv('optional')}
</Select.Trigger>
<Select.Content>
<Select.Item value="" label="Vacío">— Vacío —</Select.Item>
<Select.Item value="" label={tv('empty')}>{tv('empty')}</Select.Item>
{#each transportTypes as x}
<Select.Item value={x.transport_code} label={x.transport_code}>
{x.transport_code} — {TRANSPORT_TYPE_TRANSLATIONS[x.transport_code] || x.description}
{x.transport_code} — {getTransportTypeText(x.transport_code, x.description)}
</Select.Item>
{/each}
</Select.Content>
@@ -365,11 +347,11 @@
</div>
<div class="grid gap-2">
<Label for="entity_code">Código de Entidad</Label>
<Label for="entity_code">{tv('label_entity_code')}</Label>
<Select.Root type="single" bind:value={formData.entity_code}>
<Select.Trigger class="w-full" id="entity_code">
{ENTITY_OPTS.find((o) => o.value === (formData.entity_code || ''))?.label ??
'— Sin código —'}
tv('no_code')}
</Select.Trigger>
<Select.Content>
{#each ENTITY_OPTS as o}
@@ -377,12 +359,12 @@
{/each}
</Select.Content>
</Select.Root>
<p class="text-[10px] text-muted-foreground uppercase">Obligatorio al crear (reglas CSV)</p>
<p class="text-[10px] text-muted-foreground uppercase">{tv('entity_required_hint')}</p>
</div>
<div class="grid gap-2">
<Label for="sct_permission">Permiso SCT</Label>
<Input id="sct_permission" bind:value={formData.sct_permission} maxlength={40} placeholder="Número de permiso" />
<Label for="sct_permission">{tv('label_sct_permission')}</Label>
<Input id="sct_permission" bind:value={formData.sct_permission} maxlength={40} placeholder={tv('placeholder_sct_permission')} />
</div>
</div>
</section>
@@ -392,31 +374,31 @@
<!-- Seguro y Otros -->
<section class="space-y-4">
<h3 class="border-b pb-2 text-sm font-bold uppercase tracking-wider text-muted-foreground">
Seguro y Otros
{tv('section_insurance')}
</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid gap-2 md:col-span-2">
<Label for="insurance_company_name">Aseguradora</Label>
<Label for="insurance_company_name">{tv('label_insurance_company')}</Label>
<Input
id="insurance_company_name"
bind:value={formData.insurance_company_name}
maxlength={30}
placeholder="Nombre de la compañía"
placeholder={tv('placeholder_insurance_company')}
/>
</div>
<div class="grid gap-2">
<Label for="insurance_number">Póliza</Label>
<Label for="insurance_number">{tv('label_insurance_number')}</Label>
<Input
id="insurance_number"
bind:value={formData.insurance_number}
maxlength={20}
placeholder="Número de póliza"
placeholder={tv('placeholder_insurance_number')}
/>
</div>
<div class="grid gap-2">
<Label for="insurance_amount">Monto</Label>
<Label for="insurance_amount">{tv('label_insurance_amount')}</Label>
<Input
id="insurance_amount"
type="number"
@@ -426,7 +408,7 @@
</div>
<div class="grid gap-2">
<Label for="dot_number">Número DOT</Label>
<Label for="dot_number">{tv('label_dot_number')}</Label>
<Input id="dot_number" bind:value={formData.dot_number} maxlength={8} placeholder="DOT ID" />
</div>
</div>
@@ -435,22 +417,22 @@
<!-- Ubicación y Detalles -->
<section class="space-y-4">
<h3 class="border-b pb-2 text-sm font-bold uppercase tracking-wider text-muted-foreground">
Ubicación y Detalles
{tv('section_location')}
</h3>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid gap-2">
<Label for="country">País (AME)</Label>
<Label for="country">{tv('label_country')}</Label>
<Select.Root type="single" bind:value={formData.country} disabled={refsLoading}>
<Select.Trigger class="w-full" id="country">
{refsLoading
? '...'
: formData.country
? `${formData.country} ${countries.find((c) => c.ame_key === formData.country)?.description_es ?? ''}`
: '— Opcional'}
: tv('optional')}
</Select.Trigger>
<Select.Content class="max-h-60">
<Select.Item value="" label="Vacío">— Vacío —</Select.Item>
<Select.Item value="" label={tv('empty')}>{tv('empty')}</Select.Item>
{#each countries as c}
<Select.Item value={c.ame_key} label={c.ame_key}>
{c.ame_key}{c.description_es}
@@ -460,16 +442,16 @@
</Select.Root>
</div>
<div class="grid gap-2">
<Label for="state">Estado</Label>
<Label for="state">{tv('label_state')}</Label>
<Select.Root type="single" bind:value={formData.state} disabled={refsLoading}>
<Select.Trigger class="w-full" id="state">
{refsLoading
? '...'
: formData.state ||
(formData.country ? '— Selecciona —' : '— Primero el país —')}
(formData.country ? tv('select_state') : tv('select_country_first'))}
</Select.Trigger>
<Select.Content class="max-h-60">
<Select.Item value="" label="Vacío">— Vacío —</Select.Item>
<Select.Item value="" label={tv('empty')}>{tv('empty')}</Select.Item>
{#each statesFiltered as s}
<Select.Item value={s.description} label={s.description}>
{s.description}
@@ -480,21 +462,21 @@
</div>
<div class="grid gap-2">
<Label for="city">Ciudad</Label>
<Input id="city" bind:value={formData.city} maxlength={30} placeholder="Ciudad/Localidad" />
<Label for="city">{tv('label_city')}</Label>
<Input id="city" bind:value={formData.city} maxlength={30} placeholder={tv('placeholder_city')} />
</div>
<div class="grid gap-2">
<Label for="color">Color</Label>
<Input id="color" bind:value={formData.color} maxlength={20} placeholder="Color del vehículo" />
<Label for="color">{tv('label_color')}</Label>
<Input id="color" bind:value={formData.color} maxlength={20} placeholder={tv('placeholder_color')} />
</div>
<div class="grid gap-2">
<Label for="container_key">Tipo Contenedor</Label>
<Label for="container_key">{tv('label_container_type')}</Label>
<Input
id="container_key"
bind:value={formData.container_key}
maxlength={3}
placeholder="Ej: 40G"
placeholder={tv('placeholder_container_type')}
/>
</div>
</div>
@@ -502,12 +484,12 @@
<section class="space-y-4 pt-4">
<div class="grid gap-2">
<Label for="description">Descripción Adicional</Label>
<Label for="description">{tv('label_additional_description')}</Label>
<Input
id="description"
bind:value={formData.description}
maxlength={100}
placeholder="Notas adicionales sobre el vehículo..."
placeholder={tv('placeholder_additional_description')}
/>
</div>
</section>
@@ -516,10 +498,10 @@
<Dialog.Footer>
<Button type="button" variant="outline" onclick={handleCancel} disabled={loading}>
Cancelar
{tv('cancel')}
</Button>
<Button type="submit" disabled={loading}>
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
{loading ? tv('saving') : isEdit ? tv('update') : tv('create')}
</Button>
</Dialog.Footer>
</form>

View File

@@ -5,6 +5,7 @@
import { companyStore } from '$lib/stores/company.svelte';
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from './create-edit-dialog.svelte';
import { tv } from '$lib/i18n/vehicles';
let {
item,
@@ -26,14 +27,14 @@
async function handleDelete() {
if (
!confirm(
`¿Estás seguro de eliminar el vehículo "${item.vehicle_key}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`
tv('delete_confirm', { key: item.vehicle_key })
)
) {
return;
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
alert(tv('no_company_error'));
return;
}
@@ -45,25 +46,25 @@
if (response.error) {
if (response.status === 401) {
error = 'Sesión expirada. Recargando página...';
error = tv('session_expired');
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
error = response.error;
alert(`❌ Error al eliminar:\n\n${response.error}`);
alert(`${tv('delete_error_title')}\n\n${response.error}`);
}
return;
}
// Éxito
alert(`✅ Vehículo "${item.vehicle_key}" eliminado correctamente`);
alert(tv('delete_success_item', { key: item.vehicle_key }));
if (onSuccess) {
onSuccess();
}
} catch (e) {
error = e instanceof Error ? e.message : 'Error al eliminar';
alert(`❌ Error: ${error}`);
error = e instanceof Error ? e.message : tv('delete_generic_error');
alert(tv('delete_error_alert', { error }));
console.error('Error deleting:', e);
} finally {
loading = false;
@@ -88,18 +89,18 @@
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
<span class="sr-only">{tv('menu_open')}</span>
<EllipsisVertical size={16} />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end" class="w-[160px]">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Label>{tv('menu_actions')}</DropdownMenu.Label>
<DropdownMenu.Separator />
{#if canEdit}
<DropdownMenu.Item onclick={handleEdit}>
<Pencil size={16} class="mr-2" />
Editar
{tv('edit')}
</DropdownMenu.Item>
{/if}
{#if canDelete}
@@ -110,7 +111,7 @@
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
{tv('delete')}
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>

View File

@@ -0,0 +1,266 @@
import { getLocale } from '$lib/paraglide/runtime';
type VehiclesLocale = 'es' | 'en';
const MESSAGES = {
es: {
shortcuts_scope: 'Vehículos (transporte)',
shortcuts_save: 'Guardar',
page_title: 'Vehículos (Transporte)',
page_subtitle: 'Gestión del catálogo de camiones y vehículos de transporte',
new_vehicle: 'Nuevo Vehículo',
vehicle_list: 'Listado de Vehículos',
search_key: 'Clave',
search_plate: 'Placas',
loading_vehicles: 'Cargando vehículos...',
showing_records: 'Mostrando {shown} de {total} registros',
edit: 'Editar',
delete: 'Eliminar',
delete_confirm: '¿Estás seguro de eliminar el vehículo "{key}"?\n\nNota: No se puede eliminar si tiene registros relacionados.',
delete_error_title: '❌ Error al eliminar:',
delete_success: '✅ Vehículo eliminado correctamente',
col_key: 'Clave',
col_brand: 'Marca',
col_year: 'Año',
col_plate: 'Placas',
col_transporter: 'Transportista',
col_transport_type: 'Tipo Transporte',
col_actions: 'Acciones',
menu_open: 'Abrir menú',
menu_actions: 'Acciones',
session_expired: 'Sesión expirada. Recargando página...',
no_company_error: '❌ Error: No hay una compañía seleccionada',
delete_generic_error: 'Error al eliminar',
delete_error_alert: '❌ Error: {error}',
delete_success_item: '✅ Vehículo "{key}" eliminado correctamente',
dialog_edit_title: 'Editar Vehículo',
dialog_new_title: 'Nuevo Vehículo',
dialog_edit_description: 'Modifica los datos del vehículo',
dialog_new_description: 'Completa los datos para crear un nuevo vehículo de transporte',
tab_general: 'Información General',
tab_details: 'Seguro y Detalles',
section_vehicle_identification: 'Identificación del Vehículo',
label_vehicle_key: 'Clave del Vehículo',
placeholder_vehicle_key: 'Ej: VH001',
label_brand: 'Marca',
placeholder_brand: 'Ej: Kenworth',
label_year: 'Año',
placeholder_year: 'YYYY',
label_plate_number: 'Placas',
placeholder_plate_number: 'Placas actuales',
label_series: 'Serie / VIN',
placeholder_series: 'Número de serie',
section_transport_data: 'Datos de Transporte',
label_transporter: 'Transportista',
select_transporter: '— Seleccionar transportista —',
empty: '— Vacío —',
label_transport_identifier: 'ID Transporte',
placeholder_transport_identifier: 'Identificador único',
label_transport_type: 'Tipo de Transporte',
optional: '— Opcional —',
label_entity_code: 'Código de Entidad',
no_code: '— Sin código —',
entity_required_hint: 'Obligatorio al crear (reglas CSV)',
label_sct_permission: 'Permiso SCT',
placeholder_sct_permission: 'Número de permiso',
section_insurance: 'Seguro y Otros',
label_insurance_company: 'Aseguradora',
placeholder_insurance_company: 'Nombre de la compañía',
label_insurance_number: 'Póliza',
placeholder_insurance_number: 'Número de póliza',
label_insurance_amount: 'Monto',
label_dot_number: 'Número DOT',
label_country: 'País (AME)',
label_state: 'Estado',
select_state: '— Selecciona —',
select_country_first: '— Primero el país —',
label_city: 'Ciudad',
placeholder_city: 'Ciudad/Localidad',
label_color: 'Color',
placeholder_color: 'Color del vehículo',
label_container_type: 'Tipo Contenedor',
placeholder_container_type: 'Ej: 40G',
section_location: 'Ubicación y Detalles',
label_additional_description: 'Descripción Adicional',
placeholder_additional_description: 'Notas adicionales sobre el vehículo...',
cancel: 'Cancelar',
saving: 'Guardando...',
update: 'Actualizar',
create: 'Crear',
no_company_selected: 'No hay una compañía seleccionada',
vehicle_key_required: 'La clave del vehículo es requerida',
save_error: 'Error al guardar el vehículo',
transport_types: {
AR: 'Camión Blindado',
AU: 'Automóviles',
BT: 'Camión de Caja',
BU: 'Autobús',
BV: 'Camión de Bebidas',
BY: 'Bicicleta',
CO: 'Vehículo de Construcción',
EV: 'Vehículo de Emergencia',
FE: 'Ferry',
FM: 'Tractor Agrícola',
GB: 'Camión de Basura',
MC: 'Motocicleta',
OC: 'Otro',
PM: 'Camioneta con cabina',
PN: 'Camión Panel',
PU: 'Camioneta (Pick-up)',
PV: 'Pasajero',
RV: 'Vehículo Recreativo (RV)',
TR: 'Tractocamión',
TV: 'Van'
}
},
en: {
shortcuts_scope: 'Vehicles (transport)',
shortcuts_save: 'Save',
page_title: 'Vehicles (Transport)',
page_subtitle: 'Transport trucks and vehicles catalog management',
new_vehicle: 'New Vehicle',
vehicle_list: 'Vehicle List',
search_key: 'Key',
search_plate: 'Plates',
loading_vehicles: 'Loading vehicles...',
showing_records: 'Showing {shown} of {total} records',
edit: 'Edit',
delete: 'Delete',
delete_confirm: 'Are you sure you want to delete vehicle "{key}"?\n\nNote: It cannot be deleted if it has related records.',
delete_error_title: '❌ Error deleting:',
delete_success: '✅ Vehicle deleted successfully',
col_key: 'Key',
col_brand: 'Brand',
col_year: 'Year',
col_plate: 'Plates',
col_transporter: 'Transporter',
col_transport_type: 'Transport Type',
col_actions: 'Actions',
menu_open: 'Open menu',
menu_actions: 'Actions',
session_expired: 'Session expired. Reloading page...',
no_company_error: '❌ Error: No company selected',
delete_generic_error: 'Error deleting',
delete_error_alert: '❌ Error: {error}',
delete_success_item: '✅ Vehicle "{key}" deleted successfully',
dialog_edit_title: 'Edit Vehicle',
dialog_new_title: 'New Vehicle',
dialog_edit_description: 'Modify vehicle data',
dialog_new_description: 'Complete the data to create a new transport vehicle',
tab_general: 'General Information',
tab_details: 'Insurance and Details',
section_vehicle_identification: 'Vehicle Identification',
label_vehicle_key: 'Vehicle Key',
placeholder_vehicle_key: 'Eg: VH001',
label_brand: 'Brand',
placeholder_brand: 'Eg: Kenworth',
label_year: 'Year',
placeholder_year: 'YYYY',
label_plate_number: 'Plates',
placeholder_plate_number: 'Current plates',
label_series: 'Series / VIN',
placeholder_series: 'Serial number',
section_transport_data: 'Transport Data',
label_transporter: 'Transporter',
select_transporter: '— Select transporter —',
empty: '— Empty —',
label_transport_identifier: 'Transport ID',
placeholder_transport_identifier: 'Unique identifier',
label_transport_type: 'Transport Type',
optional: '— Optional —',
label_entity_code: 'Entity Code',
no_code: '— No code —',
entity_required_hint: 'Required on create (CSV rules)',
label_sct_permission: 'SCT Permit',
placeholder_sct_permission: 'Permit number',
section_insurance: 'Insurance and Others',
label_insurance_company: 'Insurance Company',
placeholder_insurance_company: 'Company name',
label_insurance_number: 'Policy',
placeholder_insurance_number: 'Policy number',
label_insurance_amount: 'Amount',
label_dot_number: 'DOT Number',
label_country: 'Country (AME)',
label_state: 'State',
select_state: '— Select —',
select_country_first: '— Select country first —',
label_city: 'City',
placeholder_city: 'City/Location',
label_color: 'Color',
placeholder_color: 'Vehicle color',
label_container_type: 'Container Type',
placeholder_container_type: 'Eg: 40G',
section_location: 'Location and Details',
label_additional_description: 'Additional Description',
placeholder_additional_description: 'Additional notes about the vehicle...',
cancel: 'Cancel',
saving: 'Saving...',
update: 'Update',
create: 'Create',
no_company_selected: 'No company selected',
vehicle_key_required: 'Vehicle key is required',
save_error: 'Error saving vehicle',
transport_types: {
AR: 'Armored Truck',
AU: 'Automobiles',
BT: 'Box Truck',
BU: 'Bus',
BV: 'Beverage Truck (Refer or not)',
BY: 'Bicycle',
CO: 'Construction Vehicle (general)',
EV: 'Emergency Vehicle (general)',
FE: 'Ferry',
FM: 'Farm Tractor',
GB: 'Garbage Truck',
MC: 'Motorcycle',
OC: 'Other',
PM: 'Pick-up Truck w/camper',
PN: 'Panel Truck',
PU: 'Pickup Truck',
PV: 'Passenger',
RV: 'Recreation Vehicle (RV)',
TR: 'Semi Tracker',
TV: 'Van'
}
}
} as const;
export type VehiclesMessageKey = Exclude<keyof (typeof MESSAGES)['es'], 'transport_types'>;
function activeLocale(): VehiclesLocale {
if (typeof window !== 'undefined') {
const htmlLang = document.documentElement?.lang?.toLowerCase() || '';
if (htmlLang.startsWith('es')) return 'es';
if (htmlLang.startsWith('en')) return 'en';
const storedLocaleCandidates = [
window.localStorage.getItem('locale'),
window.localStorage.getItem('lang'),
window.localStorage.getItem('language')
]
.filter(Boolean)
.map((value) => String(value).toLowerCase());
if (storedLocaleCandidates.some((value) => value.startsWith('es'))) return 'es';
if (storedLocaleCandidates.some((value) => value.startsWith('en'))) return 'en';
}
return getLocale().startsWith('es') ? 'es' : 'en';
}
export function tv(key: VehiclesMessageKey, vars?: Record<string, string | number>): string {
const locale = activeLocale();
let text = MESSAGES[locale][key] as string;
if (!vars) return text;
for (const [name, value] of Object.entries(vars)) {
text = text.replaceAll(`{${name}}`, String(value));
}
return text;
}
export function tvTransportType(code?: string, fallback?: string): string {
const normalizedCode = (code || '').trim().toUpperCase();
if (!normalizedCode) return fallback || '';
const locale = activeLocale();
return MESSAGES[locale].transport_types[normalizedCode as keyof (typeof MESSAGES)['es']['transport_types']] || fallback || '';
}

View File

@@ -4,7 +4,7 @@
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus, RefreshCw, Pencil, Trash2 } from 'lucide-svelte';
import { Plus, Pencil, Trash2 } from 'lucide-svelte';
// Importar componentes de la librería
import InfiniteDataTable from '$lib/components/dashboard/common/infinite-data-table.svelte';
@@ -16,6 +16,7 @@
import { browser } from '$app/environment';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosCatalogoSimple } from '$lib/config/shortcuts/dashboard/general_catalogs/common/factory';
import { tv } from '$lib/i18n/vehicles';
// --- ESTADO ---
let data = $state<Vehicle[]>([]);
@@ -115,7 +116,7 @@
if (!selectedVehicle) return;
if (
!confirm(
`¿Estás seguro de eliminar el vehículo "${selectedVehicle.vehicle_key}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`
tv('delete_confirm', { key: selectedVehicle.vehicle_key })
)
) {
return;
@@ -129,9 +130,9 @@
companyStore.activeCompany.id
);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
alert(`${tv('delete_error_title')}\n\n${response.error}`);
} else {
alert(`✅ Vehículo eliminado correctamente`);
alert(tv('delete_success'));
selectedIds = [];
loadData();
}
@@ -166,7 +167,7 @@
}
});
useShortcuts('Vehículos (transporte)', [
useShortcuts(tv('shortcuts_scope'), [
...obtenerAtajosCatalogoSimple({
manejarNuevo: () => {
createDialogOpen = true;
@@ -175,7 +176,7 @@
}),
{
key: 'Ctrl+S',
description: 'Guardar',
description: tv('shortcuts_save'),
action: () => {
if (!createDialogOpen) return;
document.dispatchEvent(new CustomEvent('save-form'));
@@ -189,20 +190,20 @@
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Vehículos (Transporte)</h1>
<h1 class="text-2xl font-bold tracking-tight">{tv('page_title')}</h1>
<p class="text-muted-foreground">
Gestión del catálogo de camiones y vehículos de transporte
{tv('page_subtitle')}
</p>
</div>
<div class="flex items-center gap-3"><Button class="h-9" onclick={() => { editingItem = null; createDialogOpen = true; }}><Plus class="mr-2 h-4 w-4" />Nuevo Vehículo</Button></div>
<div class="flex items-center gap-3"><Button class="h-9" onclick={() => { editingItem = null; createDialogOpen = true; }}><Plus class="mr-2 h-4 w-4" />{tv('new_vehicle')}</Button></div>
</div>
<Card.Root class="border bg-background flex flex-col">
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>Listado de Vehículos</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder="Clave" class="h-9 w-40 bg-card lg:w-52" bind:value={searchKey} oninput={handleSearch} /><Input placeholder="Placas" class="h-9 w-40 bg-card lg:w-52" bind:value={searchPlate} oninput={handleSearch} /></div></div></Card.Header>
<Card.Content class="p-0">{#if loading && data.length === 0}<div class="flex h-64 items-center justify-center text-muted-foreground">Cargando vehículos...</div>{:else}<div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable {data} {columns} {loading} {hasMore} {loadMore} {selectedIds} onSelectedIdsChange={(ids) => (selectedIds = ids)} onRowClick={handleRowClick} onRowDoubleClick={handleRowDoubleClick} getRowId={(row) => row.vehicle_key} /></div>{/if}</Card.Content>
<Card.Header><div class="flex flex-wrap items-center justify-between gap-3"><Card.Title>{tv('vehicle_list')}</Card.Title><div class="flex flex-wrap items-center gap-2"><Input placeholder={tv('search_key')} class="h-9 w-40 bg-card lg:w-52" bind:value={searchKey} oninput={handleSearch} /><Input placeholder={tv('search_plate')} class="h-9 w-40 bg-card lg:w-52" bind:value={searchPlate} oninput={handleSearch} /></div></div></Card.Header>
<Card.Content class="p-0">{#if loading && data.length === 0}<div class="flex h-64 items-center justify-center text-muted-foreground">{tv('loading_vehicles')}</div>{:else}<div class="rounded-md border bg-background overflow-hidden"><InfiniteDataTable {data} {columns} {loading} {hasMore} {loadMore} {selectedIds} onSelectedIdsChange={(ids) => (selectedIds = ids)} onRowClick={handleRowClick} onRowDoubleClick={handleRowDoubleClick} getRowId={(row) => row.vehicle_key} /></div>{/if}</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {data.length} de {totalItems} registros</div>
<div class="flex-none text-sm text-muted-foreground">{tv('showing_records', { shown: data.length, total: totalItems })}</div>
<div class="h-20"></div>
@@ -220,7 +221,7 @@
disabled={selectedIds.length !== 1}
>
<Pencil size={16} class="mr-2" />
Editar
{tv('edit')}
</Button>
<Button
variant="outline"
@@ -230,7 +231,7 @@
class="text-destructive hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 size={16} class="mr-2" />
Eliminar
{tv('delete')}
</Button>
</div>
</div>