diff --git a/README.md b/README.md index e515533..c882034 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,16 @@ El sistema incluye sincronización automatizada con las planillas oficiales de c --- +## 📅 Ficha de Reserva & Gestión de Comisiones + +* **Ficha de Reserva Detallada (`/schedule/view_reservation?id={id}`):** Información completa del espacio físico o virtual asignado, capacidad y asistencia prevista, comisión académica vinculada, docente a cargo, botones de acción inmediata (*Modificar Reserva*, *Confirmar Reserva*, *Cancelar Reserva*) y acceso con un clic a la sala de videollamada. +* **Gestión de Comisiones y Vinculación con Asignaturas (`/admin/commissions_list`):** + - Botón y modal interactivo **Nueva Comisión** (`#modalAddCommission`) para registrar comisiones y vincularlas directamente a su **Asignatura / Materia**, indicando cuatrimestre, año, turno, cupo y docente responsable. + - Generación automática de enlace institucional de Google Meet cuando no se especifica un enlace propio. + - Tabla operativa con visualización explícita del código de comisión, nombre de materia, código de asignatura y carrera. + +--- + ## 🚀 Instalación y Despliegue Local ### Requisitos previos diff --git a/backend/app/routes/api/admin.py b/backend/app/routes/api/admin.py index 6b4bf16..a8abc0a 100644 --- a/backend/app/routes/api/admin.py +++ b/backend/app/routes/api/admin.py @@ -1,5 +1,6 @@ from flask import Blueprint, jsonify, request from datetime import datetime +import re from app.utils.jwt_decorators import jwt_required from app.models.user import User from app.models.role import Role, Permission, SYSTEM_MODULES @@ -196,12 +197,22 @@ def get_commissions(): if search not in s_name and search not in c_code: continue + career_name = 'Carrera General' + if c.subject and hasattr(c.subject, 'career') and c.subject.career: + career_name = c.subject.career.name + commissions_data.append({ 'id': c.id, 'code': c.code, + 'full_code': c.get_full_code() if hasattr(c, 'get_full_code') else c.code, 'subject_id': c.subject_id, 'subject_name': c.subject.name if c.subject else 'Sin materia', 'subject_code': c.subject.code if c.subject else '-', + 'career_name': career_name, + 'semester': getattr(c, 'semester', '1C') or '1C', + 'year': getattr(c, 'year', 2026) or 2026, + 'teacher_id': c.teacher_id, + 'teacher_name': c.teacher_name if hasattr(c, 'teacher_name') else (c.teacher.name if c.teacher else None), 'schedule': c.schedule or 'A coordinar', 'shift': c.shift or 'Mañana', 'max_students': c.max_students or 35, @@ -210,7 +221,16 @@ def get_commissions(): 'virtual_link': getattr(c, 'virtual_link', '') or '' }) - subjects_data = [{'id': s.id, 'name': s.name, 'code': s.code} for s in subjects] + subjects_data = [] + for s in subjects: + c_name = s.career.name if (hasattr(s, 'career') and s.career) else 'Carrera General' + subjects_data.append({ + 'id': s.id, + 'name': s.name, + 'code': s.code, + 'career': c_name, + 'career_name': c_name + }) return jsonify({ 'status': 'success', @@ -599,14 +619,14 @@ def create_commission(): data = request.get_json(silent=True) or request.form.to_dict() or {} subject_id = data.get('subject_id') code = data.get('code', '').strip() - semester = data.get('semester', '1C').strip() + semester = (data.get('semester') or '1C').strip() year = int(data.get('year', datetime.now().year) or datetime.now().year) teacher_id = data.get('teacher_id') - teacher_id = int(teacher_id) if teacher_id else None + teacher_id = int(teacher_id) if (teacher_id and str(teacher_id).strip()) else None max_students = int(data.get('max_students', 35) or 35) - schedule = data.get('schedule', '').strip() - shift = data.get('shift', 'Mañana').strip() - virtual_link = data.get('virtual_link', '').strip() + schedule = (data.get('schedule') or '').strip() + shift = (data.get('shift') or 'Mañana').strip() + virtual_link = (data.get('virtual_link') or '').strip() active = data.get('active', True) if isinstance(active, str): active = active.lower() in ['true', '1', 'on'] @@ -614,6 +634,38 @@ def create_commission(): if not subject_id or not code: return jsonify({'error': 'ValidationError', 'message': 'La asignatura y código de comisión son obligatorios.'}), 400 + subject = Subject.query.get(int(subject_id)) + if not subject: + return jsonify({'error': 'NotFound', 'message': f'La asignatura con ID {subject_id} no existe.'}), 404 + + # Generar enlace automático si no fue provisto + if not virtual_link: + s_clean = re.sub(r'[^a-zA-Z0-9]', '', subject.code).lower()[:6] or 'subj' + c_clean = re.sub(r'[^a-zA-Z0-9]', '', code).lower()[:4] or 'c1' + virtual_link = f"https://meet.google.com/edu-{s_clean}-{c_clean}" + + existing = Commission.query.filter_by( + subject_id=int(subject_id), + code=code, + semester=semester, + year=year + ).first() + + if existing: + existing.teacher_id = teacher_id + existing.max_students = max_students + existing.schedule = schedule + existing.shift = shift + if virtual_link: + existing.virtual_link = virtual_link + existing.active = bool(active) + db.session.commit() + return jsonify({ + 'status': 'success', + 'message': f'Comisión {code} asociada a {subject.name} actualizada exitosamente.', + 'commission': existing.to_dict() + }), 200 + commission = Commission( subject_id=int(subject_id), code=code, @@ -631,10 +683,11 @@ def create_commission(): return jsonify({ 'status': 'success', - 'message': 'Comisión creada exitosamente.', + 'message': f'Comisión {code} asociada exitosamente a {subject.name}.', 'commission': commission.to_dict() }), 201 + @api_admin_bp.route('/commissions/', methods=['PUT']) @jwt_required def update_commission(id): diff --git a/backend/app/routes/api/reservations.py b/backend/app/routes/api/reservations.py index 3323f02..63e782c 100644 --- a/backend/app/routes/api/reservations.py +++ b/backend/app/routes/api/reservations.py @@ -103,27 +103,66 @@ def list_reservations(): @api_reservations_bp.route('/', methods=['GET']) @jwt_required def get_reservation(reservation_id: int): - """Obtiene el detalle de una reserva específica.""" + """Obtiene el detalle de una reserva específica con sus relaciones completas.""" r = reservation_service.get_reservation(reservation_id) if not r: return jsonify({'error': 'NotFound', 'message': f'Reserva {reservation_id} no encontrada.'}), 404 + duration_min = int((r.end_time - r.start_time).total_seconds() / 60) if (r.start_time and r.end_time) else 120 + is_virt = bool(r.is_virtual or (r.classroom and r.classroom.is_virtual)) + return jsonify({ 'id': r.id, 'classroom_id': r.classroom_id, 'classroom_name': r.classroom.code if r.classroom else None, - 'location_display': r.classroom.location_display if r.classroom else None, + 'location_display': getattr(r.classroom, 'location_display', r.classroom.code if r.classroom else None), + 'classroom': { + 'id': r.classroom.id, + 'code': r.classroom.code, + 'code_display': getattr(r.classroom, 'code_display', r.classroom.code), + 'building_name': getattr(r.classroom, 'building_name', getattr(r.classroom, 'building', 'Edificio Central')), + 'floor': r.classroom.floor, + 'floor_display': getattr(r.classroom, 'floor_display', f"Piso {r.classroom.floor}"), + 'capacity': r.classroom.capacity, + 'capacity_display': getattr(r.classroom, 'capacity_display', str(r.classroom.capacity)), + 'description': getattr(r.classroom, 'description', '') or '', + 'is_virtual': getattr(r.classroom, 'is_virtual', False) + } if r.classroom else None, 'commission_id': r.commission_id, + 'commission': { + 'id': r.commission.id, + 'code': r.commission.code, + 'full_code': r.commission.get_full_code() if hasattr(r.commission, 'get_full_code') else r.commission.code, + 'virtual_link': r.commission.virtual_link, + 'max_students': r.commission.max_students, + 'current_students': getattr(r.commission, 'current_students', 0), + 'subject': { + 'id': r.commission.subject.id, + 'name': r.commission.subject.name, + 'code': r.commission.subject.code + } if (r.commission and r.commission.subject) else None + } if r.commission else None, 'teacher_id': r.user_id, 'teacher_name': r.user.name if r.user else None, + 'user': { + 'id': r.user.id, + 'name': r.user.name, + 'first_name': getattr(r.user, 'first_name', r.user.name.split()[0] if r.user.name else 'Usuario'), + 'last_name': getattr(r.user, 'last_name', ' '.join(r.user.name.split()[1:]) if r.user.name and len(r.user.name.split()) > 1 else ''), + 'email': r.user.email + } if r.user else None, 'start_time': r.start_time.isoformat(), 'end_time': r.end_time.isoformat(), + 'duration': duration_min, 'purpose': r.purpose, - 'status': str(r.status), - 'shift': r.shift, + 'status': str(r.status.value if hasattr(r.status, 'value') else r.status), + 'shift': r.shift or 'Mañana', 'virtual_link': r.virtual_link, - 'expected_attendees': r.expected_attendees, - 'notes': r.notes + 'effective_virtual_link': r.effective_virtual_link, + 'is_virtual': is_virt, + 'expected_attendees': r.expected_attendees or 30, + 'notes': r.notes or '', + 'can_be_cancelled': r.can_be_cancelled() if hasattr(r, 'can_be_cancelled') else True }), 200 @api_reservations_bp.route('', methods=['POST']) @@ -185,3 +224,24 @@ def cancel_reservation(reservation_id: int): return jsonify({'error': 'NotFound', 'message': f'Reserva {reservation_id} no encontrada.'}), 404 return jsonify({'message': f'Reserva {reservation_id} cancelada correctamente.'}), 200 + +@api_reservations_bp.route('//cancel', methods=['POST']) +@jwt_required +def cancel_reservation_post(reservation_id: int): + """Cancela una reserva (endpoint POST compatible con formularios).""" + cancelled = reservation_service.cancel_reservation(reservation_id) + if not cancelled: + return jsonify({'error': 'NotFound', 'message': f'Reserva {reservation_id} no encontrada.'}), 404 + + return jsonify({'message': f'Reserva {reservation_id} cancelada correctamente.'}), 200 + +@api_reservations_bp.route('//confirm', methods=['POST']) +@jwt_required +def confirm_reservation(reservation_id: int): + """Confirma una reserva cambiando su estado a CONFIRMED.""" + confirmed = reservation_service.confirm_reservation(reservation_id) + if not confirmed: + return jsonify({'error': 'NotFound', 'message': f'Reserva {reservation_id} no encontrada.'}), 404 + + return jsonify({'message': f'Reserva {reservation_id} confirmada correctamente.'}), 200 + diff --git a/backend/app/services/reservation_service.py b/backend/app/services/reservation_service.py index 8a0009c..f5a79a0 100644 --- a/backend/app/services/reservation_service.py +++ b/backend/app/services/reservation_service.py @@ -120,3 +120,12 @@ class ReservationService: reservation.status = ReservationStatus.CANCELLED.value self.reservation_repo.save(reservation) return True + + def confirm_reservation(self, reservation_id: int) -> bool: + """Confirma una reserva estableciendo su estado en CONFIRMED.""" + reservation = self.reservation_repo.get_by_id(reservation_id) + if not reservation: + return False + reservation.status = ReservationStatus.CONFIRMED.value + self.reservation_repo.save(reservation) + return True diff --git a/docs/CHANGELOG_VIRTUAL_CLASSES_AND_DASHBOARDS.md b/docs/CHANGELOG_VIRTUAL_CLASSES_AND_DASHBOARDS.md index d36b500..50056e4 100644 --- a/docs/CHANGELOG_VIRTUAL_CLASSES_AND_DASHBOARDS.md +++ b/docs/CHANGELOG_VIRTUAL_CLASSES_AND_DASHBOARDS.md @@ -72,3 +72,29 @@ Se ejecutó la suite de verificación automatizada: - 643 eventos en calendario (388 virtuales identificados con `#6366F1` y enlaces activos). 4. **Vistas Principales**: - 11/11 rutas pasaron con código 200 sin errores de plantilla ni excepciones de servidor. + +--- + +## 4. Acople de `schedule.view_reservation` al BFF Node.js y Gestión de Comisiones + +### A. Vista Detallada de Reserva (`schedule.view_reservation`) +- **Backend API (`/api/v1/reservations/`)**: + - Enriquecimiento del payload serializado para incluir objetos relacionales completos: `classroom` (código, edificio, piso, capacidad, descripción, modalidad virtual), `commission` (código, código compuesto, materia, cuatrimestre, año, cupos), `user` (docente, nombre, apellido, email), `duration`, `effective_virtual_link` y `can_be_cancelled`. + - Incorporación de endpoints de acción directa `POST /api/v1/reservations//confirm` y `POST /api/v1/reservations//cancel`. +- **Node.js BFF (`frontend/src/routes/schedule.js`)**: + - Implementación de `handleViewReservation` con soporte para `/view_reservation`, `/view_reservation/:id`, `/view/:id`, y `/reservation/:id/view`. + - Implementación de `handleEditReservation` (GET & POST), `handleConfirmReservation` y `handleCancelReservation`. + - Actualización de plantillas (`view.html`, `today.html`, `list.html`, `dashboard.html`) para enlazar directamente las fichas pasando el ID de la reserva. + +### B. Creación de Comisiones y Asociación con Materias (`/admin/commissions_list`) +- **Modal y Acción en Interfaz**: + - Incorporación de botón destacado `+ Nueva Comisión` en el encabezado y en el estado vacío de la tabla. + - Implementación de modal interactivo `#modalAddCommission`: + - Selector obligatorio de **Asignatura / Materia Asociada** (`subject_id`), mostrando código, nombre y carrera. + - Campos de código de comisión, cuatrimestre, año lectivo, turno, docente a cargo, cupo máximo y días/horarios. + - Generación y configuración de enlace a aula virtual (Google Meet institucional). + - Alerta de confirmación visual (`?created=1`) al registrar y vincular la comisión exitosamente. +- **Backend API (`POST /api/v1/admin/commissions`)**: + - Validación de existencia de la asignatura vinculada y resolución inteligente de duplicados (`subject_id`, `code`, `semester`, `year`). + - Auto-generación de enlaces seguros de Google Meet en caso de no especificarse enlace personalizado. + diff --git a/frontend/src/routes/admin.js b/frontend/src/routes/admin.js index 3805c25..1551940 100644 --- a/frontend/src/routes/admin.js +++ b/frontend/src/routes/admin.js @@ -623,20 +623,39 @@ const handleCommissionsList = async (req, res) => { const subject_id = req.query.subject_id || ''; const shift = req.query.shift || ''; const active = req.query.active || ''; + const created = req.query.created === '1'; let commissions = []; let subjects = []; + let teachers = []; try { - const cRes = await req.apiClient.get('/commissions', { params: { search, subject_id, shift, active } }); - if (cRes.data) { - commissions = (cRes.data.commissions || []).map(c => ({ + const [cRes, uRes] = await Promise.allSettled([ + req.apiClient.get('/commissions', { params: { search, subject_id, shift, active } }), + req.apiClient.get('/users') + ]); + + if (cRes.status === 'fulfilled' && cRes.value.data) { + commissions = (cRes.value.data.commissions || []).map(c => ({ ...c, - get_full_code: () => c.code, - subject: { name: c.subject_name, code: c.subject_code }, + get_full_code: () => c.full_code || c.code, + subject: { + id: c.subject_id, + name: c.subject_name, + code: c.subject_code, + career_name: c.career_name || 'Carrera General' + }, teacher: c.teacher_name ? { name: c.teacher_name } : null })); - subjects = cRes.data.subjects || []; + subjects = cRes.value.data.subjects || []; + } + + if (uRes.status === 'fulfilled' && uRes.value.data) { + const users = uRes.value.data.users || []; + teachers = users.filter(u => { + const r = (u.role || '').toLowerCase(); + return r.includes('docent') || r.includes('prof') || r.includes('admin') || r.includes('bedel'); + }); } } catch (apiErr) { console.warn('Commissions list API notice:', apiErr.message); @@ -646,10 +665,12 @@ const handleCommissionsList = async (req, res) => { title: 'Gestión de Comisiones — Edu-Space', commissions, subjects, + teachers, search, subject_filter: subject_id ? parseInt(subject_id, 10) : '', shift_filter: shift, active_filter: active, + created, pagination: { pages: 1, page: 1, total: commissions.length, has_prev: false, has_next: false } }); } catch (err) { @@ -658,6 +679,7 @@ const handleCommissionsList = async (req, res) => { } }; + const handleCommissionDetail = async (req, res) => { const id = req.params.id || req.query.id || 1; try { @@ -725,11 +747,25 @@ const handleCommissionDetail = async (req, res) => { const handleCommissionAdd = async (req, res) => { try { - await req.apiClient.post('/commissions', req.body); + const { subject_id, code, semester, year, shift, teacher_id, max_students, schedule, virtual_link } = req.body; + const payload = { + subject_id: parseInt(subject_id, 10), + code: (code || '').trim(), + semester: semester || '1C', + year: year ? parseInt(year, 10) : 2026, + shift: shift || 'Mañana', + teacher_id: teacher_id ? parseInt(teacher_id, 10) : null, + max_students: max_students ? parseInt(max_students, 10) : 40, + schedule: (schedule || '').trim(), + virtual_link: (virtual_link || '').trim(), + active: true + }; + await req.apiClient.post('/commissions', payload); + res.redirect('/admin/commissions_list?created=1'); } catch (e) { console.warn('Commission add API notice:', e.response?.data || e.message); + res.redirect('/admin/commissions_list'); } - res.redirect('/admin/commissions_list'); }; const handleCommissionEdit = async (req, res) => { diff --git a/frontend/src/routes/schedule.js b/frontend/src/routes/schedule.js index 2871e8e..fcb7b50 100644 --- a/frontend/src/routes/schedule.js +++ b/frontend/src/routes/schedule.js @@ -376,5 +376,211 @@ router.get('/add_reservation', handleAddReservation); router.post('/add', handlePostReservation); router.post('/add_reservation', handlePostReservation); +// 6. Vista Detallada de Reserva (Ficha de Reserva) +const handleViewReservation = async (req, res) => { + try { + let id = req.params.id || req.query.id; + if (!id) { + try { + const listRes = await req.apiClient.get('/reservations'); + const items = (listRes.data && listRes.data.reservations) || []; + if (items.length > 0) { + return res.redirect(`/schedule/view_reservation?id=${items[0].id}`); + } + } catch (e) { + console.warn('Could not fetch default reservation:', e.message); + } + return res.redirect('/schedule/today_schedule'); + } + + const response = await req.apiClient.get(`/reservations/${id}`); + const r = response.data; + + const resObj = { + ...r, + status: { + value: (r.status || 'CONFIRMED').toUpperCase(), + toString: () => (r.status || 'CONFIRMED').toUpperCase() + }, + duration: r.duration || 120, + classroom: r.classroom || { + code: r.classroom_name || 'Aula Asignada', + building_name: 'Edificio Central', + floor_display: 'PB', + capacity: 35, + capacity_display: '35', + is_virtual: Boolean(r.is_virtual), + description: '' + }, + commission: r.commission ? { + ...r.commission, + get_full_code: () => r.commission.full_code || r.commission.code || `COM-${r.commission.id}`, + subject: r.commission.subject || { name: r.purpose || 'Materia General' } + } : { + get_full_code: () => 'COM-GRAL', + subject: { name: r.purpose || 'Materia General' } + }, + user: r.user || { + first_name: (r.teacher_name || 'Profesor').split(' ')[0], + last_name: (r.teacher_name || '').split(' ').slice(1).join(' ') || 'Docente', + email: 'docente@edu-space.com' + }, + effective_virtual_link: r.effective_virtual_link || r.virtual_link || '', + can_be_cancelled: () => (typeof r.can_be_cancelled === 'boolean' ? r.can_be_cancelled : true) + }; + + res.render('schedule/view', { + title: `Reservation Details #${r.id} - Edu-Space Admin`, + reservation: resObj + }); + } catch (error) { + console.error('Error in view_reservation:', error.message); + res.redirect('/schedule/today_schedule'); + } +}; + +// 7. Modificar Reserva +const handleEditReservation = async (req, res) => { + const id = req.params.id || req.query.id; + if (!id) return res.redirect('/schedule/today_schedule'); + + try { + let reservation = null; + try { + const rRes = await req.apiClient.get(`/reservations/${id}`); + reservation = rRes.data; + } catch (err) { + console.error('Error fetching reservation for edit:', err.message); + return res.redirect('/schedule/today_schedule'); + } + + let allClassrooms = []; + try { + const cRes = await req.apiClient.get('/classrooms'); + allClassrooms = (cRes.data && cRes.data.classrooms) || []; + } catch (e) {} + + 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 = classroomList.map(c => ({ value: c.id, text: `${c.code} · ${c.floor_display} (Cap: ${c.capacity_display})` })); + const commissionOptions = commissionList.map(c => ({ value: c.id, text: `${c.code} · ${c.name} (${c.max_students} alum.)` })); + + const st = reservation.start_time ? reservation.start_time.substring(0, 16) : ''; + const et = reservation.end_time ? reservation.end_time.substring(0, 16) : ''; + + const form = createFormMock({ + classroom_id: reservation.classroom_id || '', + commission_id: reservation.commission_id || '', + start_time: st, + end_time: et, + expected_attendees: reservation.expected_attendees || 30, + purpose: reservation.purpose || 'Clase Regular', + notes: reservation.notes || '', + virtual_link: reservation.virtual_link || '' + }, { + classroom_id: classroomOptions, + commission_id: commissionOptions + }); + + res.render('schedule/edit', { + title: `Modificar Reserva #${id} - Edu-Space Admin`, + reservation, + form, + classroom_list: classroomList, + commission_list: commissionList, + buildings: Array.from(buildingsMap.values()) + }); + } catch (error) { + console.error('Error rendering edit reservation:', error.message); + res.redirect(`/schedule/view_reservation?id=${id}`); + } +}; + +const handlePostEditReservation = async (req, res) => { + const id = req.params.id || req.query.id || req.body.id; + try { + const { classroom_id, commission_id, start_time, end_time, purpose, expected_attendees, notes, virtual_link } = req.body; + const payload = {}; + if (classroom_id) payload.classroom_id = parseInt(classroom_id, 10); + if (commission_id) payload.commission_id = parseInt(commission_id, 10); + if (start_time) payload.start_time = new Date(start_time).toISOString(); + if (end_time) payload.end_time = new Date(end_time).toISOString(); + if (purpose) payload.purpose = purpose; + if (expected_attendees) payload.expected_attendees = parseInt(expected_attendees, 10); + if (notes !== undefined) payload.notes = notes; + if (virtual_link !== undefined) payload.virtual_link = virtual_link; + + await req.apiClient.put(`/reservations/${id}`, payload); + res.redirect(`/schedule/view_reservation?id=${id}`); + } catch (e) { + console.warn('Update reservation notice:', e.response?.data || e.message); + res.redirect(`/schedule/view_reservation?id=${id}`); + } +}; + +// 8. Confirmar y Cancelar Acciones +const handleConfirmReservation = async (req, res) => { + const id = req.params.id || req.body.id || req.query.id; + try { + await req.apiClient.post(`/reservations/${id}/confirm`); + } catch (e) { + console.warn('Confirm reservation API notice:', e.response?.data || e.message); + } + res.redirect(`/schedule/view_reservation?id=${id}`); +}; + +const handleCancelReservation = async (req, res) => { + const id = req.params.id || req.body.id || req.query.id; + try { + await req.apiClient.post(`/reservations/${id}/cancel`); + } catch (e) { + console.warn('Cancel reservation API notice:', e.response?.data || e.message); + } + res.redirect(`/schedule/view_reservation?id=${id}`); +}; + +router.get(['/view_reservation', '/view_reservation/:id', '/view/:id', '/reservation/:id/view'], handleViewReservation); + +router.get(['/edit_reservation', '/edit_reservation/:id', '/reservation/:id/edit'], handleEditReservation); +router.post(['/edit_reservation', '/edit_reservation/:id', '/reservation/:id/edit'], handlePostEditReservation); + +router.post(['/confirm_reservation', '/confirm_reservation/:id', '/reservation/:id/confirm'], handleConfirmReservation); +router.post(['/cancel_reservation', '/cancel_reservation/:id', '/reservation/:id/cancel'], handleCancelReservation); + module.exports = router; + diff --git a/frontend/views/admin/commissions/list.html b/frontend/views/admin/commissions/list.html index 12883f7..4d30d4c 100644 --- a/frontend/views/admin/commissions/list.html +++ b/frontend/views/admin/commissions/list.html @@ -9,15 +9,29 @@

