erp-transportistas-backend-v2/src/modules/gps/controllers/evento-geocerca.controller.ts
Adrian Flores Cortes a4b1b2fd34 [SPRINT-6] feat: Implement transport module controllers
Carta Porte Module (4 controllers):
- mercancia.controller.ts: Cargo management endpoints
- ubicacion-carta-porte.controller.ts: Location endpoints
- figura-transporte.controller.ts: Transport figures endpoints
- inspeccion-pre-viaje.controller.ts: Pre-trip inspection endpoints

Gestion Flota Module (2 controllers):
- documento-flota.controller.ts: Fleet document management
- asignacion.controller.ts: Unit-operator assignment endpoints

Tarifas Transporte Module (2 controllers):
- factura-transporte.controller.ts: Invoice endpoints with IVA 16%
- recargos.controller.ts: Surcharge catalog and fuel surcharge endpoints

GPS Module (1 controller):
- evento-geocerca.controller.ts: Geofence event endpoints

Total: 9 new controllers following Express Router pattern

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 03:00:12 -06:00

427 lines
13 KiB
TypeScript

/**
* EventoGeocerca Controller
* ERP Transportistas
*
* REST API endpoints for geofence events (entry/exit/permanence).
* Adapted from erp-mecanicas-diesel MMD-014 GPS Integration
* Module: MAI-006 Tracking
*/
import { Router, Request, Response, NextFunction } from 'express';
import { DataSource } from 'typeorm';
import {
EventoGeocercaService,
EventoGeocercaFilters,
DateRange,
} from '../services/evento-geocerca.service';
import { TipoEventoGeocerca } from '../entities/evento-geocerca.entity';
interface TenantRequest extends Request {
tenantId?: string;
userId?: string;
}
export function createEventoGeocercaController(dataSource: DataSource): Router {
const router = Router();
const service = new EventoGeocercaService(dataSource);
const extractTenant = (req: TenantRequest, res: Response, next: NextFunction) => {
const tenantId = req.headers['x-tenant-id'] as string;
if (!tenantId) {
return res.status(400).json({ error: 'Tenant ID es requerido' });
}
req.tenantId = tenantId;
req.userId = req.headers['x-user-id'] as string;
next();
};
router.use(extractTenant);
/**
* Helper to parse date range from query parameters
*/
const parseDateRange = (query: any): DateRange => {
const now = new Date();
const defaultStart = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago
return {
fechaInicio: query.fechaDesde ? new Date(query.fechaDesde as string) : defaultStart,
fechaFin: query.fechaHasta ? new Date(query.fechaHasta as string) : now,
};
};
/**
* List geofence events with filters
* GET /api/gps/eventos-geocerca
* Query params: geocercaId, unidadId, viajeId, tipoEvento, fechaDesde, fechaHasta, page, limit
*/
router.get('/', async (req: TenantRequest, res: Response) => {
try {
const filters: EventoGeocercaFilters = {
geocercaId: req.query.geocercaId as string,
unidadId: req.query.unidadId as string,
viajeId: req.query.viajeId as string,
tipoEvento: req.query.tipoEvento as TipoEventoGeocerca,
dispositivoId: req.query.dispositivoId as string,
};
const pagination = {
page: parseInt(req.query.page as string, 10) || 1,
limit: Math.min(parseInt(req.query.limit as string, 10) || 50, 200),
};
const result = await service.findAll(req.tenantId!, filters, pagination);
res.json(result);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Get geofence alerts (events marked with alerts)
* GET /api/gps/eventos-geocerca/alertas
*/
router.get('/alertas', async (req: TenantRequest, res: Response) => {
try {
const dateRange = parseDateRange(req.query);
const limit = parseInt(req.query.limit as string, 10) || 50;
// Get recent events and filter those with alerts
const result = await service.findAll(
req.tenantId!,
{},
{ page: 1, limit: 500 }
);
const alertas = result.data.filter(
(evento) =>
evento.metadata?.alertaTriggered === true &&
evento.tiempoEvento >= dateRange.fechaInicio &&
evento.tiempoEvento <= dateRange.fechaFin
);
res.json({
data: alertas.slice(0, limit),
total: alertas.length,
fechaDesde: dateRange.fechaInicio,
fechaHasta: dateRange.fechaFin,
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Get summary statistics by geofence
* GET /api/gps/eventos-geocerca/resumen/geocerca/:geocercaId
*/
router.get('/resumen/geocerca/:geocercaId', async (req: TenantRequest, res: Response) => {
try {
const dateRange = parseDateRange(req.query);
const estadisticas = await service.getEstadisticas(
req.tenantId!,
req.params.geocercaId,
dateRange
);
res.json(estadisticas);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Get summary statistics by unit
* GET /api/gps/eventos-geocerca/resumen/unidad/:unidadId
*/
router.get('/resumen/unidad/:unidadId', async (req: TenantRequest, res: Response) => {
try {
const dateRange = parseDateRange(req.query);
// Get events for this unit
const eventos = await service.findByUnidad(
req.tenantId!,
req.params.unidadId,
dateRange
);
// Calculate summary statistics
const geocercasVisitadas = new Set<string>();
let totalEntradas = 0;
let totalSalidas = 0;
let totalPermanencias = 0;
for (const evento of eventos) {
geocercasVisitadas.add(evento.geocercaId);
switch (evento.tipoEvento) {
case TipoEventoGeocerca.ENTRADA:
totalEntradas++;
break;
case TipoEventoGeocerca.SALIDA:
totalSalidas++;
break;
case TipoEventoGeocerca.PERMANENCIA:
totalPermanencias++;
break;
}
}
res.json({
unidadId: req.params.unidadId,
totalEventos: eventos.length,
totalEntradas,
totalSalidas,
totalPermanencias,
geocercasVisitadas: geocercasVisitadas.size,
geocercaIds: Array.from(geocercasVisitadas),
periodoInicio: dateRange.fechaInicio,
periodoFin: dateRange.fechaFin,
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Get dwell time (permanence) statistics by geofence
* GET /api/gps/eventos-geocerca/permanencia/geocerca/:geocercaId
*/
router.get('/permanencia/geocerca/:geocercaId', async (req: TenantRequest, res: Response) => {
try {
const dateRange = parseDateRange(req.query);
// Get events for this geofence
const eventos = await service.findByGeocerca(
req.tenantId!,
req.params.geocercaId,
dateRange
);
// Group events by unit and calculate dwell times
const unidadEventos: Record<string, typeof eventos> = {};
for (const evento of eventos) {
if (!unidadEventos[evento.unidadId]) {
unidadEventos[evento.unidadId] = [];
}
unidadEventos[evento.unidadId].push(evento);
}
const tiemposPorUnidad: Array<{
unidadId: string;
tiempoTotalMinutos: number;
visitas: number;
promedioMinutosPorVisita: number;
}> = [];
for (const [unidadId, eventosUnidad] of Object.entries(unidadEventos)) {
const tiempoInfo = await service.getTiempoEnGeocerca(
req.tenantId!,
unidadId,
req.params.geocercaId
);
tiemposPorUnidad.push({
unidadId,
tiempoTotalMinutos: tiempoInfo.tiempoTotalMinutos,
visitas: tiempoInfo.visitasTotales,
promedioMinutosPorVisita: tiempoInfo.promedioMinutosPorVisita,
});
}
// Sort by total time descending
tiemposPorUnidad.sort((a, b) => b.tiempoTotalMinutos - a.tiempoTotalMinutos);
const tiempoTotalGeocerca = tiemposPorUnidad.reduce(
(sum, t) => sum + t.tiempoTotalMinutos,
0
);
const visitasTotales = tiemposPorUnidad.reduce((sum, t) => sum + t.visitas, 0);
res.json({
geocercaId: req.params.geocercaId,
tiempoTotalMinutos: Math.round(tiempoTotalGeocerca * 100) / 100,
visitasTotales,
unidadesUnicas: tiemposPorUnidad.length,
promedioMinutosPorVisita:
visitasTotales > 0
? Math.round((tiempoTotalGeocerca / visitasTotales) * 100) / 100
: 0,
detallesPorUnidad: tiemposPorUnidad,
periodoInicio: dateRange.fechaInicio,
periodoFin: dateRange.fechaFin,
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Get events by geofence
* GET /api/gps/eventos-geocerca/geocerca/:geocercaId
*/
router.get('/geocerca/:geocercaId', async (req: TenantRequest, res: Response) => {
try {
const dateRange = parseDateRange(req.query);
const eventos = await service.findByGeocerca(
req.tenantId!,
req.params.geocercaId,
dateRange
);
res.json({
data: eventos,
total: eventos.length,
geocercaId: req.params.geocercaId,
fechaDesde: dateRange.fechaInicio,
fechaHasta: dateRange.fechaFin,
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Get events by unit
* GET /api/gps/eventos-geocerca/unidad/:unidadId
*/
router.get('/unidad/:unidadId', async (req: TenantRequest, res: Response) => {
try {
const dateRange = parseDateRange(req.query);
const eventos = await service.findByUnidad(
req.tenantId!,
req.params.unidadId,
dateRange
);
res.json({
data: eventos,
total: eventos.length,
unidadId: req.params.unidadId,
fechaDesde: dateRange.fechaInicio,
fechaHasta: dateRange.fechaFin,
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Get events by trip
* GET /api/gps/eventos-geocerca/viaje/:viajeId
*/
router.get('/viaje/:viajeId', async (req: TenantRequest, res: Response) => {
try {
const filters: EventoGeocercaFilters = {
viajeId: req.params.viajeId,
};
const result = await service.findAll(req.tenantId!, filters, { page: 1, limit: 500 });
res.json({
data: result.data,
total: result.total,
viajeId: req.params.viajeId,
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Register a new geofence event
* POST /api/gps/eventos-geocerca
*/
router.post('/', async (req: TenantRequest, res: Response) => {
try {
const evento = await service.create(req.tenantId!, {
geocercaId: req.body.geocercaId,
dispositivoId: req.body.dispositivoId,
unidadId: req.body.unidadId,
tipoEvento: req.body.tipoEvento,
posicionId: req.body.posicionId,
latitud: parseFloat(req.body.latitud),
longitud: parseFloat(req.body.longitud),
tiempoEvento: new Date(req.body.tiempoEvento),
viajeId: req.body.viajeId,
metadata: req.body.metadata,
});
res.status(201).json(evento);
} catch (error) {
res.status(400).json({ error: (error as Error).message });
}
});
/**
* Trigger alert for an event
* POST /api/gps/eventos-geocerca/:id/alerta
*/
router.post('/:id/alerta', async (req: TenantRequest, res: Response) => {
try {
const evento = await service.triggerAlerta(req.tenantId!, req.params.id);
if (!evento) {
return res.status(404).json({ error: 'Evento de geocerca no encontrado' });
}
res.json(evento);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Get units currently inside a geofence
* GET /api/gps/eventos-geocerca/geocerca/:geocercaId/unidades-dentro
*/
router.get('/geocerca/:geocercaId/unidades-dentro', async (req: TenantRequest, res: Response) => {
try {
const unidades = await service.getUnidadesEnGeocerca(
req.tenantId!,
req.params.geocercaId
);
res.json({
data: unidades,
total: unidades.length,
geocercaId: req.params.geocercaId,
timestamp: new Date(),
});
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Get event by ID
* GET /api/gps/eventos-geocerca/:id
*/
router.get('/:id', async (req: TenantRequest, res: Response) => {
try {
const result = await service.findAll(
req.tenantId!,
{},
{ page: 1, limit: 1 }
);
// Find by ID in result
const evento = result.data.find((e) => e.id === req.params.id);
if (!evento) {
return res.status(404).json({ error: 'Evento de geocerca no encontrado' });
}
res.json(evento);
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
/**
* Delete old events (data retention)
* DELETE /api/gps/eventos-geocerca/antiguos
*/
router.delete('/antiguos', async (req: TenantRequest, res: Response) => {
try {
const antesDe = new Date(req.query.antesDe as string);
if (isNaN(antesDe.getTime())) {
return res.status(400).json({ error: 'Fecha inválida. Use formato ISO 8601.' });
}
const eliminados = await service.eliminarEventosAntiguos(req.tenantId!, antesDe);
res.json({ eliminados });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
return router;
}