/** * 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(); 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 = {}; 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; }