[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
@@ -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;
|
return router;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ function DonutChart({ segments, size = 140, strokeWidth = 18, centerLabel, cente
|
|||||||
|
|
||||||
|
|
||||||
/* ═══════════ MAIN COMPONENT ═══════════ */
|
/* ═══════════ 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 [proyecto, setProyecto] = useState<Proyecto | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -144,8 +144,9 @@ export default function ProyectoDashboard({ codigo = 'P2601' }: { codigo?: strin
|
|||||||
const fetchProyecto = async () => {
|
const fetchProyecto = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/proyectos/${codigo}`);
|
// Usar endpoint de ambitos en lugar de proyectos
|
||||||
if (!res.ok) throw new Error('Proyecto no encontrado');
|
const res = await fetch(`${API_URL}/ambitos/${codigo}/proyecto`);
|
||||||
|
if (!res.ok) throw new Error('Ámbito no encontrado');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setProyecto(data);
|
setProyecto(data);
|
||||||
setExpandedFases(new Set(data.fases.map((f: Fase) => f.id)));
|
setExpandedFases(new Set(data.fases.map((f: Fase) => f.id)));
|
||||||
|
|||||||
Reference in New Issue
Block a user