feat: endpoints RBAC - GET /mi-ruta/eta y GET /mi-ruta/estado
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const authRoutes = require('./routes/auth.routes');
|
||||
const rutaRoutes = require('./routes/ruta.routes');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -8,12 +9,13 @@ app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// ── Rutas ────────────────────────────────────────────────────────
|
||||
app.use('/auth', authRoutes);
|
||||
app.use('/auth', authRoutes);
|
||||
app.use('/mi-ruta', rutaRoutes); // GET /mi-ruta/eta GET /mi-ruta/estado
|
||||
|
||||
// ── Health check ─────────────────────────────────────────────────
|
||||
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
||||
|
||||
// ── Manejo de rutas no encontradas ───────────────────────────────
|
||||
// ── Ruta no encontrada ────────────────────────────────────────────
|
||||
app.use((_req, res) => res.status(404).json({ error: 'Ruta no encontrada' }));
|
||||
|
||||
module.exports = app;
|
||||
module.exports = app;
|
||||
271
backend/src/routes/ruta.routes.js
Normal file
271
backend/src/routes/ruta.routes.js
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* ================================================================
|
||||
* RUTAS RBAC - Endpoints del ciudadano
|
||||
*
|
||||
* GET /mi-ruta/eta → ventana horaria de llegada
|
||||
* GET /mi-ruta/estado → estado actual + mensaje preventivo
|
||||
*
|
||||
* REGLA DE ORO: nunca exponer position_id_actual ni coordenadas.
|
||||
* El ciudadano solo ve mensajes legibles y su propia ruta.
|
||||
* ================================================================
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const pool = require('../config/db');
|
||||
const { verificarToken} = require('../middleware/auth.middleware');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Todos los endpoints de este router requieren JWT válido
|
||||
router.use(verificarToken);
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Formatea un TIME de Postgres ("06:30:00") a "6:30 a.m." / "2:15 p.m."
|
||||
*/
|
||||
function formatearHora(timeStr) {
|
||||
const [h, m] = timeStr.split(':').map(Number);
|
||||
const periodo = h >= 12 ? 'p.m.' : 'a.m.';
|
||||
const h12 = h % 12 || 12;
|
||||
return `${h12}:${String(m).padStart(2, '0')} ${periodo}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula minutos restantes hasta una fecha estimada
|
||||
*/
|
||||
function minutosRestantes(horaEstimFin) {
|
||||
if (!horaEstimFin) return null;
|
||||
const diff = new Date(horaEstimFin) - new Date();
|
||||
return Math.max(0, Math.round(diff / 60000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el mensaje de ETA legible para el ciudadano
|
||||
*/
|
||||
function mensajeETA(estado, horaEstimFin, horaInicioEst, horaFinEst) {
|
||||
if (estado === 'FINALIZADA') {
|
||||
return 'El servicio de recolección de hoy ya finalizó en tu zona.';
|
||||
}
|
||||
|
||||
if (estado === 'INACTIVA') {
|
||||
if (horaInicioEst && horaFinEst) {
|
||||
return `El camión pasará por tu zona entre las ${formatearHora(horaInicioEst)} y las ${formatearHora(horaFinEst)}.`;
|
||||
}
|
||||
return 'El camión aún no ha iniciado su ruta hoy.';
|
||||
}
|
||||
|
||||
if (estado === 'EN_RUTA' || estado === 'RETRASADA') {
|
||||
const mins = minutosRestantes(horaEstimFin);
|
||||
|
||||
if (mins === null) {
|
||||
return horaInicioEst && horaFinEst
|
||||
? `El camión llegará a tu zona entre las ${formatearHora(horaInicioEst)} y las ${formatearHora(horaFinEst)}.`
|
||||
: 'El camión está en camino a tu zona.';
|
||||
}
|
||||
|
||||
if (mins <= 0) return 'El camión está llegando a tu zona ahora.';
|
||||
if (mins <= 10) return `El camión llegará a tu zona en aproximadamente ${mins} minutos.`;
|
||||
if (mins <= 30) return `El camión llegará a tu zona en aproximadamente ${mins} minutos.`;
|
||||
|
||||
// Si falta más de 30 min, dar ventana horaria con la colonia
|
||||
if (horaInicioEst && horaFinEst) {
|
||||
return `El camión llegará a tu zona entre las ${formatearHora(horaInicioEst)} y las ${formatearHora(horaFinEst)}.`;
|
||||
}
|
||||
|
||||
return `El camión llegará a tu zona en aproximadamente ${mins} minutos.`;
|
||||
}
|
||||
|
||||
return 'No hay información disponible en este momento.';
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// GET /mi-ruta/eta
|
||||
// Devuelve la ventana horaria de llegada del camión
|
||||
// La app Android usa este endpoint para la pantalla principal
|
||||
// ================================================================
|
||||
router.get('/eta', async (req, res) => {
|
||||
const { routeId, domicilioId } = req.user;
|
||||
|
||||
if (!routeId) {
|
||||
return res.status(404).json({
|
||||
error: 'Tu domicilio aún no tiene una ruta asignada.',
|
||||
detalle: 'El sistema validará tu dirección pronto.',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT
|
||||
e.estado,
|
||||
e.hora_estim_fin,
|
||||
e.minutos_retraso,
|
||||
e.actualizado_at,
|
||||
r.route_id,
|
||||
r.hora_inicio,
|
||||
r.hora_fin_ref,
|
||||
c.nombre AS colonia_nombre,
|
||||
c.horario_turno,
|
||||
c.hora_inicio_est,
|
||||
c.hora_fin_est
|
||||
FROM estado_ruta e
|
||||
JOIN rutas r ON r.id = e.ruta_id
|
||||
LEFT JOIN domicilios d ON d.id = $2
|
||||
LEFT JOIN colonias c ON c.id = d.colonia_id
|
||||
WHERE r.route_id = $1`,
|
||||
[routeId, domicilioId]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Ruta no encontrada' });
|
||||
}
|
||||
|
||||
const datos = rows[0];
|
||||
const estado = datos.estado;
|
||||
|
||||
const eta = mensajeETA(
|
||||
estado,
|
||||
datos.hora_estim_fin,
|
||||
datos.hora_inicio_est,
|
||||
datos.hora_fin_est
|
||||
);
|
||||
|
||||
// Ventana horaria estructurada (para que la app construya su UI)
|
||||
const ventana = datos.hora_inicio_est && datos.hora_fin_est
|
||||
? {
|
||||
inicio: formatearHora(datos.hora_inicio_est),
|
||||
fin: formatearHora(datos.hora_fin_est),
|
||||
turno: datos.horario_turno,
|
||||
}
|
||||
: null;
|
||||
|
||||
return res.json({
|
||||
eta,
|
||||
ventana,
|
||||
estado,
|
||||
retrasado: datos.minutos_retraso > 0,
|
||||
minutos_retraso: datos.minutos_retraso || 0,
|
||||
actualizado_hace: Math.round(
|
||||
(new Date() - new Date(datos.actualizado_at)) / 60000
|
||||
) + ' min',
|
||||
// Mensaje preventivo (requisito del reto)
|
||||
aviso: estado === 'EN_RUTA'
|
||||
? '⚠️ No salgas a buscar el camión. Espera en tu domicilio y ten tus residuos listos.'
|
||||
: null,
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Error en GET /mi-ruta/eta:', err.message);
|
||||
return res.status(500).json({ error: 'Error interno del servidor' });
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// GET /mi-ruta/estado
|
||||
// Devuelve el estado completo de la ruta del ciudadano
|
||||
// Incluye horario de colonia, estado del camión y avisos
|
||||
// ================================================================
|
||||
router.get('/estado', async (req, res) => {
|
||||
const { routeId, domicilioId } = req.user;
|
||||
|
||||
if (!routeId) {
|
||||
return res.status(404).json({
|
||||
error: 'Tu domicilio aún no tiene una ruta asignada.',
|
||||
detalle: 'El sistema validará tu dirección pronto.',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT
|
||||
e.estado,
|
||||
e.hora_estim_fin,
|
||||
e.minutos_retraso,
|
||||
e.motivo_retraso,
|
||||
e.actualizado_at,
|
||||
r.route_id,
|
||||
d.alias AS domicilio_alias,
|
||||
d.direccion_texto,
|
||||
d.validado,
|
||||
c.nombre AS colonia_nombre,
|
||||
c.horario_turno,
|
||||
c.hora_inicio_est,
|
||||
c.hora_fin_est
|
||||
FROM estado_ruta e
|
||||
JOIN rutas r ON r.id = e.ruta_id
|
||||
LEFT JOIN domicilios d ON d.id = $2
|
||||
LEFT JOIN colonias c ON c.id = d.colonia_id
|
||||
WHERE r.route_id = $1`,
|
||||
[routeId, domicilioId]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Ruta no encontrada' });
|
||||
}
|
||||
|
||||
const datos = rows[0];
|
||||
const estado = datos.estado;
|
||||
|
||||
// Mensajes por estado (citizen-friendly)
|
||||
const mensajes = {
|
||||
INACTIVA: 'El camión aún no ha iniciado su ruta hoy.',
|
||||
EN_RUTA: 'El camión está en camino hacia tu zona.',
|
||||
RETRASADA: `El camión presenta un retraso de ${datos.minutos_retraso} minutos.`,
|
||||
FINALIZADA: 'El servicio de recolección de hoy ya finalizó.',
|
||||
};
|
||||
|
||||
// Avisos preventivos según estado
|
||||
const avisos = {
|
||||
INACTIVA: 'No saques tu basura antes del horario indicado.',
|
||||
EN_RUTA: '⚠️ No persigas al camión. Espera en tu domicilio con los residuos listos.',
|
||||
RETRASADA: '⚠️ Tu camión está retrasado. Mantén los residuos en tu domicilio hasta que llegue.',
|
||||
FINALIZADA: 'Si el camión no pasó hoy, repórtalo en la sección de incidencias.',
|
||||
};
|
||||
|
||||
return res.json({
|
||||
estado,
|
||||
mensaje: mensajes[estado] || 'Sin información disponible.',
|
||||
aviso: avisos[estado] || null,
|
||||
|
||||
// Horario programado de la colonia
|
||||
horario: datos.hora_inicio_est && datos.hora_fin_est
|
||||
? {
|
||||
turno: datos.horario_turno,
|
||||
inicio: formatearHora(datos.hora_inicio_est),
|
||||
fin: formatearHora(datos.hora_fin_est),
|
||||
}
|
||||
: null,
|
||||
|
||||
// ETA calculado
|
||||
eta: mensajeETA(
|
||||
estado,
|
||||
datos.hora_estim_fin,
|
||||
datos.hora_inicio_est,
|
||||
datos.hora_fin_est
|
||||
),
|
||||
|
||||
// Info del domicilio registrado (solo el del usuario)
|
||||
domicilio: {
|
||||
alias: datos.domicilio_alias,
|
||||
colonia: datos.colonia_nombre,
|
||||
validado: datos.validado,
|
||||
},
|
||||
|
||||
// Retraso
|
||||
retraso: datos.minutos_retraso > 0
|
||||
? {
|
||||
minutos: datos.minutos_retraso,
|
||||
motivo: datos.motivo_retraso || 'Causa operativa',
|
||||
}
|
||||
: null,
|
||||
|
||||
actualizado_at: datos.actualizado_at,
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Error en GET /mi-ruta/estado:', err.message);
|
||||
return res.status(500).json({ error: 'Error interno del servidor' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user