Files
admin-edu-space/app/templates/schedule/edit.html
T

483 lines
25 KiB
HTML

{% extends "base.html" %}
{% block title %}{% trans %}Modificar Reserva{% endtrans %} #{{ reservation.id }} - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1 small">
<li class="breadcrumb-item"><a href="{{ url_for('schedule.today_schedule') }}">{% trans %}Schedule{% endtrans %}</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('schedule.list_reservations') }}">{% trans %}Reservas{% endtrans %}</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('schedule.view_reservation', id=reservation.id) }}">#{{ reservation.id }}</a></li>
<li class="breadcrumb-item active">{% trans %}Modificar{% endtrans %}</li>
</ol>
</nav>
<h1 class="h3 mb-0">
<i class="bi bi-pencil-square text-warning me-2"></i>{% trans %}Modificar Reserva{% endtrans %} #{{ reservation.id }}
</h1>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('schedule.view_reservation', id=reservation.id) }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>{% trans %}Volver al Detalle{% endtrans %}
</a>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-10 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 h6 fw-bold">
<i class="bi bi-calendar3 text-warning me-2"></i>{% trans %}Editar Parámetros de la Reserva{% endtrans %}
</h5>
<span class="badge {% if reservation.status == 'CONFIRMED' %}bg-success{% elif reservation.status == 'PENDING' %}bg-warning{% else %}bg-secondary{% endif %}">
{{ reservation.status }}
</span>
</div>
<div class="card-body p-4">
<form method="POST" id="reservation-form" novalidate>
{{ form.hidden_tag() }}
<!-- 1. SELECTORES EN CASCADA: Edificio -> Piso -> Aula -->
<div class="mb-4">
<div class="p-3 rounded bg-body-tertiary border">
<label class="form-label fw-bold text-primary mb-2">
<i class="bi bi-geo-alt-fill me-1"></i>{% trans %}Espacio Físico / Modalidad{% endtrans %} <span class="text-danger">*</span>
</label>
<div class="row g-3">
<!-- Edificio / Sede -->
<div class="col-md-4">
<label for="cascade-building" class="form-label small fw-semibold text-muted mb-1">
<i class="bi bi-buildings text-primary me-1"></i>1. {% trans %}Edificio / Sede{% endtrans %}
</label>
<select id="cascade-building" class="form-select form-select-sm">
<option value="">{% trans %}-- Selecciona Edificio --{% endtrans %}</option>
{% for b in buildings %}
<option value="{{ b.id }}" data-virtual="{{ 'true' if b.code == 'VIRTUAL' or 'VIRTUAL' in (b.name|upper) else 'false' }}">
{% if b.code == 'VIRTUAL' or 'VIRTUAL' in (b.name|upper) %}🌐{% else %}🏢{% endif %} {{ b.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Piso / Nivel -->
<div class="col-md-4">
<label for="cascade-floor" class="form-label small fw-semibold text-muted mb-1">
<i class="bi bi-layers text-secondary me-1"></i>2. {% trans %}Piso / Nivel{% endtrans %}
</label>
<select id="cascade-floor" class="form-select form-select-sm" disabled>
<option value="">{% trans %}-- Selecciona Piso --{% endtrans %}</option>
</select>
</div>
<!-- Aula / Espacio -->
<div class="col-md-4">
<label for="classroom_id" class="form-label small fw-semibold text-muted mb-1">
<i class="bi bi-door-open text-success me-1"></i>3. {% trans %}Aula Final{% endtrans %} <span class="text-danger">*</span>
</label>
{{ form.classroom_id(class="form-select form-select-sm", id="classroom_id") }}
{% if form.classroom_id.errors %}
<div class="invalid-feedback d-block">{{ form.classroom_id.errors[0] }}</div>
{% endif %}
</div>
</div>
<!-- Card de Info Dinámica del Aula -->
<div id="classroom-info-card" class="mt-3 p-2 rounded bg-body border" style="display: none;">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div>
<span class="badge bg-primary me-2" id="info-room-code">-</span>
<small class="text-muted" id="info-room-location">-</small>
</div>
<div>
<span class="badge bg-info-subtle text-info border me-1" id="info-room-capacity">Cap: -</span>
<span class="badge bg-success-subtle text-success border" id="info-room-type">Presencial</span>
</div>
</div>
</div>
</div>
</div>
<!-- 2. DATOS ACADÉMICOS Y HORARIOS -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label for="commission_id" class="form-label fw-bold text-primary mb-1">
<i class="bi bi-diagram-3 me-1"></i>{% trans %}Comisión Académica{% endtrans %}
</label>
{{ form.commission_id(class="form-select", id="commission_id") }}
<div class="form-text small text-muted">{% trans %}Comisión y asignatura vinculada a esta reserva.{% endtrans %}</div>
{% if form.commission_id.errors %}
<div class="invalid-feedback d-block">{{ form.commission_id.errors[0] }}</div>
{% endif %}
</div>
<div class="col-md-3">
<label for="start_time" class="form-label fw-bold text-primary mb-1">
<i class="bi bi-clock me-1"></i>{% trans %}Inicio{% endtrans %} <span class="text-danger">*</span>
</label>
{{ form.start_time(class="form-control", id="start_time", type="datetime-local") }}
{% if form.start_time.errors %}
<div class="invalid-feedback d-block">{{ form.start_time.errors[0] }}</div>
{% endif %}
</div>
<div class="col-md-3">
<label for="end_time" class="form-label fw-bold text-primary mb-1">
<i class="bi bi-clock-fill me-1"></i>{% trans %}Fin{% endtrans %} <span class="text-danger">*</span>
</label>
{{ form.end_time(class="form-control", id="end_time", type="datetime-local") }}
{% if form.end_time.errors %}
<div class="invalid-feedback d-block">{{ form.end_time.errors[0] }}</div>
{% endif %}
</div>
</div>
<!-- 3. PROPÓSITO Y ASISTENTES -->
<div class="row g-3 mb-4">
<div class="col-md-8">
<label for="purpose" class="form-label fw-bold text-primary mb-1">
<i class="bi bi-journal-text me-1"></i>{% trans %}Propósito / Motivo{% endtrans %} <span class="text-danger">*</span>
</label>
{{ form.purpose(class="form-control", id="purpose", placeholder="ej. Cursada regular, Examen Parcial, Clase Especial") }}
{% if form.purpose.errors %}
<div class="invalid-feedback d-block">{{ form.purpose.errors[0] }}</div>
{% endif %}
</div>
<div class="col-md-4">
<label for="expected_attendees" class="form-label fw-bold text-primary mb-1">
<i class="bi bi-people me-1"></i>{% trans %}Asistentes Estimados{% endtrans %}
</label>
{{ form.expected_attendees(class="form-control", id="expected_attendees", type="number", min="0") }}
{% if form.expected_attendees.errors %}
<div class="invalid-feedback d-block">{{ form.expected_attendees.errors[0] }}</div>
{% endif %}
</div>
</div>
<!-- 4. ENLACE DE AULA VIRTUAL (Zoom, Meet, Teams, etc) -->
<div class="mb-4">
<div class="p-3 rounded bg-primary-subtle border border-primary-subtle">
<label for="virtual_link" class="form-label fw-bold text-primary mb-1">
<i class="bi bi-camera-video-fill me-1"></i>{% trans %}Enlace de Aula Virtual / Videoconferencia{% endtrans %}
</label>
<div class="input-group">
<span class="input-group-text bg-body text-primary"><i class="bi bi-link-45deg"></i></span>
{{ form.virtual_link(class="form-control font-monospace", id="virtual_link", placeholder="https://zoom.us/j/... o https://meet.google.com/...") }}
<button type="button" class="btn btn-outline-primary" id="btn-test-link" title="{% trans %}Probar enlace en nueva pestaña{% endtrans %}">
<i class="bi bi-box-arrow-up-right me-1"></i>{% trans %}Probar{% endtrans %}
</button>
</div>
<div class="form-text small text-muted">
{% trans %}Enlace directo a Zoom, Google Meet, Teams o Campus Virtual. Si se ingresa, se habilitará el botón de acceso directo en cartelera y cronograma.{% endtrans %}
</div>
{% if form.virtual_link.errors %}
<div class="invalid-feedback d-block">{{ form.virtual_link.errors[0] }}</div>
{% endif %}
</div>
</div>
<!-- 5. NOTAS ADICIONALES -->
<div class="mb-4">
<label for="notes" class="form-label fw-bold text-primary mb-1">
<i class="bi bi-card-text me-1"></i>{% trans %}Observaciones / Notas Adicionales{% endtrans %}
</label>
{{ form.notes(class="form-control", id="notes", rows="3", placeholder="Observaciones sobre equipamiento, requerimientos o modalidad...") }}
{% if form.notes.errors %}
<div class="invalid-feedback d-block">{{ form.notes.errors[0] }}</div>
{% endif %}
</div>
<!-- 6. RESUMEN DINÁMICO DE MODIFICACIÓN -->
<div class="card bg-body-tertiary border mb-4 shadow-none">
<div class="card-header bg-transparent py-2 border-bottom">
<h6 class="card-title mb-0 small fw-bold text-primary">
<i class="bi bi-check2-circle me-1"></i>{% trans %}Resumen de Cambios{% endtrans %}
</h6>
</div>
<div class="card-body p-3">
<div class="row g-2 small mb-2">
<div class="col-md-4">
<span class="text-muted">🏢 {% trans %}Edificio:{% endtrans %}</span>
<strong id="summary-building" class="text-body ms-1">-</strong>
</div>
<div class="col-md-4">
<span class="text-muted">📍 {% trans %}Piso / Nivel:{% endtrans %}</span>
<strong id="summary-floor" class="text-body ms-1">-</strong>
</div>
<div class="col-md-4">
<span class="text-muted">🚪 {% trans %}Aula Seleccionada:{% endtrans %}</span>
<strong id="summary-classroom" class="text-primary ms-1">-</strong>
</div>
</div>
<div class="row g-2 small mb-2">
<div class="col-md-4">
<span class="text-muted">⏱️ {% trans %}Duración:{% endtrans %}</span>
<strong id="summary-duration" class="text-body ms-1">-</strong>
</div>
<div class="col-md-4">
<span class="text-muted">👥 {% trans %}Capacidad:{% endtrans %}</span>
<strong id="summary-capacity" class="text-body ms-1">-</strong>
</div>
<div class="col-md-4">
<span class="text-muted">📚 {% trans %}Comisión:{% endtrans %}</span>
<strong id="summary-commission" class="text-body ms-1">-</strong>
</div>
</div>
<div class="p-2 rounded bg-body border d-flex align-items-center justify-content-between">
<div class="text-truncate me-2">
<small class="text-muted d-block">🎥 {% trans %}Enlace de Aula Virtual:{% endtrans %}</small>
<span id="summary-virtual-link" class="small font-monospace text-primary text-break">{% trans %}Sin enlace configurado{% endtrans %}</span>
</div>
<span class="badge bg-success-subtle text-success border" id="summary-virtual-badge" style="display:none;">
<i class="bi bi-camera-video me-1"></i>Virtual
</span>
</div>
</div>
</div>
<!-- Botones -->
<div class="d-flex justify-content-between align-items-center">
<a href="{{ url_for('schedule.view_reservation', id=reservation.id) }}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-1"></i>{% trans %}Cancelar{% endtrans %}
</a>
<button type="submit" class="btn btn-warning px-4 fw-bold">
<i class="bi bi-check-circle me-1"></i>{% trans %}Guardar Modificaciones{% endtrans %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script type="application/json" id="classroom-data">
{
{% for c in classroom_list %}
"{{ c.id }}": {
"id": {{ c.id }},
"building_id": {{ (c.building_id or 0)|tojson }},
"building_name": {{ c.building_name|tojson }},
"floor": {{ c.floor|tojson }},
"floor_display": {{ c.floor_display|tojson }},
"code": {{ c.code|tojson }},
"code_display": {{ c.code_display|tojson }},
"location_display": {{ c.location_display|tojson }},
"capacity": {{ c.capacity_display|tojson }},
"capacity_num": {{ c.capacity }},
"is_virtual": {{ c.is_virtual|tojson }}
}{% if not loop.last %},{% endif %}
{% endfor %}
}
</script>
<script type="application/json" id="commission-data">
{
{% for c in commission_list %}
"{{ c.id }}": {
"id": {{ c.id }},
"code": {{ c.code|tojson }},
"virtual_link": {{ (c.virtual_link or '')|tojson }},
"max_students": {{ c.max_students }}
}{% if not loop.last %},{% endif %}
{% endfor %}
}
</script>
<script>
$(document).ready(function() {
let classroomData = {};
try {
const rawData = document.getElementById('classroom-data')?.textContent;
if (rawData) classroomData = JSON.parse(rawData);
} catch (err) {
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);
}
const $bldgSelect = $('#cascade-building');
const $floorSelect = $('#cascade-floor');
const $roomSelect = $('#classroom_id');
const $commSelect = $('#commission_id');
const $virtualLinkInput = $('#virtual_link');
const $btnTestLink = $('#btn-test-link');
const initialRoomId = "{{ reservation.classroom_id }}";
// 1. Población de Pisos al cambiar Edificio
$bldgSelect.on('change', function() {
const selectedBldg = $(this).val();
$floorSelect.empty().append('<option value="">-- Selecciona Piso --</option>');
$roomSelect.empty().append('<option value="">-- Selecciona Aula --</option>');
if (!selectedBldg) {
$floorSelect.prop('disabled', true);
$roomSelect.prop('disabled', true);
updateSummary();
return;
}
const floors = new Set();
Object.values(classroomData).forEach(function(room) {
if (String(room.building_id) === String(selectedBldg)) {
floors.add(room.floor);
}
});
const sortedFloors = Array.from(floors).sort(function(a, b) {
if (a === '0' || a === 'PB') return -1;
if (b === '0' || b === 'PB') return 1;
return String(a).localeCompare(String(b), undefined, {numeric: true});
});
sortedFloors.forEach(function(fl) {
let label = (fl === '0' || fl === 'PB') ? 'Planta Baja' : ('Piso ' + fl);
$floorSelect.append($('<option>', { value: fl, text: label }));
});
$floorSelect.prop('disabled', false);
$roomSelect.prop('disabled', true);
updateSummary();
});
// 2. Población de Aulas al cambiar Piso
$floorSelect.on('change', function() {
const selectedBldg = $bldgSelect.val();
const selectedFloor = $(this).val();
$roomSelect.empty().append('<option value="">-- Selecciona Aula --</option>');
if (!selectedFloor) {
$roomSelect.prop('disabled', true);
updateSummary();
return;
}
const matchingRooms = Object.values(classroomData).filter(function(room) {
return String(room.building_id) === String(selectedBldg) && String(room.floor) === String(selectedFloor);
});
matchingRooms.sort(function(a, b) { return a.code.localeCompare(b.code); });
matchingRooms.forEach(function(room) {
const capText = room.is_virtual ? '∞ Remoto' : ('Cap: ' + room.capacity);
$roomSelect.append($('<option>', {
value: room.id,
text: room.code + ' (' + capText + ')'
}));
});
$roomSelect.prop('disabled', false);
updateSummary();
});
// 3. Cambio de Aula
$roomSelect.on('change', function() {
updateClassroomInfo();
updateSummary();
});
// 4. Preselección inicial de Aula actual
if (initialRoomId && classroomData[initialRoomId]) {
const initRoom = classroomData[initialRoomId];
$bldgSelect.val(initRoom.building_id).trigger('change');
$floorSelect.val(initRoom.floor).trigger('change');
$roomSelect.val(initRoom.id).trigger('change');
}
function updateClassroomInfo() {
const roomId = $roomSelect.val();
const room = classroomData[roomId];
const $card = $('#classroom-info-card');
if (!room) {
$card.slideUp(150);
return;
}
$('#info-room-code').text(room.code);
$('#info-room-location').text(room.location_display);
$('#info-room-capacity').text('Capacidad: ' + room.capacity);
if (room.is_virtual) {
$('#info-room-type').removeClass('bg-success-subtle text-success').addClass('bg-primary-subtle text-primary').text('🌐 Campus Virtual');
} else {
$('#info-room-type').removeClass('bg-primary-subtle text-primary').addClass('bg-success-subtle text-success').text('🏢 Presencial');
}
$card.slideDown(150);
}
function updateSummary() {
const bldgName = $bldgSelect.find('option:selected').text().trim();
const floorName = $floorSelect.find('option:selected').text().trim();
const roomId = $roomSelect.val();
const room = classroomData[roomId];
$('#summary-building').text(bldgName && !$bldgSelect.val() ? '-' : (bldgName || '-'));
$('#summary-floor').text(floorName && !$floorSelect.val() ? '-' : (floorName || '-'));
$('#summary-classroom').text(room ? room.code : '-');
$('#summary-capacity').text(room ? (room.is_virtual ? '∞ Ilimitada' : room.capacity) : '-');
const commText = $commSelect.find('option:selected').text().trim();
$('#summary-commission').text(commText && $commSelect.val() ? commText : '-');
// Duración
const st = $('#start_time').val();
const et = $('#end_time').val();
if (st && et) {
const startDate = new Date(st);
const endDate = new Date(et);
const diffMs = endDate - startDate;
if (diffMs > 0) {
const diffHrs = Math.floor(diffMs / 3600000);
const diffMins = Math.round((diffMs % 3600000) / 60000);
let durText = '';
if (diffHrs > 0) durText += diffHrs + ' hs ';
if (diffMins > 0) durText += diffMins + ' min';
$('#summary-duration').text(durText);
} else {
$('#summary-duration').text('Rango inválido');
}
}
// Enlace virtual
const vLink = $virtualLinkInput.val() ? $virtualLinkInput.val().trim() : '';
if (vLink) {
$('#summary-virtual-link').html('<a href="' + vLink + '" target="_blank" class="text-decoration-none text-primary"><i class="bi bi-box-arrow-up-right me-1"></i>' + vLink + '</a>');
$('#summary-virtual-badge').show();
} else {
$('#summary-virtual-link').text('Sin enlace configurado');
$('#summary-virtual-badge').hide();
}
}
// Eventos reactivos
$('#start_time, #end_time, #commission_id, #virtual_link').on('input change', updateSummary);
// Botón Probar enlace
$btnTestLink.on('click', function() {
const link = $virtualLinkInput.val() ? $virtualLinkInput.val().trim() : '';
if (link) {
window.open(link, '_blank');
} else {
alert('Por favor ingresa primero una URL para probar.');
}
});
updateSummary();
});
</script>
{% endblock %}