feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia
This commit is contained in:
@@ -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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user