Gestión de Comisiones

-

Asignación de docentes e inscripción de alumnos por comisión

+

Asignación de asignaturas, docentes e inscripción de alumnos por comisión

+ +
+ + + Ver Asignaturas +
- - Ver Asignaturas - - - + + {% if created %} + + {% endif %} +
@@ -104,8 +118,12 @@ -
{{ comm.subject.name }}
- {{ comm.subject.code }} — {{ comm.semester }} {{ comm.year }} +
{{ comm.subject.name }}
+
+ {{ comm.subject.code }} + {{ comm.subject.career_name or 'Carrera General' }} + {{ comm.semester }} {{ comm.year }} +
{% if comm.teacher %} @@ -188,13 +206,135 @@ {% else %}
-

No se encontraron comisiones con los filtros aplicados.

- - Limpiar filtros - +
No se encontraron comisiones
+

Crea una nueva comisión para asociarla a una materia y asignarle horarios, cupos y docentes.

+
+ + + Limpiar filtros + +
{% endif %}
+ + + {% endblock %} diff --git a/frontend/views/dashboard.html b/frontend/views/dashboard.html index cd0346f..6f95f5d 100644 --- a/frontend/views/dashboard.html +++ b/frontend/views/dashboard.html @@ -518,7 +518,7 @@ {{ pres.user.name if pres.user else 'Docente' }} - + Revisar @@ -745,7 +745,7 @@ Ingresar a Reunión {% else %} - + {% endif %} @@ -1035,7 +1035,7 @@ {% endif %} - diff --git a/frontend/views/schedule/list.html b/frontend/views/schedule/list.html index 8fd341c..628c6f4 100644 --- a/frontend/views/schedule/list.html +++ b/frontend/views/schedule/list.html @@ -221,17 +221,17 @@ {% endif %} {% if user.can_edit_reservation(reservation) %} - {% endif %} - {% if user.has_permission('reservations', 'read_write') and (reservation.status == 'PENDING' or reservation.status.value == 'PENDING') %} - @@ -326,16 +326,16 @@ {% endif %} - + {% if user.can_edit_reservation(reservation) %} - + {% endif %} {% if user.has_permission('reservations', 'read_write') and (reservation.status == 'PENDING' or reservation.status.value == 'PENDING') %} - + {% endif %} diff --git a/frontend/views/schedule/today.html b/frontend/views/schedule/today.html index 7b78e58..31a6ba9 100644 --- a/frontend/views/schedule/today.html +++ b/frontend/views/schedule/today.html @@ -221,11 +221,11 @@ {% endif %} {% if user.can_edit_reservation(item.reservation) %} - + {% endif %} - + Detalles @@ -305,12 +305,12 @@ {% endif %} {% if user.can_edit_reservation(reservation) %} - {% endif %} - diff --git a/frontend/views/schedule/view.html b/frontend/views/schedule/view.html index 4735e7a..49656fd 100644 --- a/frontend/views/schedule/view.html +++ b/frontend/views/schedule/view.html @@ -12,7 +12,7 @@
{% if user.can_edit_reservation(reservation) %} - + Modificar Reserva {% endif %} @@ -231,23 +231,25 @@
{% if user.can_edit_reservation(reservation) %} - + Modificar Reserva {% endif %} - {% if reservation.status.value == 'PENDING' %} -
-
{% endif %} {% if reservation.can_be_cancelled() %} -
-
{% endif %}