[SPRINT-0] fix: Resolve all TypeScript compilation errors in frontend

- Add @types/node for vite.config.ts __dirname support
- Extend OrdenTransporte interface with alias properties
- Add missing enum states to OTStatusBadge
- Fix EventosList type references and property access
- Fix GeocercasList property names (activo -> activa)
- Fix useTrackingWebSocket NodeJS.Timeout type
- Remove unused imports across components

Build now passes: 191 modules transformed successfully

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-02-03 02:31:37 -06:00
parent b3982844f8
commit 0bc16b52bf
16 changed files with 204 additions and 111 deletions

18
package-lock.json generated
View File

@ -26,6 +26,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/node": "^25.2.0",
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/eslint-plugin": "^6.12.0",
@ -1505,6 +1506,16 @@
"@types/geojson": "*" "@types/geojson": "*"
} }
}, },
"node_modules/@types/node": {
"version": "25.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz",
"integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.15", "version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@ -6695,6 +6706,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",

View File

@ -15,37 +15,38 @@
"test:cov": "vitest run --coverage" "test:cov": "vitest run --coverage"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.3.2",
"@tanstack/react-query": "^5.8.4",
"axios": "^1.7.7",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"leaflet": "^1.9.4",
"lucide-react": "^0.460.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
"zustand": "^5.0.1",
"axios": "^1.7.7",
"@tanstack/react-query": "^5.8.4",
"lucide-react": "^0.460.0",
"date-fns": "^2.30.0",
"clsx": "^2.0.0",
"react-hook-form": "^7.48.2", "react-hook-form": "^7.48.2",
"@hookform/resolvers": "^3.3.2", "react-leaflet": "^4.2.1",
"react-router-dom": "^6.28.0",
"zod": "^3.22.4", "zod": "^3.22.4",
"leaflet": "^1.9.4", "zustand": "^5.0.1"
"react-leaflet": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.9.8",
"@types/node": "^25.2.0",
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@types/leaflet": "^1.9.8", "@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"eslint": "^8.54.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.15",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"vite": "^5.0.8", "vite": "^5.0.8",
"vitest": "^1.0.4", "vitest": "^1.0.4"
"eslint": "^8.54.0",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"

View File

@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ordenesTransporteApi } from '../api/ordenes-transporte.api'; import { ordenesTransporteApi } from '../api/ordenes-transporte.api';
import { OTStatusBadge } from './OTStatusBadge'; import { OTStatusBadge } from './OTStatusBadge';
import type { OrdenTransporte, EstadoOrdenTransporte } from '../types'; import type { OrdenTransporte } from '../types';
interface OTDetailProps { interface OTDetailProps {
orden: OrdenTransporte; orden: OrdenTransporte;
@ -20,7 +20,7 @@ export function OTDetail({ orden, onClose, onEdit }: OTDetailProps) {
}); });
const cancelarMutation = useMutation({ const cancelarMutation = useMutation({
mutationFn: () => ordenesTransporteApi.cancelar(orden.id), mutationFn: (motivo: string) => ordenesTransporteApi.cancelar(orden.id, motivo),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ordenes-transporte'] }); queryClient.invalidateQueries({ queryKey: ['ordenes-transporte'] });
}, },
@ -59,8 +59,9 @@ export function OTDetail({ orden, onClose, onEdit }: OTDetailProps) {
}; };
const handleCancelar = async () => { const handleCancelar = async () => {
if (window.confirm('¿Cancelar esta orden de transporte?')) { const motivo = window.prompt('Motivo de cancelación:');
await cancelarMutation.mutateAsync(); if (motivo) {
await cancelarMutation.mutateAsync(motivo);
} }
}; };

View File

@ -3,6 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ordenesTransporteApi } from '../api/ordenes-transporte.api'; import { ordenesTransporteApi } from '../api/ordenes-transporte.api';
import { TipoCarga, ModalidadServicio, TipoEquipo } from '../types';
import type { OrdenTransporte, CreateOTDto, UpdateOTDto } from '../types'; import type { OrdenTransporte, CreateOTDto, UpdateOTDto } from '../types';
const otSchema = z.object({ const otSchema = z.object({
@ -72,20 +73,20 @@ export function OTForm({ initialData, onSuccess, onCancel }: OTFormProps) {
? { ? {
clienteId: initialData.clienteId, clienteId: initialData.clienteId,
referenciaCliente: initialData.referenciaCliente || '', referenciaCliente: initialData.referenciaCliente || '',
modalidad: initialData.modalidad, modalidad: (initialData.modalidad || 'FTL') as 'FTL' | 'LTL' | 'DEDICADO' | 'EXPRESS' | 'CONSOLIDADO',
tipoCarga: initialData.tipoCarga, tipoCarga: initialData.tipoCarga as 'GENERAL' | 'REFRIGERADA' | 'PELIGROSA' | 'SOBREDIMENSIONADA' | 'FRAGIL' | 'GRANEL' | 'LIQUIDOS' | 'CONTENEDOR',
tipoEquipo: initialData.tipoEquipo, tipoEquipo: initialData.tipoEquipo as 'CAJA_SECA' | 'REFRIGERADO' | 'PLATAFORMA' | 'TANQUE' | 'LOWBOY' | 'PORTACONTENEDOR' | 'TOLVA' | 'GONDOLA' | undefined,
origenDireccion: initialData.origenDireccion, origenDireccion: initialData.origenDireccion,
origenCiudad: initialData.origenCiudad, origenCiudad: initialData.origenCiudad || '',
origenEstado: initialData.origenEstado, origenEstado: initialData.origenEstado || '',
origenCP: initialData.origenCP || '', origenCP: initialData.origenCP || '',
origenPais: initialData.origenPais || 'MX', origenPais: initialData.origenPais || 'MX',
destinoDireccion: initialData.destinoDireccion, destinoDireccion: initialData.destinoDireccion,
destinoCiudad: initialData.destinoCiudad, destinoCiudad: initialData.destinoCiudad || '',
destinoEstado: initialData.destinoEstado, destinoEstado: initialData.destinoEstado || '',
destinoCP: initialData.destinoCP || '', destinoCP: initialData.destinoCP || '',
destinoPais: initialData.destinoPais || 'MX', destinoPais: initialData.destinoPais || 'MX',
fechaRecoleccion: initialData.fechaRecoleccion?.split('T')[0], fechaRecoleccion: initialData.fechaRecoleccion?.split('T')[0] || '',
fechaEntregaEstimada: initialData.fechaEntregaEstimada?.split('T')[0], fechaEntregaEstimada: initialData.fechaEntregaEstimada?.split('T')[0],
pesoBruto: initialData.pesoBruto, pesoBruto: initialData.pesoBruto,
pesoNeto: initialData.pesoNeto, pesoNeto: initialData.pesoNeto,
@ -99,8 +100,16 @@ export function OTForm({ initialData, onSuccess, onCancel }: OTFormProps) {
notas: initialData.notas || '', notas: initialData.notas || '',
} }
: { : {
modalidad: 'FTL', clienteId: '',
tipoCarga: 'GENERAL', origenDireccion: '',
origenCiudad: '',
origenEstado: '',
destinoDireccion: '',
destinoCiudad: '',
destinoEstado: '',
fechaRecoleccion: '',
modalidad: 'FTL' as const,
tipoCarga: 'GENERAL' as const,
origenPais: 'MX', origenPais: 'MX',
destinoPais: 'MX', destinoPais: 'MX',
moneda: 'MXN', moneda: 'MXN',
@ -124,10 +133,17 @@ export function OTForm({ initialData, onSuccess, onCancel }: OTFormProps) {
}); });
const onSubmit = async (data: OTFormData) => { const onSubmit = async (data: OTFormData) => {
const dto = {
...data,
tipoCarga: data.tipoCarga as TipoCarga,
modalidadServicio: data.modalidad as ModalidadServicio,
modalidad: data.modalidad as ModalidadServicio,
tipoEquipo: data.tipoEquipo as TipoEquipo | undefined,
};
if (isEditing) { if (isEditing) {
await updateMutation.mutateAsync(data as UpdateOTDto); await updateMutation.mutateAsync(dto as UpdateOTDto);
} else { } else {
await createMutation.mutateAsync(data as CreateOTDto); await createMutation.mutateAsync(dto as CreateOTDto);
} }
}; };

View File

@ -8,10 +8,13 @@ interface OTStatusBadgeProps {
const estadoConfig: Record<EstadoOrdenTransporte, { label: string; color: string }> = { const estadoConfig: Record<EstadoOrdenTransporte, { label: string; color: string }> = {
[EstadoOrdenTransporte.BORRADOR]: { label: 'Borrador', color: 'bg-gray-100 text-gray-800' }, [EstadoOrdenTransporte.BORRADOR]: { label: 'Borrador', color: 'bg-gray-100 text-gray-800' },
[EstadoOrdenTransporte.PENDIENTE]: { label: 'Pendiente', color: 'bg-yellow-100 text-yellow-800' }, [EstadoOrdenTransporte.PENDIENTE]: { label: 'Pendiente', color: 'bg-yellow-100 text-yellow-800' },
[EstadoOrdenTransporte.SOLICITADA]: { label: 'Solicitada', color: 'bg-orange-100 text-orange-800' },
[EstadoOrdenTransporte.CONFIRMADA]: { label: 'Confirmada', color: 'bg-blue-100 text-blue-800' }, [EstadoOrdenTransporte.CONFIRMADA]: { label: 'Confirmada', color: 'bg-blue-100 text-blue-800' },
[EstadoOrdenTransporte.PROGRAMADA]: { label: 'Programada', color: 'bg-indigo-100 text-indigo-800' }, [EstadoOrdenTransporte.ASIGNADA]: { label: 'Asignada', color: 'bg-indigo-100 text-indigo-800' },
[EstadoOrdenTransporte.EN_PROCESO]: { label: 'En Proceso', color: 'bg-purple-100 text-purple-800' }, [EstadoOrdenTransporte.EN_PROCESO]: { label: 'En Proceso', color: 'bg-purple-100 text-purple-800' },
[EstadoOrdenTransporte.EN_TRANSITO]: { label: 'En Tránsito', color: 'bg-cyan-100 text-cyan-800' },
[EstadoOrdenTransporte.COMPLETADA]: { label: 'Completada', color: 'bg-green-100 text-green-800' }, [EstadoOrdenTransporte.COMPLETADA]: { label: 'Completada', color: 'bg-green-100 text-green-800' },
[EstadoOrdenTransporte.ENTREGADA]: { label: 'Entregada', color: 'bg-emerald-100 text-emerald-800' },
[EstadoOrdenTransporte.FACTURADA]: { label: 'Facturada', color: 'bg-teal-100 text-teal-800' }, [EstadoOrdenTransporte.FACTURADA]: { label: 'Facturada', color: 'bg-teal-100 text-teal-800' },
[EstadoOrdenTransporte.CANCELADA]: { label: 'Cancelada', color: 'bg-red-100 text-red-800' }, [EstadoOrdenTransporte.CANCELADA]: { label: 'Cancelada', color: 'bg-red-100 text-red-800' },
}; };

View File

@ -25,6 +25,7 @@ export enum TipoCarga {
LIQUIDOS = 'LIQUIDOS', LIQUIDOS = 'LIQUIDOS',
CONTENEDOR = 'CONTENEDOR', CONTENEDOR = 'CONTENEDOR',
AUTOMOVILES = 'AUTOMOVILES', AUTOMOVILES = 'AUTOMOVILES',
FRAGIL = 'FRAGIL',
} }
export enum ModalidadServicio { export enum ModalidadServicio {
@ -51,8 +52,10 @@ export interface OrdenTransporte {
tenantId: string; tenantId: string;
codigo: string; codigo: string;
numeroOt?: string; numeroOt?: string;
numero?: string; // Alias for display
referenciaCliente?: string; referenciaCliente?: string;
clienteId: string; clienteId: string;
clienteNombre?: string; // For display
shipperId?: string; shipperId?: string;
shipperNombre: string; shipperNombre: string;
consigneeId: string; consigneeId: string;
@ -60,8 +63,10 @@ export interface OrdenTransporte {
// Origen // Origen
origenDireccion: string; origenDireccion: string;
origenCodigoPostal?: string; origenCodigoPostal?: string;
origenCP?: string; // Alias
origenCiudad?: string; origenCiudad?: string;
origenEstado?: string; origenEstado?: string;
origenPais?: string;
origenLatitud?: number; origenLatitud?: number;
origenLongitud?: number; origenLongitud?: number;
origenContacto?: string; origenContacto?: string;
@ -69,8 +74,10 @@ export interface OrdenTransporte {
// Destino // Destino
destinoDireccion: string; destinoDireccion: string;
destinoCodigoPostal?: string; destinoCodigoPostal?: string;
destinoCP?: string; // Alias
destinoCiudad?: string; destinoCiudad?: string;
destinoEstado?: string; destinoEstado?: string;
destinoPais?: string;
destinoLatitud?: number; destinoLatitud?: number;
destinoLongitud?: number; destinoLongitud?: number;
destinoContacto?: string; destinoContacto?: string;
@ -79,14 +86,21 @@ export interface OrdenTransporte {
fechaRecoleccion?: string; fechaRecoleccion?: string;
fechaRecoleccionProgramada?: string; fechaRecoleccionProgramada?: string;
fechaEntregaProgramada?: string; fechaEntregaProgramada?: string;
fechaEntregaEstimada?: string; // Alias
// Carga // Carga
tipoCarga: TipoCarga; tipoCarga: TipoCarga;
descripcionCarga?: string; descripcionCarga?: string;
descripcionMercancia?: string; // Alias
pesoKg?: number; pesoKg?: number;
pesoBruto?: number; // Alias
pesoNeto?: number;
volumenM3?: number; volumenM3?: number;
volumen?: number; // Alias
piezas?: number; piezas?: number;
pallets?: number; pallets?: number;
cantidadBultos?: number;
valorDeclarado?: number; valorDeclarado?: number;
valorMercancia?: number; // Alias
// Requisitos // Requisitos
requiereTemperatura: boolean; requiereTemperatura: boolean;
temperaturaMin?: number; temperaturaMin?: number;
@ -95,18 +109,24 @@ export interface OrdenTransporte {
requiereEscolta: boolean; requiereEscolta: boolean;
instruccionesEspeciales?: string; instruccionesEspeciales?: string;
observaciones?: string; observaciones?: string;
notas?: string; // Alias
// Servicio y tarifa // Servicio y tarifa
modalidadServicio: ModalidadServicio; modalidadServicio: ModalidadServicio;
modalidad?: ModalidadServicio; // Alias
tipoEquipo?: TipoEquipo;
tarifaId?: string; tarifaId?: string;
tarifaBase?: number; tarifaBase?: number;
tarifaTotal?: number;
recargos: number; recargos: number;
descuentos: number; descuentos: number;
subtotal?: number; subtotal?: number;
iva?: number; iva?: number;
total?: number; total?: number;
moneda?: string;
// Estado y asignacion // Estado y asignacion
estado: EstadoOrdenTransporte; estado: EstadoOrdenTransporte;
viajeId?: string; viajeId?: string;
viajeNumero?: string;
embarqueId?: string; embarqueId?: string;
// Auditoria // Auditoria
createdAt: string; createdAt: string;
@ -136,13 +156,15 @@ export interface CreateOrdenTransporteDto {
referenciaCliente?: string; referenciaCliente?: string;
clienteId: string; clienteId: string;
shipperId?: string; shipperId?: string;
shipperNombre: string; shipperNombre?: string;
consigneeId: string; consigneeId?: string;
consigneeNombre: string; consigneeNombre?: string;
origenDireccion: string; origenDireccion: string;
origenCodigoPostal?: string; origenCodigoPostal?: string;
origenCiudad?: string; origenCiudad?: string;
origenEstado?: string; origenEstado?: string;
origenPais?: string;
origenCP?: string;
origenLatitud?: number; origenLatitud?: number;
origenLongitud?: number; origenLongitud?: number;
origenContacto?: string; origenContacto?: string;
@ -151,19 +173,29 @@ export interface CreateOrdenTransporteDto {
destinoCodigoPostal?: string; destinoCodigoPostal?: string;
destinoCiudad?: string; destinoCiudad?: string;
destinoEstado?: string; destinoEstado?: string;
destinoPais?: string;
destinoCP?: string;
destinoLatitud?: number; destinoLatitud?: number;
destinoLongitud?: number; destinoLongitud?: number;
destinoContacto?: string; destinoContacto?: string;
destinoTelefono?: string; destinoTelefono?: string;
fechaRecoleccion?: string;
fechaRecoleccionProgramada?: string; fechaRecoleccionProgramada?: string;
fechaEntregaProgramada?: string; fechaEntregaProgramada?: string;
fechaEntregaEstimada?: string;
tipoCarga?: TipoCarga; tipoCarga?: TipoCarga;
descripcionCarga?: string; descripcionCarga?: string;
descripcionMercancia?: string;
pesoKg?: number; pesoKg?: number;
pesoBruto?: number;
pesoNeto?: number;
volumenM3?: number; volumenM3?: number;
volumen?: number;
piezas?: number; piezas?: number;
pallets?: number; pallets?: number;
cantidadBultos?: number;
valorDeclarado?: number; valorDeclarado?: number;
valorMercancia?: number;
requiereTemperatura?: boolean; requiereTemperatura?: boolean;
temperaturaMin?: number; temperaturaMin?: number;
temperaturaMax?: number; temperaturaMax?: number;
@ -171,9 +203,22 @@ export interface CreateOrdenTransporteDto {
requiereEscolta?: boolean; requiereEscolta?: boolean;
instruccionesEspeciales?: string; instruccionesEspeciales?: string;
modalidadServicio?: ModalidadServicio; modalidadServicio?: ModalidadServicio;
modalidad?: ModalidadServicio;
tipoEquipo?: TipoEquipo;
tarifaBase?: number;
tarifaTotal?: number;
moneda?: string;
notas?: string;
} }
export interface UpdateOrdenTransporteDto extends Partial<CreateOrdenTransporteDto> { export interface UpdateOrdenTransporteDto extends Partial<CreateOrdenTransporteDto> {
estado?: EstadoOrdenTransporte; estado?: EstadoOrdenTransporte;
observaciones?: string; observaciones?: string;
} }
// Type aliases for shorter names (used in components)
export type CreateOTDto = CreateOrdenTransporteDto;
export type UpdateOTDto = UpdateOrdenTransporteDto;
export type OTFilters = OrdenTransporteFilters & {
tipoCarga?: TipoCarga;
};

View File

@ -105,7 +105,7 @@ export function ETAProgressBar({
{/* Milestone markers */} {/* Milestone markers */}
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">
{milestones.map((milestone, index) => ( {milestones.map((milestone) => (
<div <div
key={milestone.label} key={milestone.label}
className="absolute flex flex-col items-center" className="absolute flex flex-col items-center"

View File

@ -11,7 +11,6 @@ import { TipoEventoTracking, FuenteEvento, type EventoTracking } from '../types'
interface EventTimelineProps { interface EventTimelineProps {
eventos: EventoTracking[]; eventos: EventoTracking[];
loading?: boolean; loading?: boolean;
viajeId?: string;
} }
const tipoEventoConfig: Record<TipoEventoTracking, { label: string; icon: string; color: string }> = { const tipoEventoConfig: Record<TipoEventoTracking, { label: string; icon: string; color: string }> = {
@ -100,7 +99,7 @@ const fuenteLabels: Record<FuenteEvento, string> = {
[FuenteEvento.GEOCERCA]: 'Geocerca', [FuenteEvento.GEOCERCA]: 'Geocerca',
}; };
export function EventTimeline({ eventos, loading = false, viajeId }: EventTimelineProps) { export function EventTimeline({ eventos, loading = false }: EventTimelineProps) {
// Filter out frequent GPS positions to show only meaningful events // Filter out frequent GPS positions to show only meaningful events
const eventosImportantes = eventos.filter( const eventosImportantes = eventos.filter(
(e) => (e) =>

View File

@ -1,7 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { trackingApi } from '../api/tracking.api'; import { trackingApi } from '../api/tracking.api';
import type { EventoTracking, EventoTrackingFilters, TipoEventoTracking } from '../types'; import { TipoEventoTracking } from '../types';
import type { EventoTracking, EventoFilters } from '../types';
interface EventosListProps { interface EventosListProps {
viajeId?: string; viajeId?: string;
@ -10,33 +11,39 @@ interface EventosListProps {
} }
const tipoEventoLabels: Record<TipoEventoTracking, string> = { const tipoEventoLabels: Record<TipoEventoTracking, string> = {
POSICION: 'Posición', [TipoEventoTracking.POSICION]: 'Posición',
ENCENDIDO: 'Encendido', [TipoEventoTracking.SALIDA]: 'Salida',
APAGADO: 'Apagado', [TipoEventoTracking.ARRIBO_ORIGEN]: 'Arribo a Origen',
VELOCIDAD_EXCESIVA: 'Velocidad Excesiva', [TipoEventoTracking.INICIO_CARGA]: 'Inicio de Carga',
ENTRADA_GEOCERCA: 'Entrada Geocerca', [TipoEventoTracking.FIN_CARGA]: 'Fin de Carga',
SALIDA_GEOCERCA: 'Salida Geocerca', [TipoEventoTracking.ARRIBO_DESTINO]: 'Arribo a Destino',
PARADA_PROLONGADA: 'Parada Prolongada', [TipoEventoTracking.INICIO_DESCARGA]: 'Inicio de Descarga',
DESVIO_RUTA: 'Desvío de Ruta', [TipoEventoTracking.FIN_DESCARGA]: 'Fin de Descarga',
BOTON_PANICO: 'Botón de Pánico', [TipoEventoTracking.ENTREGA_POD]: 'POD Entregado',
BATERIA_BAJA: 'Batería Baja', [TipoEventoTracking.DESVIO]: 'Desvío',
DESCONEXION_GPS: 'Desconexión GPS', [TipoEventoTracking.PARADA]: 'Parada',
RECONEXION_GPS: 'Reconexión GPS', [TipoEventoTracking.INCIDENTE]: 'Incidente',
[TipoEventoTracking.GPS_POSICION]: 'Posición GPS',
[TipoEventoTracking.GEOCERCA_ENTRADA]: 'Entrada Geocerca',
[TipoEventoTracking.GEOCERCA_SALIDA]: 'Salida Geocerca',
}; };
const tipoEventoColors: Record<TipoEventoTracking, string> = { const tipoEventoColors: Record<TipoEventoTracking, string> = {
POSICION: 'bg-gray-100 text-gray-800', [TipoEventoTracking.POSICION]: 'bg-gray-100 text-gray-800',
ENCENDIDO: 'bg-green-100 text-green-800', [TipoEventoTracking.SALIDA]: 'bg-green-100 text-green-800',
APAGADO: 'bg-red-100 text-red-800', [TipoEventoTracking.ARRIBO_ORIGEN]: 'bg-blue-100 text-blue-800',
VELOCIDAD_EXCESIVA: 'bg-orange-100 text-orange-800', [TipoEventoTracking.INICIO_CARGA]: 'bg-blue-200 text-blue-900',
ENTRADA_GEOCERCA: 'bg-blue-100 text-blue-800', [TipoEventoTracking.FIN_CARGA]: 'bg-blue-100 text-blue-800',
SALIDA_GEOCERCA: 'bg-purple-100 text-purple-800', [TipoEventoTracking.ARRIBO_DESTINO]: 'bg-purple-100 text-purple-800',
PARADA_PROLONGADA: 'bg-yellow-100 text-yellow-800', [TipoEventoTracking.INICIO_DESCARGA]: 'bg-purple-200 text-purple-900',
DESVIO_RUTA: 'bg-pink-100 text-pink-800', [TipoEventoTracking.FIN_DESCARGA]: 'bg-purple-100 text-purple-800',
BOTON_PANICO: 'bg-red-200 text-red-900', [TipoEventoTracking.ENTREGA_POD]: 'bg-green-200 text-green-900',
BATERIA_BAJA: 'bg-amber-100 text-amber-800', [TipoEventoTracking.DESVIO]: 'bg-yellow-100 text-yellow-800',
DESCONEXION_GPS: 'bg-gray-200 text-gray-900', [TipoEventoTracking.PARADA]: 'bg-orange-100 text-orange-800',
RECONEXION_GPS: 'bg-teal-100 text-teal-800', [TipoEventoTracking.INCIDENTE]: 'bg-red-100 text-red-800',
[TipoEventoTracking.GPS_POSICION]: 'bg-gray-200 text-gray-900',
[TipoEventoTracking.GEOCERCA_ENTRADA]: 'bg-cyan-100 text-cyan-800',
[TipoEventoTracking.GEOCERCA_SALIDA]: 'bg-cyan-200 text-cyan-900',
}; };
export function EventosList({ viajeId, unidadId, onSelect }: EventosListProps) { export function EventosList({ viajeId, unidadId, onSelect }: EventosListProps) {
@ -44,10 +51,10 @@ export function EventosList({ viajeId, unidadId, onSelect }: EventosListProps) {
const [tipoFilter, setTipoFilter] = useState<TipoEventoTracking | ''>(''); const [tipoFilter, setTipoFilter] = useState<TipoEventoTracking | ''>('');
const limit = 20; const limit = 20;
const filters: EventoTrackingFilters = { const filters: EventoFilters = {
viajeId, viajeId,
unidadId, unidadId,
tipo: tipoFilter || undefined, tipoEvento: tipoFilter || undefined,
limit, limit,
offset: (page - 1) * limit, offset: (page - 1) * limit,
}; };
@ -114,10 +121,10 @@ export function EventosList({ viajeId, unidadId, onSelect }: EventosListProps) {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${ className={`rounded-full px-2 py-0.5 text-xs font-medium ${
tipoEventoColors[evento.tipo] || 'bg-gray-100 text-gray-800' tipoEventoColors[evento.tipoEvento] || 'bg-gray-100 text-gray-800'
}`} }`}
> >
{tipoEventoLabels[evento.tipo] || evento.tipo} {tipoEventoLabels[evento.tipoEvento] || evento.tipoEvento}
</span> </span>
<span className="text-sm text-gray-500"> <span className="text-sm text-gray-500">
{formatDateTime(evento.timestamp)} {formatDateTime(evento.timestamp)}
@ -139,9 +146,9 @@ export function EventosList({ viajeId, unidadId, onSelect }: EventosListProps) {
)} )}
{/* Additional data */} {/* Additional data */}
{evento.datosExtra && Object.keys(evento.datosExtra).length > 0 && ( {evento.datosAdicionales && Object.keys(evento.datosAdicionales).length > 0 && (
<div className="mt-2 text-xs text-gray-500"> <div className="mt-2 text-xs text-gray-500">
{Object.entries(evento.datosExtra) {Object.entries(evento.datosAdicionales)
.slice(0, 3) .slice(0, 3)
.map(([key, value]) => ( .map(([key, value]) => (
<span key={key} className="mr-3"> <span key={key} className="mr-3">

View File

@ -1,7 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { trackingApi } from '../api/tracking.api'; import { trackingApi } from '../api/tracking.api';
import type { Geocerca, GeocercaFilters, TipoGeocerca } from '../types'; import { TipoGeocerca } from '../types';
import type { Geocerca, GeocercaFilters } from '../types';
interface GeocercasListProps { interface GeocercasListProps {
onSelect?: (geocerca: Geocerca) => void; onSelect?: (geocerca: Geocerca) => void;
@ -9,29 +10,29 @@ interface GeocercasListProps {
} }
const tipoGeocercaLabels: Record<TipoGeocerca, string> = { const tipoGeocercaLabels: Record<TipoGeocerca, string> = {
CLIENTE: 'Cliente', [TipoGeocerca.CIRCULAR]: 'Circular',
ALMACEN: 'Almacén', [TipoGeocerca.POLIGONAL]: 'Poligonal',
GASOLINERA: 'Gasolinera', [TipoGeocerca.CLIENTE]: 'Cliente',
CASETA: 'Caseta', [TipoGeocerca.PROVEEDOR]: 'Proveedor',
PUNTO_CONTROL: 'Punto de Control', [TipoGeocerca.PATIO]: 'Patio',
ZONA_RIESGO: 'Zona de Riesgo', [TipoGeocerca.ZONA_RIESGO]: 'Zona de Riesgo',
ZONA_DESCANSO: 'Zona de Descanso', [TipoGeocerca.CASETA]: 'Caseta',
FRONTERA: 'Frontera', [TipoGeocerca.GASOLINERA]: 'Gasolinera',
ADUANA: 'Aduana', [TipoGeocerca.PUNTO_CONTROL]: 'Punto de Control',
PUERTO: 'Puerto', [TipoGeocerca.OTRO]: 'Otro',
}; };
const tipoGeocercaIcons: Record<TipoGeocerca, string> = { const tipoGeocercaIcons: Record<TipoGeocerca, string> = {
CLIENTE: '🏢', [TipoGeocerca.CIRCULAR]: '⭕',
ALMACEN: '📦', [TipoGeocerca.POLIGONAL]: '🔷',
GASOLINERA: '⛽', [TipoGeocerca.CLIENTE]: '🏢',
CASETA: '🚧', [TipoGeocerca.PROVEEDOR]: '🏭',
PUNTO_CONTROL: '✓', [TipoGeocerca.PATIO]: '📦',
ZONA_RIESGO: '⚠️', [TipoGeocerca.ZONA_RIESGO]: '⚠️',
ZONA_DESCANSO: '🅿️', [TipoGeocerca.CASETA]: '🚧',
FRONTERA: '🚩', [TipoGeocerca.GASOLINERA]: '⛽',
ADUANA: '🛃', [TipoGeocerca.PUNTO_CONTROL]: '✓',
PUERTO: '⚓', [TipoGeocerca.OTRO]: '📍',
}; };
export function GeocercasList({ onSelect, onEdit }: GeocercasListProps) { export function GeocercasList({ onSelect, onEdit }: GeocercasListProps) {
@ -44,8 +45,7 @@ export function GeocercasList({ onSelect, onEdit }: GeocercasListProps) {
const filters: GeocercaFilters = { const filters: GeocercaFilters = {
tipo: tipoFilter || undefined, tipo: tipoFilter || undefined,
activo: activoFilter !== '' ? activoFilter : undefined, activa: activoFilter !== '' ? activoFilter : undefined,
search: searchTerm || undefined,
limit, limit,
offset: (page - 1) * limit, offset: (page - 1) * limit,
}; };
@ -160,9 +160,9 @@ export function GeocercasList({ onSelect, onEdit }: GeocercasListProps) {
> >
<td className="whitespace-nowrap px-4 py-3"> <td className="whitespace-nowrap px-4 py-3">
<div className="font-medium text-gray-900">{geocerca.nombre}</div> <div className="font-medium text-gray-900">{geocerca.nombre}</div>
{geocerca.descripcion && ( {geocerca.direccion && (
<div className="text-sm text-gray-500 line-clamp-1"> <div className="text-sm text-gray-500 line-clamp-1">
{geocerca.descripcion} {geocerca.direccion}
</div> </div>
)} )}
</td> </td>
@ -176,12 +176,12 @@ export function GeocercasList({ onSelect, onEdit }: GeocercasListProps) {
<td className="whitespace-nowrap px-4 py-3"> <td className="whitespace-nowrap px-4 py-3">
<span <span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${ className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${
geocerca.activo geocerca.activa
? 'bg-green-100 text-green-800' ? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800' : 'bg-gray-100 text-gray-800'
}`} }`}
> >
{geocerca.activo ? 'Activa' : 'Inactiva'} {geocerca.activa ? 'Activa' : 'Inactiva'}
</span> </span>
</td> </td>
<td className="whitespace-nowrap px-4 py-3 text-right text-sm"> <td className="whitespace-nowrap px-4 py-3 text-right text-sm">

View File

@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { trackingApi } from '../api/tracking.api'; import { trackingApi } from '../api/tracking.api';
import type { PosicionActual, EventoTracking } from '../types'; import type { PosicionActual } from '../types';
interface TrackingMapProps { interface TrackingMapProps {
unidadIds?: string[]; unidadIds?: string[];
@ -38,7 +38,7 @@ export function TrackingMap({
const rutaData = ruta?.data || []; const rutaData = ruta?.data || [];
// Simple map rendering (placeholder - integrate with actual map library) // Simple map rendering (placeholder - integrate with actual map library)
const renderPosition = (pos: PosicionActual, index: number) => { const renderPosition = (pos: PosicionActual, _index: number) => {
const isSelected = selectedUnidad === pos.unidadId; const isSelected = selectedUnidad === pos.unidadId;
return ( return (
<div <div

View File

@ -11,14 +11,13 @@ import { useQuery } from '@tanstack/react-query';
import { trackingApi } from '../api/tracking.api'; import { trackingApi } from '../api/tracking.api';
import { ETAProgressBar } from './ETAProgressBar'; import { ETAProgressBar } from './ETAProgressBar';
import { EventTimeline } from './EventTimeline'; import { EventTimeline } from './EventTimeline';
import { TipoEventoTracking, type EventoTracking } from '../types'; import { TipoEventoTracking } from '../types';
interface ViajeTrackingViewProps { interface ViajeTrackingViewProps {
viajeId: string; viajeId: string;
folio: string; folio: string;
origen: string; origen: string;
destino: string; destino: string;
fechaSalida: string;
etaOriginal: string; etaOriginal: string;
onClose?: () => void; onClose?: () => void;
} }
@ -40,7 +39,6 @@ export function ViajeTrackingView({
folio, folio,
origen, origen,
destino, destino,
fechaSalida,
etaOriginal, etaOriginal,
onClose, onClose,
}: ViajeTrackingViewProps) { }: ViajeTrackingViewProps) {
@ -254,7 +252,6 @@ export function ViajeTrackingView({
<EventTimeline <EventTimeline
eventos={eventos} eventos={eventos}
loading={loadingEventos} loading={loadingEventos}
viajeId={viajeId}
/> />
</div> </div>
</div> </div>

View File

@ -48,7 +48,7 @@ export function useTrackingWebSocket({
const [error, setError] = useState<Error | null>(null); const [error, setError] = useState<Error | null>(null);
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null); const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const getWebSocketUrl = useCallback(() => { const getWebSocketUrl = useCallback(() => {
// Build WebSocket URL with subscription parameters // Build WebSocket URL with subscription parameters

View File

@ -2,11 +2,12 @@ import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { viajesApi } from '../api/viajes.api'; import { viajesApi } from '../api/viajes.api';
import { ViajeStatusBadge } from './ViajeStatusBadge'; import { ViajeStatusBadge } from './ViajeStatusBadge';
import type { Viaje, EstadoViaje } from '../types'; import type { Viaje } from '../types';
interface ViajeDetailProps { interface ViajeDetailProps {
viaje: Viaje; viaje: Viaje;
onClose: () => void; onClose: () => void;
onEdit?: () => void;
} }
export function ViajeDetail({ viaje, onClose }: ViajeDetailProps) { export function ViajeDetail({ viaje, onClose }: ViajeDetailProps) {

View File

@ -22,12 +22,13 @@ type FormData = z.infer<typeof viajeSchema>;
interface ViajeFormProps { interface ViajeFormProps {
viaje?: Viaje; viaje?: Viaje;
onSubmit: (data: CreateViajeDto | UpdateViajeDto) => Promise<void>; onSubmit?: (data: CreateViajeDto | UpdateViajeDto) => Promise<void>;
onSuccess?: () => void;
onCancel: () => void; onCancel: () => void;
isLoading?: boolean; isLoading?: boolean;
} }
export function ViajeForm({ viaje, onSubmit, onCancel, isLoading }: ViajeFormProps) { export function ViajeForm({ viaje, onSubmit, onSuccess, onCancel, isLoading }: ViajeFormProps) {
const isEditing = !!viaje; const isEditing = !!viaje;
const { const {
@ -80,7 +81,10 @@ export function ViajeForm({ viaje, onSubmit, onCancel, isLoading }: ViajeFormPro
...(data.distanciaEstimadaKm && { distanciaEstimadaKm: data.distanciaEstimadaKm }), ...(data.distanciaEstimadaKm && { distanciaEstimadaKm: data.distanciaEstimadaKm }),
...(data.tiempoEstimadoHoras && { tiempoEstimadoHoras: data.tiempoEstimadoHoras }), ...(data.tiempoEstimadoHoras && { tiempoEstimadoHoras: data.tiempoEstimadoHoras }),
}; };
await onSubmit(cleanData); if (onSubmit) {
await onSubmit(cleanData);
}
onSuccess?.();
}; };
return ( return (

View File

@ -5,7 +5,8 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true "strict": true,
"types": ["node"]
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }