diff --git a/app/forms/reservation.py b/app/forms/reservation.py index 05ebede..3e822c0 100644 --- a/app/forms/reservation.py +++ b/app/forms/reservation.py @@ -21,6 +21,10 @@ class ReservationForm(FlaskForm): Optional(), Length(max=1000) ]) + virtual_link = StringField(_l('Virtual Classroom Link'), validators=[ + Optional(), + Length(max=500) + ]) submit = SubmitField(_l('Make Reservation')) def validate_end_time(self, field): diff --git a/app/models/reservation.py b/app/models/reservation.py index 2cac5ae..6583dd0 100644 --- a/app/models/reservation.py +++ b/app/models/reservation.py @@ -47,6 +47,7 @@ class Reservation(db.Model): status = db.Column(StatusType, default='PENDING', nullable=False) # Maps to VARCHAR(20) with StatusString expected_attendees = db.Column(db.Integer, nullable=False) shift = db.Column(db.String(30), nullable=True) # Mañana, Tarde, Vespertino, Noche + virtual_link = db.Column(db.String(500), nullable=True) # Zoom, Meet, Teams, etc. notes = db.Column(db.Text) created_at = db.Column(db.DateTime, default=datetime.utcnow) @@ -81,6 +82,24 @@ class Reservation(db.Model): if self.classroom: return self.classroom.code return '' + + @property + def effective_virtual_link(self): + """Devuelve el enlace explícito de la reserva o hereda el de la comisión""" + if self.virtual_link and self.virtual_link.strip(): + return self.virtual_link.strip() + if self.commission and self.commission.virtual_link: + return self.commission.virtual_link.strip() + return None + + @property + def is_virtual(self): + """Verifica si la reserva es virtual por aula o por contar con enlace virtual""" + if self.classroom and self.classroom.is_virtual: + return True + if self.effective_virtual_link: + return True + return False # Relationships classroom = db.relationship('Classroom', backref=db.backref('reservation_list', lazy=True, cascade='all, delete-orphan')) @@ -100,6 +119,8 @@ class Reservation(db.Model): 'status': self.status, # Already a string in DB 'expected_attendees': self.expected_attendees, 'notes': self.notes, + 'virtual_link': self.effective_virtual_link, + 'is_virtual': self.is_virtual, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 'classroom': self.classroom.to_dict() if self.classroom else None, diff --git a/app/routes/schedule.py b/app/routes/schedule.py index 5164534..aaf306a 100644 --- a/app/routes/schedule.py +++ b/app/routes/schedule.py @@ -446,6 +446,7 @@ def add_reservation(): end_time=form.end_time.data, purpose=form.purpose.data, notes=form.notes.data, + virtual_link=form.virtual_link.data.strip() if form.virtual_link.data else None, status=ReservationStatus.PENDING.value, expected_attendees=form.expected_attendees.data ) @@ -462,6 +463,50 @@ def add_reservation(): return render_template('schedule/add.html', form=form, title='New Reservation', classroom_list=classrooms, commission_list=commissions, buildings=buildings) +@schedule_bp.route('/reservation/add', methods=['POST']) +@login_required +def quick_reservation_add(): + """Endpoint AJAX para crear reserva rápida desde el modal de comisiones""" + try: + commission_id = request.form.get('commission_id', type=int) + classroom_id = request.form.get('classroom_id', type=int) + purpose = request.form.get('purpose', '').strip() or 'Clase de cursada' + start_str = request.form.get('start_time', '').strip() + end_str = request.form.get('end_time', '').strip() + virtual_link = request.form.get('virtual_link', '').strip() or None + + if not commission_id or not classroom_id or not start_str or not end_str: + return jsonify({'error': 'Faltan campos obligatorios'}), 400 + + start_time = datetime.strptime(start_str, '%Y-%m-%d %H:%M') + end_time = datetime.strptime(end_str, '%Y-%m-%d %H:%M') + + from app.models.subject import Commission + from app.models.classroom import Classroom + comm = Commission.query.get_or_404(commission_id) + + if not virtual_link and comm.virtual_link: + virtual_link = comm.virtual_link + + new_res = Reservation( + classroom_id=classroom_id, + commission_id=commission_id, + user_id=comm.teacher_id or current_user.id, + start_time=start_time, + end_time=end_time, + purpose=purpose, + expected_attendees=comm.max_students or 30, + shift=comm.shift or 'Tarde', + virtual_link=virtual_link, + status=ReservationStatus.CONFIRMED.value if current_user.is_admin() else ReservationStatus.PENDING.value + ) + db.session.add(new_res) + db.session.commit() + return jsonify({'success': True, 'id': new_res.id}) + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + @schedule_bp.route('/list') @login_required def list_reservations(): @@ -777,6 +822,7 @@ def calendar_data(): 'attendees': reservation.expected_attendees or 0, 'notes': reservation.notes or '', 'teacher': reservation.user.name if reservation.user else '', + 'virtual_link': reservation.effective_virtual_link, 'cancel_url': url_for('schedule.cancel_reservation', id=reservation.id) } }) diff --git a/app/templates/classrooms/view.html b/app/templates/classrooms/view.html index c3429df..3f2e4a1 100644 --- a/app/templates/classrooms/view.html +++ b/app/templates/classrooms/view.html @@ -108,7 +108,7 @@ - + @@ -119,7 +119,10 @@ {% for res in upcoming_reservations %} @@ -136,6 +139,11 @@ {{ res.expected_attendees }} / {{ classroom.capacity }}
{% trans %}Horario{% endtrans %}{% trans %}Fecha y Horario{% endtrans %} {% trans %}Materia / Motivo{% endtrans %} {% trans %}Comisión{% endtrans %} {% trans %}Asistentes{% endtrans %}
-
+
+ {{ res.start_time.strftime('%d/%m/%Y') }} +
+
{{ res.start_time.strftime('%H:%M') }} - {{ res.end_time.strftime('%H:%M') }} hs
+ {% if res.effective_virtual_link %} + + + + {% endif %} diff --git a/app/templates/schedule/add.html b/app/templates/schedule/add.html index c184a93..5363918 100644 --- a/app/templates/schedule/add.html +++ b/app/templates/schedule/add.html @@ -152,7 +152,7 @@ -
+
{{ form.notes.label(class="form-label fw-semibold") }} {{ form.notes(class="form-control", rows="2", placeholder=_("Cualquier nota adicional o requerimientos especiales...")) }} {% if form.notes.errors %} @@ -161,8 +161,24 @@
{% endif %}
+ + + - +
@@ -191,6 +207,21 @@
+ +
+
+
+
+ 🎥 {% trans %}Enlace Virtual (Videoconferencia){% endtrans %} + {% trans %}Sin enlace configurado{% endtrans %} +
+ +
+
+
+
@@ -243,6 +274,19 @@ } + + @@ -267,6 +312,14 @@ $(document).ready(function() { console.error('Error parsing classroomData:', err); } + let commissionData = {}; + try { + const rawComm = document.getElementById('commission-data')?.textContent; + if (rawComm) commissionData = JSON.parse(rawComm); + } catch (err) { + console.error('Error parsing commissionData:', err); + } + let i18n = {}; try { const rawI18n = document.getElementById('i18n-strings')?.textContent; @@ -457,13 +510,28 @@ $(document).ready(function() { $('#summary-classroom').text(i18n.not_selected || 'No seleccionada'); } - // Update commission summary + // Update commission summary and autofill virtual link if available if (commissionId) { const commissionText = $('#commission-select option:selected').text(); $('#summary-commission').text(commissionText); } else { $('#summary-commission').text(i18n.not_selected || 'No seleccionada'); } + + // Update virtual link in summary + const vLink = ($('#virtual_link').val() || '').trim(); + const isVirtual = (classroomId && classroomData[classroomId] && classroomData[classroomId].is_virtual); + + if (vLink) { + $('#summary-virtual-link').text(vLink); + $('#summary-virtual-badge').show(); + } else if (isVirtual) { + $('#summary-virtual-link').html('Campus Virtual (Recomendado agregar enlace Zoom/Meet)'); + $('#summary-virtual-badge').hide(); + } else { + $('#summary-virtual-link').text(i18n.no_virtual_link || 'Sin enlace configurado (Clase presencial)'); + $('#summary-virtual-badge').hide(); + } // Calculate duration if (startTime && endTime) { @@ -526,7 +594,19 @@ $(document).ready(function() { } } - $('#commission-select, #start-time, #end-time').on('change', updateSummary); + $('#commission-select').on('change', function() { + const cId = $(this).val(); + if (cId && commissionData[cId]) { + const comm = commissionData[cId]; + if (comm.virtual_link && !$('#virtual_link').val().trim()) { + $('#virtual_link').val(comm.virtual_link); + } + } + updateSummary(); + }); + + $('#virtual_link').on('input change', updateSummary); + $('#start-time, #end-time').on('change', updateSummary); $('#expected_attendees').on('input', function() { checkCapacity(); updateSummary(); diff --git a/app/templates/schedule/calendar.html b/app/templates/schedule/calendar.html index 08c68c4..aa88aff 100644 --- a/app/templates/schedule/calendar.html +++ b/app/templates/schedule/calendar.html @@ -237,6 +237,11 @@ {% trans %}Campus Virtual UniCABA (Teams / Moodle){% endtrans %}
+
@@ -963,6 +968,17 @@ document.addEventListener('DOMContentLoaded', function() { if (virtualWrap) virtualWrap.classList.add('d-none'); } + // Enlace de Aula Virtual / Videollamada + var vLink = ext.virtual_link; + var vBtnWrap = document.getElementById('previewVirtualLinkWrapper'); + var vBtn = document.getElementById('previewVirtualButton'); + if (vLink && vBtnWrap && vBtn) { + vBtn.href = vLink; + vBtnWrap.classList.remove('d-none'); + } else if (vBtnWrap) { + vBtnWrap.classList.add('d-none'); + } + // Carrera, Comisión, Docente y Asistentes document.getElementById('previewCareer').textContent = career; document.getElementById('previewCommission').textContent = commission; diff --git a/app/templates/schedule/commissions.html b/app/templates/schedule/commissions.html index 47473c9..0e86f41 100644 --- a/app/templates/schedule/commissions.html +++ b/app/templates/schedule/commissions.html @@ -2,6 +2,515 @@ {% block title %}{% trans %}Comisiones y Cursadas{% endtrans %} - Edu-Space{% endblock %} +{% block extra_css %} + +{% endblock %} + {% block content %}
@@ -38,48 +547,48 @@
-
+
-
+
-
{{ subjects|length }}
-
{% trans %}Materias Activas{% endtrans %}
+
{{ subjects|length }}
+
{% trans %}Materias Activas{% endtrans %}
-
+
-
+
-
{{ total_commissions }}
-
{% trans %}Total Comisiones{% endtrans %}
+
{{ total_commissions }}
+
{% trans %}Total Comisiones{% endtrans %}
-
+
-
+
-
{{ total_capacity }}
-
{% trans %}Capacidad Total{% endtrans %}
+
{{ total_capacity }}
+
{% trans %}Capacidad Total{% endtrans %}
-
+
-
+
@@ -89,8 +598,8 @@ {% if c.virtual_link %}{% set ns.virtual_count = ns.virtual_count + 1 %}{% endif %} {% endfor %} {% endfor %} -
{{ ns.virtual_count }}
-
{% trans %}Aulas Virtuales Config.{% endtrans %}
+
{{ ns.virtual_count }}
+
{% trans %}Aulas Virtuales Config.{% endtrans %}
@@ -98,16 +607,16 @@
-
+
- - + +
-
+
{% trans %}Profesores, Bedelía y Administradores pueden actualizar el enlace de videollamada y agendar hitos de examen.{% endtrans %}
@@ -118,25 +627,25 @@
{% for subject in subjects %}
-
-
+
+
- {{ subject.code }} + {{ subject.code }}
-
{{ subject.name }}
- +
{{ subject.name }}
+ {{ subject.department or 'UniCABA' }} {% if subject.career %} • {{ subject.career }}{% endif %}
- + {{ subject.commissions|length }} {% trans %}comisiones{% endtrans %} {% if can_edit_commissions %} -