diff --git a/servicios/nginx/dtic-bitacoras/backend/src/routes/ambitos.js b/servicios/nginx/dtic-bitacoras/backend/src/routes/ambitos.js index 824f7773..d5d78394 100644 --- a/servicios/nginx/dtic-bitacoras/backend/src/routes/ambitos.js +++ b/servicios/nginx/dtic-bitacoras/backend/src/routes/ambitos.js @@ -114,17 +114,17 @@ module.exports = (pool) => { const nodos = await pool.query('SELECT * FROM bitacoras.nodos WHERE ambito_id = $1 AND activo = true', [id]); const entradasRecientes = await pool.query(` - SELECT e.*, n.nombre as nodo_nombre - FROM bitacoras.entradas e - JOIN bitacoras.nodos n ON e.nodo_id = n.id - WHERE n.ambito_id = $1 + SELECT e.*, n.nombre as nodo_nombre + FROM bitacoras.entradas e + JOIN bitacoras.nodos n ON e.nodo_id = n.id + WHERE n.ambito_id = $1 ORDER BY e.inicio DESC LIMIT 10 `, [id]); - + const gestionPendiente = await pool.query(` - SELECT g.*, n.nombre as nodo_nombre - FROM bitacoras.gestion g - JOIN bitacoras.nodos n ON g.nodo_id = n.id + SELECT g.*, n.nombre as nodo_nombre + FROM bitacoras.gestion g + JOIN bitacoras.nodos n ON g.nodo_id = n.id WHERE n.ambito_id = $1 AND g.activo = true AND g.tipo = 'pendiente' `, [id]); @@ -139,5 +139,86 @@ module.exports = (pool) => { } }); + // GET /api/ambitos/:codigo/proyecto — Dashboard con planes e hitos (compatible con frontend P2601) + router.get('/:codigo/proyecto', async (req, res) => { + const { codigo } = req.params; + + const aResult = await pool.query('SELECT * FROM bitacoras.ambitos WHERE codigo = $1', [codigo]); + if (aResult.rows.length === 0) return res.status(404).json({ error: 'Ámbito no encontrado' }); + const ambito = aResult.rows[0]; + + // Planes con hitos y nodo + const { rows: planes } = await pool.query(` + SELECT p.*, json_agg( + json_build_object( + 'id', h.id, + 'id_hito', h.id_hito, + 'titulo', h.titulo, + 'descripcion', h.descripcion, + 'estado', h.estado, + 'fecha', h.fecha, + 'horas_presencial', h.horas_presencial, + 'horas_remoto', h.horas_remoto, + 'nodo_nombre', n.nombre, + 'nodo_ip', n.ip + ) ORDER BY h.fecha NULLS LAST, h.id + ) FILTER (WHERE h.id IS NOT NULL) AS hitos + FROM bitacoras.planes p + LEFT JOIN bitacoras.hitos h ON h.plan_id = p.id + LEFT JOIN bitacoras.nodos n ON h.nodo_id = n.id + WHERE p.ambito_id = $1 + GROUP BY p.id + ORDER BY p.codigo + `, [ambito.id]); + + // Métricas globales + const { rows: [metricas] } = await pool.query(` + SELECT + COUNT(*) FILTER (WHERE h.estado = '✅') AS hitos_completados, + COUNT(*) FILTER (WHERE h.estado != '✅') AS hitos_pendientes, + COUNT(*) AS hitos_total, + COALESCE(SUM(h.horas_presencial), 0) AS total_presencial, + COALESCE(SUM(h.horas_remoto), 0) AS total_remoto, + COALESCE(SUM(h.horas_presencial + h.horas_remoto), 0) AS total_horas + FROM bitacoras.hitos h + JOIN bitacoras.planes p ON h.plan_id = p.id + WHERE p.ambito_id = $1 + `, [ambito.id]); + + // Nodos únicos involucrados + const { rows: nodos } = await pool.query(` + SELECT DISTINCT n.id, n.nombre, n.ip, n.tipo, n.descripcion + FROM bitacoras.hitos h + JOIN bitacoras.planes p ON h.plan_id = p.id + JOIN bitacoras.nodos n ON h.nodo_id = n.id + WHERE p.ambito_id = $1 + ORDER BY n.nombre + `, [ambito.id]); + + // Construir respuesta compatible con frontend existente + const response = { + id: ambito.id, + codigo: ambito.codigo, + nombre: ambito.nombre, + estado: ambito.activo ? '✅' : '⏳', + descripcion: ambito.descripcion || '', + detalles_json: null, + fases: planes.map(p => ({ + id: p.id, + numero: parseInt(p.codigo.split('.')[1] || '0'), + nombre: p.titulo, + estado: p.estado, + descripcion: p.descripcion || '', + fecha_inicio: p.fecha_inicio, + fecha_fin: p.fecha_fin, + hitos: p.hitos + })), + metricas, + nodos + }; + + res.json(response); + }); + return router; }; diff --git a/servicios/nginx/dtic-bitacoras/frontend/src/pages/ProyectoDashboard.tsx b/servicios/nginx/dtic-bitacoras/frontend/src/pages/ProyectoDashboard.tsx index ea29c3d5..ecad9f55 100644 --- a/servicios/nginx/dtic-bitacoras/frontend/src/pages/ProyectoDashboard.tsx +++ b/servicios/nginx/dtic-bitacoras/frontend/src/pages/ProyectoDashboard.tsx @@ -131,7 +131,7 @@ function DonutChart({ segments, size = 140, strokeWidth = 18, centerLabel, cente /* ═══════════ MAIN COMPONENT ═══════════ */ -export default function ProyectoDashboard({ codigo = 'P2601' }: { codigo?: string }) { +export default function ProyectoDashboard({ codigo = 'A04' }: { codigo?: string }) { const [proyecto, setProyecto] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -144,8 +144,9 @@ export default function ProyectoDashboard({ codigo = 'P2601' }: { codigo?: strin const fetchProyecto = async () => { setLoading(true); try { - const res = await fetch(`${API_URL}/proyectos/${codigo}`); - if (!res.ok) throw new Error('Proyecto no encontrado'); + // Usar endpoint de ambitos en lugar de proyectos + const res = await fetch(`${API_URL}/ambitos/${codigo}/proyecto`); + if (!res.ok) throw new Error('Ámbito no encontrado'); const data = await res.json(); setProyecto(data); setExpandedFases(new Set(data.fases.map((f: Fase) => f.id)));