Files
admin-edu-space/frontend/src/routes/schedule.js
T

381 lines
15 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { requireAuth } = require('../middlewares/authMiddleware');
router.use(requireAuth);
// 1. Calendario General de Reservas
const handleCalendarView = async (req, res) => {
try {
const response = await req.apiClient.get('/classrooms');
const classrooms = (response.data && response.data.classrooms) || [];
const buildingsMap = new Map();
const floorsSet = new Set();
classrooms.forEach(c => {
if (c.building_id && c.building) {
buildingsMap.set(c.building_id, { id: c.building_id, name: c.building });
}
if (c.floor) {
floorsSet.add(c.floor);
}
});
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'
})),
subjects: [
{ id: 1, name: 'Arquitectura de Software', code: 'ARQ-101' },
{ id: 2, name: 'Bases de Datos II', code: 'BD-201' },
{ id: 3, name: 'Programación Avanzada', code: 'PRG-301' }
],
careers: [
{ id: 1, name: 'Licenciatura en Sistemas' },
{ id: 2, name: 'Tecnicatura Universitaria en Desarrollo de Software' }
],
buildings: Array.from(buildingsMap.values()),
available_floors: Array.from(floorsSet).sort(),
session: { get: (k, d) => d }
});
} catch (error) {
console.error('Error loading calendar view:', error.message);
res.render('schedule/calendar', {
title: 'Reservations Calendar (Offline)',
today_date: new Date().toISOString().split('T')[0],
classrooms: [],
subjects: [],
careers: [],
buildings: [],
available_floors: [],
session: { get: (k, d) => d }
});
}
};
router.get('/calendar', handleCalendarView);
router.get('/calendar_view', handleCalendarView);
// 2. Feed JSON de eventos para FullCalendar
const handleCalendarData = async (req, res) => {
try {
const { start, end, classroom_id } = req.query;
let queryParams = {};
if (start) queryParams.start_date = start.split('T')[0];
if (end) queryParams.end_date = end.split('T')[0];
if (classroom_id) queryParams.classroom_id = classroom_id;
const response = await req.apiClient.get('/reservations', { params: queryParams });
const reservations = (response.data && response.data.reservations) || [];
const events = reservations.map(r => {
const isVirtual = Boolean(r.virtual_link || (r.classroom_name && r.classroom_name.includes('VIRTUAL')));
let eventColor = '#B43E8E';
if (r.status === 'PENDING') eventColor = '#F59E0B';
else if (isVirtual) eventColor = '#6366F1';
return {
id: r.id,
title: `${r.purpose || 'Clase Regular'} - ${r.classroom_name || 'Aula'}`,
start: r.start_time,
end: r.end_time,
backgroundColor: eventColor,
borderColor: eventColor,
extendedProps: {
subject_name: 'Arquitectura de Software',
subject_code: 'ARQ-101',
classroom: r.classroom_name || 'Aula Principal',
is_virtual: isVirtual,
location_display: r.classroom_name || 'Edificio Central',
career: 'Licenciatura en Sistemas',
commission: 'COM-A',
shift: r.shift || 'Noche',
status: (r.status || 'CONFIRMED').toUpperCase(),
purpose: r.purpose || 'Clase Regular',
attendees: r.expected_attendees || 35,
teacher: r.teacher_name || 'Profesor Titular',
virtual_link: r.virtual_link || '',
cancel_url: `/schedule/cancel_reservation?id=${r.id}`
}
};
});
res.json(events);
} catch (error) {
console.error('Error fetching calendar data:', error.message);
res.json([]);
}
};
router.get('/calendar_data', handleCalendarData);
router.get('/calendar-data', handleCalendarData);
// 3. Vista de Lista de Reservas
const handleReservationsList = async (req, res) => {
try {
const response = await req.apiClient.get('/reservations');
const reservations = (response.data && response.data.reservations) || [];
res.render('schedule/list', {
title: 'Listado de Reservas - Edu-Space Admin',
reservations: {
items: reservations,
total: reservations.length,
pages: 1,
page: 1,
has_prev: false,
has_next: false,
iter_pages: () => [1]
},
classrooms: [],
subjects: [],
session: { get: (k, d) => d }
});
} catch (error) {
console.error('Error fetching reservations list:', error.message);
res.render('schedule/list', {
title: 'Listado de Reservas (Offline)',
reservations: { items: [], total: 0, pages: 1, page: 1, has_prev: false, has_next: false, iter_pages: () => [1] },
classrooms: [],
subjects: [],
session: { get: (k, d) => d }
});
}
};
router.get('/list', handleReservationsList);
router.get('/list_reservations', handleReservationsList);
// 4. Vista de Hoy (Cartelera del Día)
const handleTodaySchedule = async (req, res) => {
try {
const todayStr = req.query.date || '2026-09-18';
const hide_virtual = req.query.hide_virtual === '1' || req.query.hide_virtual === 'true';
const floor_filter = req.query.floor || '';
const shift_filter = req.query.shift || '';
const params = {
start_date: todayStr,
end_date: todayStr
};
if (hide_virtual) params.hide_virtual = '1';
if (floor_filter) params.floor = floor_filter;
if (shift_filter) params.shift = shift_filter;
const response = await req.apiClient.get('/reservations', { params });
let rawReservations = (response.data && response.data.reservations) || [];
// Format each reservation for the template
const reservations = rawReservations.map(r => {
const st = r.start_time.includes('T') ? r.start_time.split('T')[1].substring(0, 5) : r.start_time;
const et = r.end_time.includes('T') ? r.end_time.split('T')[1].substring(0, 5) : r.end_time;
const isVirt = Boolean(r.is_virtual || (r.classroom && r.classroom.is_virtual));
return {
...r,
start_time: st,
end_time: et,
subject_name: r.subject_name || r.purpose || 'Clase Regular',
subject_code: r.subject_code || '',
effective_virtual_link: r.effective_virtual_link || r.virtual_link || '',
is_virtual: isVirt,
classroom: r.classroom || { code: r.classroom_name || (isVirt ? 'VIRTUAL' : 'Sin Aula'), floor: isVirt ? 0 : 1, is_virtual: isVirt },
user: r.user || { name: r.teacher_name || 'Docente' }
};
});
// Build time_blocks for hours 7 to 21
const time_blocks = {};
for (let hour = 7; hour <= 21; hour++) {
time_blocks[hour] = [];
}
reservations.forEach(r => {
const startHour = parseInt(r.start_time.split(':')[0], 10) || 8;
const endHour = parseInt(r.end_time.split(':')[0], 10) || (startHour + 2);
for (let h = Math.max(7, startHour); h <= Math.min(21, endHour - 1); h++) {
time_blocks[h].push({
reservation: r,
classroom: r.classroom,
commission: r.commission,
user: r.user
});
}
});
// Compute stats
const confirmed = reservations.filter(r => r.status === 'CONFIRMED').length;
const pending = reservations.filter(r => r.status === 'PENDING').length;
const occupied_classrooms = new Set(reservations.map(r => r.classroom_id).filter(Boolean)).size;
const stats = {
total_reservations: reservations.length,
confirmed,
pending,
occupied_classrooms,
total_classrooms: 59
};
const available_floors = [0, 1, 2, 3, 4];
res.render('schedule/today', {
title: 'Cartelera del Día - Edu-Space Admin',
reservations,
time_blocks,
stats,
today: todayStr,
hide_virtual,
floor_filter,
shift_filter,
available_floors,
session: { get: (k, d) => d }
});
} catch (error) {
console.error('Error fetching today reservations:', error.message);
res.render('schedule/today', {
title: 'Cartelera del Día (Offline)',
reservations: [],
time_blocks: {},
stats: { total_reservations: 0, confirmed: 0, pending: 0, occupied_classrooms: 0, total_classrooms: 59 },
today: '2026-09-18',
hide_virtual: false,
floor_filter: '',
shift_filter: '',
available_floors: [0, 1, 2, 3, 4],
session: { get: (k, d) => d }
});
}
};
router.get('/today', handleTodaySchedule);
router.get('/today_schedule', handleTodaySchedule);
const { createFormMock } = require('../utils/formHelper');
// 5. Vista de Crear Reserva
const handleAddReservation = async (req, res) => {
try {
let allClassrooms = [];
try {
const response = await req.apiClient.get('/classrooms');
allClassrooms = (response.data && response.data.classrooms) || [];
} catch (e) {
console.warn('Could not fetch classrooms for reservation:', e.message);
}
const buildingsMap = new Map();
buildingsMap.set('1', { id: 1, name: 'Edificio Central', code: 'CENTRAL' });
buildingsMap.set('2', { id: 2, name: 'Campus Virtual', code: 'VIRTUAL' });
const classroomList = allClassrooms.map(c => {
const bId = c.building_id || (c.is_virtual ? 2 : 1);
const bName = c.building || (c.is_virtual ? 'Campus Virtual' : 'Edificio Central');
if (!buildingsMap.has(String(bId))) {
buildingsMap.set(String(bId), { id: bId, name: bName, code: c.is_virtual ? 'VIRTUAL' : 'CENTRAL' });
}
const flDisplay = c.floor_display || (c.floor === '0' || c.floor === 'PB' ? 'Planta Baja' : (c.floor ? `Piso ${c.floor}` : 'Campus Virtual'));
const cap = c.capacity || (c.is_virtual ? 0 : 35);
return {
id: c.id,
building_id: bId,
building_name: bName,
floor: c.floor || '',
floor_display: flDisplay,
code: c.code,
code_display: c.code,
location_display: c.location_display || `${bName} - ${flDisplay}`,
capacity: cap === 0 ? '∞' : String(cap),
capacity_display: cap === 0 ? '∞' : String(cap),
capacity_num: cap,
is_virtual: Boolean(c.is_virtual || (c.code && c.code.includes('VIRTUAL')))
};
});
const commissionList = [
{ id: 1, code: 'COM-ARQ-01', name: 'Arquitectura de Software - Comisión A', virtual_link: 'https://meet.google.com/edu-arq-01', max_students: 35 },
{ id: 2, code: 'COM-BD-01', name: 'Bases de Datos II - Comisión Única', virtual_link: 'https://meet.google.com/edu-bd-01', max_students: 40 },
{ id: 3, code: 'COM-PRG-01', name: 'Programación Avanzada - Comisión B', virtual_link: 'https://meet.google.com/edu-prg-01', max_students: 30 },
{ id: 4, code: 'COM-RED-01', name: 'Redes y Telecomunicaciones - Comisión A', virtual_link: '', max_students: 28 },
{ id: 5, code: 'COM-IA-01', name: 'Inteligencia Artificial - Comisión Tarde', virtual_link: 'https://meet.google.com/edu-ia-01', max_students: 32 }
];
const classroomOptions = [
{ value: '', text: '-- Selecciona Aula --' },
...classroomList.map(c => ({ value: c.id, text: `${c.code} · ${c.floor_display} (Cap: ${c.capacity_display})` }))
];
const commissionOptions = [
{ value: '', text: '-- Selecciona Comisión / Materia --' },
...commissionList.map(c => ({ value: c.id, text: `${c.code} · ${c.name} (${c.max_students} alum.)` }))
];
const initialClassroomId = req.query.classroom_id || '';
const now = new Date();
const startTimeDefault = new Date(now.getTime() + 3600000).toISOString().substring(0, 16);
const endTimeDefault = new Date(now.getTime() + 7200000).toISOString().substring(0, 16);
const form = createFormMock({
classroom_id: initialClassroomId,
start_time: startTimeDefault,
end_time: endTimeDefault,
expected_attendees: 30,
purpose: 'Clase Regular'
}, {
classroom_id: classroomOptions,
commission_id: commissionOptions
});
res.render('schedule/add', {
title: 'New Reservation - Edu-Space Admin',
form,
classroom_list: classroomList,
commission_list: commissionList,
buildings: Array.from(buildingsMap.values())
});
} catch (error) {
console.error('Error rendering add reservation:', error.message);
res.status(500).send('Error loading reservation form');
}
};
const handlePostReservation = async (req, res) => {
try {
const { classroom_id, commission_id, start_time, end_time, purpose, expected_attendees, notes, virtual_link } = req.body;
const payload = {
classroom_id: parseInt(classroom_id, 10),
commission_id: commission_id ? parseInt(commission_id, 10) : 1,
start_time: start_time ? new Date(start_time).toISOString() : new Date().toISOString(),
end_time: end_time ? new Date(end_time).toISOString() : new Date(Date.now() + 7200000).toISOString(),
purpose: purpose || 'Clase regular',
expected_attendees: parseInt(expected_attendees, 10) || 30,
notes: notes || '',
virtual_link: virtual_link || ''
};
try {
await req.apiClient.post('/reservations', payload);
} catch (apiErr) {
console.warn('API reservation creation notice:', apiErr.response?.data || apiErr.message);
}
res.redirect('/schedule/calendar_view');
} catch (error) {
console.error('Error saving reservation:', error.message);
res.redirect('/schedule/calendar_view');
}
};
router.get('/add', handleAddReservation);
router.get('/add_reservation', handleAddReservation);
router.post('/add', handlePostReservation);
router.post('/add_reservation', handlePostReservation);
module.exports = router;