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 @@
| {% trans %}Horario{% endtrans %} | +{% trans %}Fecha y Horario{% endtrans %} | {% trans %}Materia / Motivo{% endtrans %} | {% trans %}Comisión{% endtrans %} | {% trans %}Asistentes{% endtrans %} | @@ -119,7 +119,10 @@ {% for res in upcoming_reservations %}
|---|---|---|---|---|
|
-
+
+ {{ res.start_time.strftime('%d/%m/%Y') }}
+
+
{{ res.start_time.strftime('%H:%M') }} - {{ res.end_time.strftime('%H:%M') }} hs
|
@@ -136,6 +139,11 @@
{{ res.expected_attendees }} / {{ classroom.capacity }}
+ {% 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 %}
+
+ {{ form.virtual_link(class="form-control", id="virtual_link", placeholder="https://zoom.us/j/... o https://meet.google.com/...") }}
+ {% if form.virtual_link.errors %}
+
-
+
+ {{ form.virtual_link.errors[0] }}
+
+ {% endif %}
+
+ {% trans %}Obligatorio para Campus Virtual o clases remotas/híbridas. Se autocompleta con el enlace de la comisión seleccionada.{% endtrans %}
+
+ @@ -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 %}
+
@@ -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 %}
-
+
-
@@ -98,16 +607,16 @@
+
@@ -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 %}
+
-
-
+
+
+
@@ -118,25 +627,25 @@
{% trans %}Profesores, Bedelía y Administradores pueden actualizar el enlace de videollamada y agendar hitos de examen.{% endtrans %}
{% 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 %}
- |