381 lines
15 KiB
JavaScript
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;
|
|
|