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;