feat: completar fases 3 y 4 del roadmap mvp (gradebook, drag-and-drop, impersonacion) y actualizar README
This commit is contained in:
@@ -189,10 +189,27 @@ app.use((req, res, next) => {
|
||||
};
|
||||
} catch (e) {}
|
||||
}
|
||||
let isImpersonating = false;
|
||||
let impersonatedUser = null;
|
||||
if (user.is_admin() && req.cookies.impersonate_user_id) {
|
||||
isImpersonating = true;
|
||||
const targetId = req.cookies.impersonate_user_id;
|
||||
try {
|
||||
if (req.cookies.impersonate_user_data) {
|
||||
impersonatedUser = JSON.parse(req.cookies.impersonate_user_data);
|
||||
}
|
||||
} catch (e) {}
|
||||
if (!impersonatedUser) {
|
||||
impersonatedUser = { id: targetId, name: `Usuario #${targetId}`, role: 'usuario' };
|
||||
}
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
res.locals.user = user;
|
||||
res.locals.current_user = user;
|
||||
res.locals.role = user.role;
|
||||
res.locals.is_impersonating = isImpersonating;
|
||||
res.locals.impersonated_user = impersonatedUser;
|
||||
res.locals.session = { get: (k, d) => d };
|
||||
res.locals.occupancy_metrics = { get: (k, d) => d };
|
||||
next();
|
||||
@@ -259,6 +276,9 @@ app.use('/api', async (req, res) => {
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
if (req.cookies.impersonate_user_id) {
|
||||
headers['X-Impersonate-User'] = req.cookies.impersonate_user_id;
|
||||
}
|
||||
const flaskUrl = `http://127.0.0.1:5000/api${req.url}`;
|
||||
const response = await axios({
|
||||
method: req.method,
|
||||
|
||||
@@ -65,10 +65,38 @@ const requireAuth = (req, res, next) => {
|
||||
console.error('Error decoding token payload:', e.message);
|
||||
}
|
||||
|
||||
let isImpersonating = false;
|
||||
let impersonatedUser = null;
|
||||
|
||||
if (user.is_admin() && req.cookies.impersonate_user_id) {
|
||||
isImpersonating = true;
|
||||
const targetId = req.cookies.impersonate_user_id;
|
||||
req.apiClient.defaults.headers['X-Impersonate-User'] = targetId;
|
||||
|
||||
try {
|
||||
if (req.cookies.impersonate_user_data) {
|
||||
impersonatedUser = JSON.parse(req.cookies.impersonate_user_data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing impersonate_user_data:', e.message);
|
||||
}
|
||||
|
||||
if (!impersonatedUser) {
|
||||
impersonatedUser = {
|
||||
id: targetId,
|
||||
name: `Usuario #${targetId}`,
|
||||
email: 'impersonated@edu-space.com',
|
||||
role: 'usuario'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
res.locals.user = user;
|
||||
res.locals.current_user = user;
|
||||
res.locals.role = user.role;
|
||||
res.locals.is_impersonating = isImpersonating;
|
||||
res.locals.impersonated_user = impersonatedUser;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -38,9 +38,58 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const { requireAuth } = require('../middlewares/authMiddleware');
|
||||
|
||||
router.get('/logout', (req, res) => {
|
||||
res.clearCookie('auth_token');
|
||||
res.clearCookie('impersonate_user_id');
|
||||
res.clearCookie('impersonate_user_data');
|
||||
res.redirect('/auth/login');
|
||||
});
|
||||
|
||||
// Iniciar Modo Impersonación (Solo Admin)
|
||||
router.get('/impersonate/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const targetUserId = parseInt(req.params.id, 10);
|
||||
if (!req.user || !req.user.is_admin()) {
|
||||
return res.status(403).send('Solo los administradores pueden activar el modo de impersonación.');
|
||||
}
|
||||
|
||||
const response = await req.apiClient.post('/admin/impersonate', {
|
||||
user_id: targetUserId
|
||||
});
|
||||
|
||||
const targetUser = response.data && response.data.target_user;
|
||||
if (targetUser) {
|
||||
res.cookie('impersonate_user_id', targetUserId.toString(), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax'
|
||||
});
|
||||
res.cookie('impersonate_user_data', JSON.stringify(targetUser), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax'
|
||||
});
|
||||
}
|
||||
res.redirect('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Error initiating impersonation:', error.response?.data || error.message);
|
||||
res.redirect('/admin/users_list?error=impersonate_failed');
|
||||
}
|
||||
});
|
||||
|
||||
// Detener Modo Impersonación
|
||||
router.get('/stop-impersonating', requireAuth, async (req, res) => {
|
||||
try {
|
||||
await req.apiClient.post('/admin/stop-impersonating', {});
|
||||
} catch (error) {
|
||||
console.error('Error stopping impersonation on backend:', error.response?.data || error.message);
|
||||
} finally {
|
||||
res.clearCookie('impersonate_user_id');
|
||||
res.clearCookie('impersonate_user_data');
|
||||
res.redirect('/admin/users_list');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -176,6 +176,65 @@ router.get('/', async (req, res) => {
|
||||
};
|
||||
} else if (role === 'alumno') {
|
||||
const studentClasses = apiTodaySchedule ? apiTodaySchedule.slice(0, 4) : null;
|
||||
let upcomingMilestonesList = [];
|
||||
try {
|
||||
const mResp = await req.apiClient.get('/students/my-upcoming-milestones');
|
||||
if (mResp.data && Array.isArray(mResp.data.upcoming_milestones) && mResp.data.upcoming_milestones.length > 0) {
|
||||
upcomingMilestonesList = mResp.data.upcoming_milestones.map(m => {
|
||||
const isFinal = (m.type || '').includes('Final');
|
||||
const isParcial = (m.type || '').includes('Parcial');
|
||||
const color = isFinal ? '#dc3545' : (isParcial ? '#fd7e14' : '#0d6efd');
|
||||
return {
|
||||
...m,
|
||||
materia_codigo: m.commission_code || m.subject_name,
|
||||
type_obj: { name: m.type, color },
|
||||
milestone_type: { name: m.type, color }
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Student milestones fetch notice:', e.message);
|
||||
}
|
||||
|
||||
if (upcomingMilestonesList.length === 0) {
|
||||
upcomingMilestonesList = [
|
||||
{
|
||||
title: 'Primer Examen Parcial',
|
||||
materia_codigo: 'BDR-201',
|
||||
subject_name: 'Bases de Datos Relacionales',
|
||||
date: '25/09/2026',
|
||||
days_remaining: 2,
|
||||
type_obj: { name: 'Examen Parcial', color: '#dc3545' },
|
||||
milestone_type: { name: 'Examen Parcial', color: '#dc3545' },
|
||||
notes: 'Traer DNI y calculadora',
|
||||
classroom: 'Piso 2 · Aula 204'
|
||||
},
|
||||
{
|
||||
title: 'Entrega TP Grupal Nº 2',
|
||||
materia_codigo: 'AED-102',
|
||||
subject_name: 'Algoritmos y Estructuras de Datos',
|
||||
date: '30/09/2026',
|
||||
days_remaining: 7,
|
||||
type_obj: { name: 'Trabajo Práctico', color: '#198754' },
|
||||
milestone_type: { name: 'Trabajo Práctico', color: '#198754' },
|
||||
notes: 'Subir código al repositorio',
|
||||
classroom: 'Campus Virtual',
|
||||
link: 'https://campus.edu-space.com/tps'
|
||||
},
|
||||
{
|
||||
title: 'Recuperatorio Parcial 1',
|
||||
materia_codigo: 'RYT-302',
|
||||
subject_name: 'Redes y Telecomunicaciones',
|
||||
date: '08/10/2026',
|
||||
days_remaining: 15,
|
||||
type_obj: { name: 'Recuperatorio', color: '#fd7e14' },
|
||||
milestone_type: { name: 'Recuperatorio', color: '#fd7e14' },
|
||||
notes: 'Piso 2 · Aula 204',
|
||||
classroom: 'Piso 2 · Aula 204'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
role_data = {
|
||||
today_classes: studentClasses || [
|
||||
{
|
||||
@@ -197,42 +256,7 @@ router.get('/', async (req, res) => {
|
||||
virtual_link: 'https://meet.google.com/edu-space-demo'
|
||||
}
|
||||
],
|
||||
upcoming_milestones: apiMilestones ? apiMilestones.map(m => ({
|
||||
...m,
|
||||
milestone_type: m.type_obj
|
||||
})) : [
|
||||
{
|
||||
title: 'Primer Examen Parcial',
|
||||
materia_codigo: 'BDR-201',
|
||||
subject_name: 'Bases de Datos Relacionales',
|
||||
date: '25/09/2026',
|
||||
type_obj: { name: 'Examen Parcial', color: '#dc3545' },
|
||||
milestone_type: { name: 'Examen Parcial', color: '#dc3545' },
|
||||
notes: 'Traer DNI y calculadora',
|
||||
classroom: 'Piso 2 · Aula 204'
|
||||
},
|
||||
{
|
||||
title: 'Entrega TP Grupal Nº 2',
|
||||
materia_codigo: 'AED-102',
|
||||
subject_name: 'Algoritmos y Estructuras de Datos',
|
||||
date: '30/09/2026',
|
||||
type_obj: { name: 'Trabajo Práctico', color: '#198754' },
|
||||
milestone_type: { name: 'Trabajo Práctico', color: '#198754' },
|
||||
notes: 'Subir código al repositorio',
|
||||
classroom: 'Campus Virtual',
|
||||
link: 'https://campus.edu-space.com/tps'
|
||||
},
|
||||
{
|
||||
title: 'Recuperatorio Parcial 1',
|
||||
materia_codigo: 'RYT-302',
|
||||
subject_name: 'Redes y Telecomunicaciones',
|
||||
date: '08/10/2026',
|
||||
type_obj: { name: 'Recuperatorio', color: '#fd7e14' },
|
||||
milestone_type: { name: 'Recuperatorio', color: '#fd7e14' },
|
||||
notes: 'Piso 2 · Aula 204',
|
||||
classroom: 'Piso 2 · Aula 204'
|
||||
}
|
||||
]
|
||||
upcoming_milestones: upcomingMilestonesList
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -98,5 +98,7 @@ const handleMySubjectDetail = async (req, res) => {
|
||||
|
||||
router.get(['/', '/list', '/my_subjects_list'], handleMySubjectsList);
|
||||
router.get(['/detail', '/commission_detail', '/:id', '/detail/:id'], handleMySubjectDetail);
|
||||
router.get(['/gradebook/:id', '/:id/gradebook'], (req, res) => res.redirect(`/schedule/gradebook/${req.params.id}`));
|
||||
router.get(['/grades', '/my_grades', '/mis-notas'], (req, res) => res.redirect('/schedule/my_grades'));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -21,14 +21,24 @@ const handleCalendarView = async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
let unassignedCommissions = [];
|
||||
try {
|
||||
const commsRes = await req.apiClient.get('/genetic/commissions');
|
||||
unassignedCommissions = (commsRes.data && commsRes.data.commissions) || [];
|
||||
} catch (e) {
|
||||
console.warn('Could not fetch unassigned commissions for calendar:', e.message);
|
||||
}
|
||||
|
||||
res.render('schedule/calendar', {
|
||||
title: 'Reservations Calendar - Edu-Space Admin',
|
||||
today_date: new Date().toISOString().split('T')[0],
|
||||
classrooms: classrooms.map(c => ({
|
||||
id: c.id,
|
||||
code: c.code,
|
||||
building: c.building || c.building_name || 'Edificio Central'
|
||||
building: c.building || c.building_name || 'Edificio Central',
|
||||
capacity: c.capacity || 40
|
||||
})),
|
||||
unassigned_commissions: unassignedCommissions,
|
||||
subjects: [
|
||||
{ id: 1, name: 'Arquitectura de Software', code: 'ARQ-101' },
|
||||
{ id: 2, name: 'Bases de Datos II', code: 'BD-201' },
|
||||
@@ -48,6 +58,7 @@ const handleCalendarView = async (req, res) => {
|
||||
title: 'Reservations Calendar (Offline)',
|
||||
today_date: new Date().toISOString().split('T')[0],
|
||||
classrooms: [],
|
||||
unassigned_commissions: [],
|
||||
subjects: [],
|
||||
careers: [],
|
||||
buildings: [],
|
||||
@@ -581,6 +592,48 @@ router.post(['/edit_reservation', '/edit_reservation/:id', '/reservation/:id/edi
|
||||
router.post(['/confirm_reservation', '/confirm_reservation/:id', '/reservation/:id/confirm'], handleConfirmReservation);
|
||||
router.post(['/cancel_reservation', '/cancel_reservation/:id', '/reservation/:id/cancel'], handleCancelReservation);
|
||||
|
||||
// 9. Libro de Calificaciones / Gradebook (Fase 3)
|
||||
const handleGradebook = async (req, res) => {
|
||||
const id = req.params.id || req.query.id;
|
||||
try {
|
||||
const resp = await req.apiClient.get(`/commissions/${id}/gradebook`);
|
||||
const data = resp.data || {};
|
||||
res.render('schedule/gradebook', {
|
||||
title: `Libro de Calificaciones — Edu-Space`,
|
||||
commission: data.commission || { id, code: 'C1' },
|
||||
milestone_columns: data.milestone_columns || [],
|
||||
students: data.students || [],
|
||||
role: req.user?.role || 'docente'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching commission gradebook:', err.message);
|
||||
res.redirect(`/admin/commission_detail?id=${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 10. Consulta de Calificaciones del Alumno (Fase 3)
|
||||
const handleMyGrades = async (req, res) => {
|
||||
try {
|
||||
const resp = await req.apiClient.get('/students/my-grades');
|
||||
const subjects = resp.data?.subjects || [];
|
||||
res.render('schedule/my_grades', {
|
||||
title: 'Mis Calificaciones — Edu-Space',
|
||||
subjects,
|
||||
role: req.user?.role || 'alumno'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching student grades:', err.message);
|
||||
res.render('schedule/my_grades', {
|
||||
title: 'Mis Calificaciones — Edu-Space',
|
||||
subjects: [],
|
||||
role: req.user?.role || 'alumno'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
router.get(['/gradebook', '/gradebook/:id', '/commissions/:id/gradebook'], handleGradebook);
|
||||
router.get(['/my_grades', '/my-grades', '/mis_calificaciones', '/mis-notas'], handleMyGrades);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user