[Dashboard] Backend/Frontend actualizados — Endpoint Ámbitos con Planes
BACKEND (ambitos.js):
- Nuevo endpoint GET /api/ambitos/:codigo/proyecto
- Devuelve estructura compatible con frontend existente
- Planes con hitos, métricas y nodos (igual que proyectos)
FRONTEND (ProyectoDashboard.tsx):
- Cambiado default de 'P2601' a 'A04'
- Fetch desde /ambitos/${codigo}/proyecto en lugar de /proyectos
- Mantiene compatibilidad con interfaz existente
DATOS ACTUALIZADOS:
- 29 hitos totales
- 21 completados ✅
- 8 pendientes/legacy ⏳🛑
- 6 planes: A04.P001 a A04.P006
- 60 horas totales (14.5h presencial + 45.5h remoto)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
cafa07303b
commit
5b384a1b37
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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<Proyecto | null>(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)));
|
||||
|
||||
Reference in New Issue
Block a user