feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia

This commit is contained in:
2026-09-18 23:04:48 -03:00
parent 47b50ede7e
commit 1080acd43a
240 changed files with 8354 additions and 8886 deletions
+384
View File
@@ -0,0 +1,384 @@
require('dotenv').config();
const express = require('express');
const nunjucks = require('nunjucks');
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
try {
require('nunjucks/src/jinja-compat').call(nunjucks);
} catch (e) {
console.warn('Jinja compat loader notice:', e.message);
}
const nunjucksRuntime = require('nunjucks/src/runtime');
const origMemberLookup = nunjucksRuntime.memberLookup;
nunjucksRuntime.memberLookup = function(obj, val) {
if (obj && val === 'get' && typeof obj.get !== 'function') {
return function(key, defaultVal) {
return (obj[key] !== undefined && obj[key] !== null) ? obj[key] : defaultVal;
};
}
const res = origMemberLookup(obj, val);
if (typeof res === 'function' && obj && typeof obj[val] === 'function') {
Object.assign(res, obj[val]);
}
return res;
};
const cookieParser = require('cookie-parser');
const path = require('path');
const axios = require('axios');
const app = express();
const port = process.env.PORT || 3000;
// Configuración de middlewares
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// Configuración de archivos estáticos
app.use(express.static(path.join(__dirname, '../public')));
app.use('/static', express.static(path.join(__dirname, '../public')));
// Configuración de Nunjucks
const env = nunjucks.configure(path.join(__dirname, '../views'), {
autoescape: true,
express: app,
watch: process.env.NODE_ENV !== 'production'
});
// Nunjucks custom filters/helpers
env.addFilter('get', function(obj, key, defaultVal) {
if (!obj) return defaultVal;
return obj[key] !== undefined ? obj[key] : defaultVal;
});
env.addFilter('min', function(arr) {
if (Array.isArray(arr)) return Math.min(...arr);
return arr;
});
env.addFilter('max', function(arr) {
if (Array.isArray(arr)) return Math.max(...arr);
return arr;
});
env.addFilter('round', function(val, precision = 0) {
if (val === undefined || val === null) return 0;
const factor = Math.pow(10, precision);
return Math.round(val * factor) / factor;
});
env.addFilter('format', function(str, ...args) {
if (!str) return '';
let i = 0;
return String(str).replace(/%0?(\d+)?d|%s/g, (match, pad) => {
let val = args[i++];
if (pad && typeof val === 'number') {
return String(val).padStart(parseInt(pad, 10), '0');
}
return val !== undefined ? String(val) : match;
});
});
const formatDateHelper = function(d, fmt) {
if (!d) return '';
if (typeof d === 'string') return d.split('T')[0];
return new Date(d).toISOString().split('T')[0];
};
const formatDateTimeHelper = function(d, fmt) {
if (!d) return '';
if (typeof d === 'string') return d.replace('T', ' ').substring(0, 16);
return new Date(d).toLocaleString();
};
env.addGlobal('format_date', formatDateHelper);
env.addGlobal('format_datetime', formatDateTimeHelper);
env.addFilter('format_date', formatDateHelper);
env.addFilter('format_datetime', formatDateTimeHelper);
env.addFilter('tojson', function(val) {
return JSON.stringify(val !== undefined ? val : null);
});
env.addFilter('sum', function(arr, attribute) {
if (!Array.isArray(arr)) return 0;
return arr.reduce((acc, item) => {
let val = 0;
if (attribute && typeof item === 'object' && item !== null) {
val = item[attribute] || 0;
} else {
val = Number(item) || 0;
}
return acc + Number(val);
}, 0);
});
const translateHelper = function(s) {
return s || '';
};
env.addGlobal('_', translateHelper);
env.addFilter('_', translateHelper);
app.set('view engine', 'html');
// Helper para extraer usuario de cookie si existe
app.use((req, res, next) => {
const token = req.cookies.auth_token;
let user = {
name: 'System',
first_name: 'System',
email: 'admin@edu-space.com',
role: 'admin',
is_authenticated: Boolean(token),
is_admin: () => true,
has_permission: () => true,
has_role: () => true,
can_edit_reservation: () => true,
can_delete_reservation: () => true,
get_institutional_role: () => 'admin',
role_badge_display: { class: 'bg-primary text-white', icon: 'bi-shield-check', label: 'Administrador' }
};
if (token) {
try {
const decoded = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString('utf8'));
const roleStr = (decoded.role || 'Admin').toLowerCase();
user = {
...user,
id: decoded.sub,
name: decoded.name || 'System',
first_name: (decoded.name || 'System').split(' ')[0],
email: decoded.email || 'admin@edu-space.com',
role: roleStr,
is_admin: () => roleStr === 'admin' || decoded.is_admin === true
};
} catch (e) {}
}
req.user = user;
res.locals.user = user;
res.locals.current_user = user;
res.locals.role = user.role;
res.locals.session = { get: (k, d) => d };
res.locals.occupancy_metrics = { get: (k, d) => d };
next();
});
// Rutas Principales
const indexRoutes = require('./routes/index');
const authRoutes = require('./routes/auth');
const dashboardRoutes = require('./routes/dashboard');
const classroomsRoutes = require('./routes/classrooms');
const scheduleRoutes = require('./routes/schedule');
const buildingsRoutes = require('./routes/buildings');
const adminRoutes = require('./routes/admin');
const mySubjectsRoutes = require('./routes/my_subjects');
app.use('/', indexRoutes);
app.use('/auth', authRoutes);
app.use('/dashboard', dashboardRoutes);
app.use('/classrooms', classroomsRoutes);
app.use('/schedule', scheduleRoutes);
app.use('/buildings', buildingsRoutes);
app.use('/admin', adminRoutes);
app.use('/my_subjects', mySubjectsRoutes);
app.use('/mis-materias', mySubjectsRoutes);
// Redirección de enlaces heredados de Jinja /main/dashboard preservando query params
app.get(['/main/dashboard', '/main'], (req, res) => {
const query = req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : '';
res.redirect('/dashboard' + query);
});
// Módulo Predictivo y Optimizador
app.get('/predictive', (req, res) => {
res.render('predictive/index', {
title: 'Dashboard Predictivo - Edu-Space Admin'
});
});
app.get(['/genetic_optimizer', '/genetic_algorithm_web/genetic_optimizer'], (req, res) => {
res.render('genetic_optimizer', {
title: 'Optimizador de Aulas (IA) - Edu-Space Admin'
});
});
// Proxy transparente de peticiones /api a Flask
app.use('/api', async (req, res) => {
// Si es dashboard-stats mock directo
if (req.path === '/dashboard-stats') {
return res.json({
monthly_data: [
{ month: 'Ene', reservations: 120 }, { month: 'Feb', reservations: 180 }, { month: 'Mar', reservations: 250 },
{ month: 'Abr', reservations: 220 }, { month: 'May', reservations: 300 }, { month: 'Jun', reservations: 310 },
{ month: 'Jul', reservations: 200 }, { month: 'Ago', reservations: 350 }, { month: 'Sep', reservations: 420 },
{ month: 'Oct', reservations: 480 }, { month: 'Nov', reservations: 520 }, { month: 'Dic', reservations: 210 }
]
});
}
try {
const token = req.cookies.auth_token;
const headers = { ...req.headers };
delete headers.host;
delete headers.connection;
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const flaskUrl = `http://127.0.0.1:5000/api${req.url}`;
const response = await axios({
method: req.method,
url: flaskUrl,
data: req.body,
headers,
validateStatus: () => true
});
res.status(response.status).json(response.data);
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// Wildcard route to catch unmigrated menu links and attempt to render the Nunjucks templates
app.get('/:blueprint/:route', (req, res) => {
const { blueprint, route } = req.params;
// Redirects for specific known route names
if (blueprint === 'main' && route === 'dashboard') {
return res.redirect('/dashboard');
}
if (blueprint === 'classrooms' && (route === 'list_classrooms' || route === 'list')) {
return res.redirect('/classrooms/list_classrooms');
}
if (blueprint === 'classrooms' && (route === 'add_classroom' || route === 'add')) {
return res.redirect('/classrooms/add_classroom');
}
if (blueprint === 'buildings' && (route === 'list_buildings' || route === 'list')) {
return res.redirect('/buildings/list_buildings');
}
if (blueprint === 'buildings' && (route === 'add_building' || route === 'add')) {
return res.redirect('/buildings/add_building');
}
if (blueprint === 'schedule' && (route === 'calendar_view' || route === 'calendar')) {
return res.redirect('/schedule/calendar_view');
}
if (blueprint === 'schedule' && (route === 'add_reservation' || route === 'add')) {
return res.redirect('/schedule/add_reservation');
}
if (blueprint === 'schedule' && (route === 'today_schedule' || route === 'today')) {
const query = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
return res.redirect('/schedule/today' + query);
}
if (blueprint === 'schedule' && (route === 'list_reservations' || route === 'list')) {
return res.redirect('/schedule/list_reservations');
}
if (blueprint === 'admin' && (route === 'google_sheets_import' || route === 'import')) {
return res.redirect('/admin/google_sheets_import');
}
if (blueprint === 'admin' && (route === 'occupancy_metrics' || route === 'occupancy-metrics')) {
return res.redirect('/admin/occupancy_metrics');
}
if (blueprint === 'admin' && (route === 'users_list' || route === 'user_list' || route === 'users')) {
return res.redirect('/admin/users_list');
}
if (blueprint === 'admin' && (route === 'user_add' || route === 'user_new')) {
return res.redirect('/admin/user_add');
}
if (blueprint === 'admin' && (route === 'user_edit' || route === 'user_edit/:id')) {
const id = req.query.id || '';
return res.redirect(id ? `/admin/user_edit/${id}` : '/admin/user_edit');
}
if (blueprint === 'admin' && (route === 'roles_list' || route === 'role_list' || route === 'roles')) {
return res.redirect('/admin/roles_list');
}
if (blueprint === 'admin' && (route === 'role_add' || route === 'role_new')) {
return res.redirect('/admin/role_add');
}
if (blueprint === 'admin' && (route === 'role_edit' || route === 'role_edit/:id')) {
const id = req.query.id || '';
return res.redirect(id ? `/admin/role_edit/${id}` : '/admin/role_edit');
}
if (blueprint === 'classrooms' && (route === 'view_classroom' || route === 'view')) {
return res.redirect('/classrooms/view_classroom');
}
if (blueprint === 'classrooms' && (route === 'edit_classroom' || route === 'edit')) {
const id = req.query.id || req.query.classroom_id || '';
return res.redirect(`/classrooms/edit/${id}`);
}
if (blueprint === 'admin' && route.includes('careers')) {
return res.redirect('/admin/careers_list');
}
if (blueprint === 'admin' && route.includes('subjects')) {
return res.redirect('/admin/subjects_list');
}
if (blueprint === 'admin' && route.includes('commissions')) {
return res.redirect('/admin/commissions_list');
}
if (blueprint === 'admin' && (route.includes('academic_term') || route.includes('academic_terms'))) {
return res.redirect('/admin/academic_terms_list');
}
if (blueprint === 'admin' && (route.includes('milestone_type') || route.includes('milestone_types'))) {
return res.redirect('/admin/milestone_types_list');
}
if (blueprint === 'admin' && route.includes('audit')) {
return res.redirect('/admin/audit_logs');
}
if (blueprint === 'genetic_algorithm_web' && route === 'genetic_optimizer') {
return res.redirect('/genetic_optimizer');
}
let templateName = `${blueprint}/${route}`;
if (route.endsWith('_list') || route.endsWith('_view')) {
templateName = `${blueprint}/list`;
if (blueprint === 'admin' && route.includes('users')) templateName = 'admin/users/list';
if (blueprint === 'admin' && route.includes('roles')) templateName = 'admin/roles/list';
if (blueprint === 'admin' && route.includes('academic_terms')) templateName = 'admin/academic_terms/list';
if (blueprint === 'admin' && route.includes('subjects')) templateName = 'admin/subjects/list';
if (blueprint === 'admin' && route.includes('careers')) templateName = 'admin/careers/list';
if (blueprint === 'admin' && route.includes('commissions')) templateName = 'admin/commissions/list';
if (blueprint === 'admin' && route.includes('milestone_types')) templateName = 'admin/milestone_types/list';
}
const mockContext = {
title: 'Vista de Administración',
role: 'admin',
session: { get: (k, d) => d },
occupancy_metrics: { get: (id, d) => d },
users: [],
roles: [],
careers: [],
subjects: [],
commissions: [],
academic_terms: [],
terms: [],
items: [],
audit_logs: []
};
res.render(templateName, mockContext, function(err, html) {
if (err) {
console.log(`Fallback for ${blueprint}/${route}, tried to render ${templateName}. Error:`, err.message);
res.send(`
<!DOCTYPE html><html><head><title>Vista en Migración</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>body { background: #121212; color: #fff; font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }</style>
</head><body><div class="text-center">
<h1 class="display-4 text-primary mb-3"><i class="bi bi-tools"></i> En Construcción</h1>
<p class="lead">La funcionalidad <strong>${blueprint}.${route}</strong> está siendo acoplada al nuevo BFF Node.js.</p>
<p class="text-muted">La migración de ORM Python a JSON REST está en curso para este módulo.</p>
<a href="/dashboard" class="btn btn-outline-light mt-4">Volver al Panel</a>
</div></body></html>
`);
} else {
res.send(html);
}
});
});
app.listen(port, () => {
console.log(`BFF Frontend corriendo en http://localhost:${port}`);
});