feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia
This commit is contained in:
@@ -0,0 +1,664 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}New Reservation - 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>
|
||||
<h1 class="h3 mb-0">
|
||||
<i class="bi bi-calendar-plus text-primary me-2"></i>New Reservation
|
||||
</h1>
|
||||
</div>
|
||||
<a href="/schedule/list_reservations" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back to Reservations
|
||||
</a>
|
||||
</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">
|
||||
<h5 class="mb-0 h6 fw-bold">
|
||||
<i class="bi bi-calendar3 text-primary me-2"></i>Detalles de la Reserva y Espacio Físico
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" id="reservation-form" novalidate>
|
||||
|
||||
<!-- 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>Selección de Espacio Físico / Modalidad <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. Edificio / Sede
|
||||
</label>
|
||||
<select id="cascade-building" class="form-select form-select-sm">
|
||||
<option value="">-- Selecciona Edificio --</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 class="form-text small text-muted">Sede física o Campus Virtual</div>
|
||||
</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. Piso / Nivel
|
||||
</label>
|
||||
<select id="cascade-floor" class="form-select form-select-sm" disabled>
|
||||
<option value="">-- Selecciona Piso --</option>
|
||||
</select>
|
||||
<div class="form-text small text-muted" id="floorHelpText">Filtra por planta o nivel</div>
|
||||
</div>
|
||||
|
||||
<!-- Aula / Espacio (Campo del Formulario) -->
|
||||
<div class="col-md-4">
|
||||
<label for="classroom-select" class="form-label small fw-semibold text-muted mb-1">
|
||||
<i class="bi bi-door-open text-success me-1"></i>3. Aula / Espacio <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.classroom_id(class="form-select form-select-sm", id="classroom-select") }}
|
||||
{% if form.classroom_id.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.classroom_id.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text small text-muted">Capacidad y disponibilidad</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Comisión y Materia -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12">
|
||||
{{ form.commission_id.label(class="form-label fw-semibold") }} <span class="text-danger">*</span>
|
||||
{{ form.commission_id(class="form-select", id="commission-select") }}
|
||||
{% if form.commission_id.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.commission_id.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text small text-muted">Materia, código de comisión y cantidad de alumnos inscriptos</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Horario de Inicio y Fin -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
{{ form.start_time.label(class="form-label fw-semibold") }} <span class="text-danger">*</span>
|
||||
{{ form.start_time(class="form-control", id="start-time") }}
|
||||
{% if form.start_time.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.start_time.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<small class="text-muted">¿Cuándo comenzará la clase?</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
{{ form.end_time.label(class="form-label fw-semibold") }} <span class="text-danger">*</span>
|
||||
{{ form.end_time(class="form-control", id="end-time") }}
|
||||
{% if form.end_time.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.end_time.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<small class="text-muted">¿Cuándo finalizará la clase?</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Propósito y Asistentes -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-8">
|
||||
{{ form.purpose.label(class="form-label fw-semibold") }} <span class="text-danger">*</span>
|
||||
{{ form.purpose(class="form-control", placeholder=_("Clase regular, examen, sesión de estudio, etc.")) }}
|
||||
{% if form.purpose.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.purpose.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
{{ form.expected_attendees.label(class="form-label fw-semibold") }} <span class="text-danger">*</span>
|
||||
{{ form.expected_attendees(class="form-control", id="expected_attendees", placeholder="30") }}
|
||||
{% if form.expected_attendees.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.expected_attendees.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<small class="text-muted">Cantidad de asistentes esperados</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. Notas -->
|
||||
<div class="mb-3">
|
||||
{{ 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 %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.notes.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 6. Enlace de Aula Virtual (Zoom, Meet, Teams, Moodle) -->
|
||||
<div class="mb-4" id="virtual-link-container">
|
||||
<label for="virtual_link" class="form-label fw-semibold">
|
||||
<i class="bi bi-camera-video text-primary me-1"></i>Enlace de Aula Virtual (Zoom, Google Meet, Teams, Moodle)
|
||||
</label>
|
||||
{{ form.virtual_link(class="form-control", id="virtual_link", placeholder="https://zoom.us/j/... o https://meet.google.com/...") }}
|
||||
{% if form.virtual_link.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.virtual_link.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text small text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i>Obligatorio para Campus Virtual o clases remotas/híbridas. Se autocompleta con el enlace de la comisión seleccionada.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 7. TARJETA DE RESUMEN DESGLOSADA: Edificio, Piso, Aula, Modalidad -->
|
||||
<div class="card bg-body-tertiary border-0 shadow-xs mb-4" id="reservation-summary">
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-subtitle mb-2 text-muted small fw-bold">
|
||||
<i class="bi bi-info-circle text-info me-1"></i>Resumen de la Reserva
|
||||
</h6>
|
||||
|
||||
<!-- Badges desglosados de Ubicación -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="p-2 rounded bg-body border">
|
||||
<small class="text-muted d-block">🏢 Edificio / Sede</small>
|
||||
<strong id="summary-building" class="text-primary">-</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="p-2 rounded bg-body border">
|
||||
<small class="text-muted d-block">🪜 Piso / Nivel</small>
|
||||
<strong id="summary-floor" class="text-secondary">-</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="p-2 rounded bg-body border">
|
||||
<small class="text-muted d-block">🚪 Aula Seleccionada</small>
|
||||
<strong id="summary-classroom" class="text-success">No seleccionada</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fila de Enlace Virtual -->
|
||||
<div class="row g-2 mb-3" id="summary-virtual-row">
|
||||
<div class="col-12">
|
||||
<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">🎥 Enlace Virtual (Videoconferencia)</small>
|
||||
<span id="summary-virtual-link" class="small font-monospace text-primary text-break">Sin enlace configurado</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>
|
||||
|
||||
<!-- Duración y Comisión -->
|
||||
<div class="row g-2 small">
|
||||
<div class="col-md-4">
|
||||
<span class="text-muted">⏱️ Duración:</span>
|
||||
<strong id="summary-duration" class="text-body ms-1">No calculada</strong>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<span class="text-muted">📚 Comisión:</span>
|
||||
<strong id="summary-commission" class="text-body ms-1">No seleccionada</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botones -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="/schedule/today_schedule" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-circle me-1"></i>Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-success px-4">
|
||||
<i class="bi bi-calendar-check me-1"></i>Realizar Reserva
|
||||
</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 type="application/json" id="i18n-strings">
|
||||
{
|
||||
"not_selected": "No seleccionada",
|
||||
"not_calculated": "No calculada",
|
||||
"invalid_time": "Rango horario inválido",
|
||||
"hour": "hora",
|
||||
"hours": "horas",
|
||||
"minute": "minuto",
|
||||
"minutes": "minutos",
|
||||
"capacity": "Capacidad",
|
||||
"capacity_exceeded": "Los asistentes esperados superan la capacidad del aula",
|
||||
"no_virtual_link": "Sin enlace configurado (Clase presencial)"
|
||||
}
|
||||
</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);
|
||||
}
|
||||
|
||||
let i18n = {};
|
||||
try {
|
||||
const rawI18n = document.getElementById('i18n-strings')?.textContent;
|
||||
if (rawI18n) i18n = JSON.parse(rawI18n);
|
||||
} catch (err) {
|
||||
console.error('Error parsing i18n:', err);
|
||||
}
|
||||
|
||||
const $building = $('#cascade-building');
|
||||
const $floor = $('#cascade-floor');
|
||||
const $classroom = $('#classroom-select');
|
||||
const $floorHelp = $('#floorHelpText');
|
||||
|
||||
// Guardar todas las opciones originales de aulas
|
||||
const originalClassroomOptions = [];
|
||||
$classroom.find('option').each(function() {
|
||||
originalClassroomOptions.push({
|
||||
value: $(this).val(),
|
||||
text: $(this).text()
|
||||
});
|
||||
});
|
||||
|
||||
// 1. Manejo del cambio de Edificio
|
||||
$building.on('change', function() {
|
||||
const selectedBuildingId = $(this).val();
|
||||
const isVirtual = $(this).find(':selected').data('virtual') === true || $(this).find(':selected').data('virtual') === 'true';
|
||||
|
||||
$floor.empty().append('<option value="">-- ' + (isVirtual ? 'No aplica (Campus Virtual)' : 'Selecciona Piso') + ' --</option>');
|
||||
$classroom.empty().append('<option value="">-- Selecciona Aula --</option>');
|
||||
|
||||
if (!selectedBuildingId) {
|
||||
$floor.prop('disabled', true);
|
||||
$classroom.prop('disabled', false);
|
||||
// Restaurar todas
|
||||
originalClassroomOptions.forEach(opt => {
|
||||
if (opt.value) $classroom.append($('<option>', { value: opt.value, text: opt.text }));
|
||||
});
|
||||
updateSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVirtual) {
|
||||
$floor.prop('disabled', true);
|
||||
$floorHelp.text('Modalidad 100% remota sin piso asignado');
|
||||
|
||||
// Filtrar solo aulas virtuales
|
||||
let virtualCount = 0;
|
||||
let lastVirtualId = null;
|
||||
Object.values(classroomData).forEach(c => {
|
||||
if (c.is_virtual || c.building_id == selectedBuildingId) {
|
||||
$classroom.append($('<option>', {
|
||||
value: c.id,
|
||||
text: `${c.code_display} (Cap: ${c.capacity})`
|
||||
}));
|
||||
lastVirtualId = c.id;
|
||||
virtualCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (virtualCount === 1 && lastVirtualId) {
|
||||
$classroom.val(lastVirtualId);
|
||||
}
|
||||
updateSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
// Edificio Físico
|
||||
$floor.prop('disabled', false);
|
||||
$floorHelp.text('Filtra por planta o nivel');
|
||||
|
||||
// Extraer pisos únicos de este edificio
|
||||
const buildingFloors = [];
|
||||
Object.values(classroomData).forEach(c => {
|
||||
if (c.building_id == selectedBuildingId && c.floor) {
|
||||
if (!buildingFloors.some(f => f.floor === c.floor)) {
|
||||
buildingFloors.push({
|
||||
floor: c.floor,
|
||||
floor_display: c.floor_display
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Ordenar pisos: PB primero, luego numérico
|
||||
buildingFloors.sort((a, b) => {
|
||||
if (a.floor === '0' || a.floor.toUpperCase() === 'PB') return -1;
|
||||
if (b.floor === '0' || b.floor.toUpperCase() === 'PB') return 1;
|
||||
return a.floor.localeCompare(b.floor, undefined, { numeric: true });
|
||||
});
|
||||
|
||||
buildingFloors.forEach(f => {
|
||||
$floor.append($('<option>', {
|
||||
value: f.floor,
|
||||
text: f.floor_display
|
||||
}));
|
||||
});
|
||||
|
||||
// Poblar aulas de este edificio inicialmente
|
||||
Object.values(classroomData).forEach(c => {
|
||||
if (c.building_id == selectedBuildingId) {
|
||||
$classroom.append($('<option>', {
|
||||
value: c.id,
|
||||
text: `${c.code} · ${c.floor_display} (Cap: ${c.capacity})`
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
updateSummary();
|
||||
});
|
||||
|
||||
// 2. Manejo del cambio de Piso
|
||||
$floor.on('change', function() {
|
||||
const selectedBuildingId = $building.val();
|
||||
const selectedFloor = $(this).val();
|
||||
|
||||
$classroom.empty().append('<option value="">-- Selecciona Aula --</option>');
|
||||
|
||||
let count = 0;
|
||||
let lastId = null;
|
||||
|
||||
Object.values(classroomData).forEach(c => {
|
||||
const matchesBuilding = !selectedBuildingId || c.building_id == selectedBuildingId;
|
||||
const matchesFloor = !selectedFloor || c.floor == selectedFloor;
|
||||
|
||||
if (matchesBuilding && matchesFloor) {
|
||||
$classroom.append($('<option>', {
|
||||
value: c.id,
|
||||
text: `${c.code} (Cap: ${c.capacity})`
|
||||
}));
|
||||
lastId = c.id;
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
if (count === 1 && lastId) {
|
||||
$classroom.val(lastId);
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
});
|
||||
|
||||
// 3. Manejo del cambio de Aula directo
|
||||
$classroom.on('change', function() {
|
||||
const cId = $(this).val();
|
||||
if (cId && classroomData[cId]) {
|
||||
const c = classroomData[cId];
|
||||
// Sincronizar selectores en cascada si no coinciden
|
||||
if ($building.val() != c.building_id) {
|
||||
$building.val(c.building_id);
|
||||
// Si no es virtual, sincronizar piso
|
||||
if (!c.is_virtual) {
|
||||
$floor.prop('disabled', false);
|
||||
$floor.val(c.floor);
|
||||
}
|
||||
} else if (!c.is_virtual && $floor.val() != c.floor) {
|
||||
$floor.val(c.floor);
|
||||
}
|
||||
}
|
||||
updateSummary();
|
||||
});
|
||||
|
||||
// Inicializar cascada si ya hay un aula seleccionada (ej. post-error o query string)
|
||||
const initialClassroomId = $classroom.val();
|
||||
if (initialClassroomId && classroomData[initialClassroomId]) {
|
||||
const c = classroomData[initialClassroomId];
|
||||
$building.val(c.building_id).trigger('change');
|
||||
if (!c.is_virtual) {
|
||||
$floor.val(c.floor).trigger('change');
|
||||
}
|
||||
$classroom.val(initialClassroomId);
|
||||
}
|
||||
|
||||
function updateSummary() {
|
||||
const classroomId = $classroom.val();
|
||||
const commissionId = $('#commission-select').val();
|
||||
const startTime = $('#start-time').val();
|
||||
const endTime = $('#end-time').val();
|
||||
|
||||
// Update classroom badges
|
||||
if (classroomId && classroomData[classroomId]) {
|
||||
const c = classroomData[classroomId];
|
||||
$('#summary-building').text(c.building_name);
|
||||
$('#summary-floor').text(c.is_virtual ? 'Remoto (Virtual)' : c.floor_display);
|
||||
$('#summary-classroom').text(`${c.code} (Cap: ${c.capacity})`);
|
||||
} else {
|
||||
$('#summary-building').text('-');
|
||||
$('#summary-floor').text('-');
|
||||
$('#summary-classroom').text(i18n.not_selected || 'No seleccionada');
|
||||
}
|
||||
|
||||
// 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('<span class="text-warning fw-semibold"><i class="bi bi-exclamation-triangle me-1"></i>Campus Virtual (Recomendado agregar enlace Zoom/Meet)</span>');
|
||||
$('#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) {
|
||||
const start = new Date(startTime);
|
||||
const end = new Date(endTime);
|
||||
|
||||
if (end > start) {
|
||||
const diffMs = end - start;
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
let durationText = '';
|
||||
if (diffHours > 0) {
|
||||
const hLabel = diffHours > 1 ? (i18n.hours || 'horas') : (i18n.hour || 'hora');
|
||||
durationText += `${diffHours} ${hLabel}`;
|
||||
}
|
||||
if (diffMins > 0) {
|
||||
if (durationText) durationText += ' ';
|
||||
const mLabel = diffMins > 1 ? (i18n.minutes || 'minutos') : (i18n.minute || 'minuto');
|
||||
durationText += `${diffMins} ${mLabel}`;
|
||||
}
|
||||
|
||||
$('#summary-duration').text(durationText || '0 min');
|
||||
} else {
|
||||
$('#summary-duration').text(i18n.invalid_time || 'Rango horario inválido');
|
||||
}
|
||||
} else {
|
||||
$('#summary-duration').text(i18n.not_calculated || 'No calculada');
|
||||
}
|
||||
}
|
||||
|
||||
// Check capacity vs expected attendees
|
||||
function checkCapacity() {
|
||||
const classroomId = $classroom.val();
|
||||
const attendees = parseInt($('#expected_attendees').val()) || 0;
|
||||
|
||||
if (classroomId && classroomData[classroomId] && attendees > 0) {
|
||||
const c = classroomData[classroomId];
|
||||
// No validar si es virtual o capacidad ilimitada
|
||||
if (c.is_virtual || c.capacity === '∞' || c.capacity_num === 0) {
|
||||
$('#expected_attendees').removeClass('is-invalid');
|
||||
$('#expected_attendees').siblings('.invalid-feedback').remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (attendees > c.capacity_num) {
|
||||
$('#expected_attendees').addClass('is-invalid');
|
||||
const msg = i18n.capacity_exceeded || 'Los asistentes esperados superan la capacidad del aula';
|
||||
if ($('#expected_attendees').siblings('.invalid-feedback').length === 0) {
|
||||
$('#expected_attendees').after(`
|
||||
<div class="invalid-feedback">
|
||||
${msg} (${attendees} > ${c.capacity_num})
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
$('#expected_attendees').removeClass('is-invalid');
|
||||
$('#expected_attendees').siblings('.invalid-feedback').remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('#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();
|
||||
});
|
||||
|
||||
updateSummary();
|
||||
|
||||
// Form validation
|
||||
$('#reservation-form').on('submit', function(e) {
|
||||
let isValid = true;
|
||||
|
||||
if (!$classroom.val()) {
|
||||
$classroom.addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
$classroom.removeClass('is-invalid');
|
||||
}
|
||||
|
||||
if (!$('#commission-select').val()) {
|
||||
$('#commission-select').addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
$('#commission-select').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
if (!$('#start-time').val()) {
|
||||
$('#start-time').addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
$('#start-time').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
if (!$('#end-time').val()) {
|
||||
$('#end-time').addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
$('#end-time').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
const startTime = new Date($('#start-time').val());
|
||||
const endTime = new Date($('#end-time').val());
|
||||
|
||||
if (endTime <= startTime) {
|
||||
$('#end-time').addClass('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
checkCapacity();
|
||||
if ($('#expected_attendees').hasClass('is-invalid')) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
});
|
||||
|
||||
// Set minimum datetime to current
|
||||
const now = new Date();
|
||||
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
|
||||
$('#start-time, #end-time').attr('min', now.toISOString().slice(0, 16));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,481 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Modificar Reserva #{{ 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="/schedule/today_schedule">Schedule</a></li>
|
||||
<li class="breadcrumb-item"><a href="/schedule/list_reservations">Reservas</a></li>
|
||||
<li class="breadcrumb-item"><a href="/schedule/view_reservation">#{{ reservation.id }}</a></li>
|
||||
<li class="breadcrumb-item active">Modificar</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="h3 mb-0">
|
||||
<i class="bi bi-pencil-square text-warning me-2"></i>Modificar Reserva #{{ reservation.id }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/schedule/view_reservation" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Volver al Detalle
|
||||
</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>Editar Parámetros de la Reserva
|
||||
</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>
|
||||
|
||||
<!-- 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>Espacio Físico / Modalidad <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. Edificio / Sede
|
||||
</label>
|
||||
<select id="cascade-building" class="form-select form-select-sm">
|
||||
<option value="">-- Selecciona Edificio --</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. Piso / Nivel
|
||||
</label>
|
||||
<select id="cascade-floor" class="form-select form-select-sm" disabled>
|
||||
<option value="">-- Selecciona Piso --</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. Aula Final <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>Comisión Académica
|
||||
</label>
|
||||
{{ form.commission_id(class="form-select", id="commission_id") }}
|
||||
<div class="form-text small text-muted">Comisión y asignatura vinculada a esta reserva.</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>Inicio <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>Fin <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>Propósito / Motivo <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>Asistentes Estimados
|
||||
</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>Enlace de Aula Virtual / Videoconferencia
|
||||
</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="Probar enlace en nueva pestaña">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>Probar
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text small text-muted">
|
||||
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.
|
||||
</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>Observaciones / Notas Adicionales
|
||||
</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>Resumen de Cambios
|
||||
</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">🏢 Edificio:</span>
|
||||
<strong id="summary-building" class="text-body ms-1">-</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<span class="text-muted">📍 Piso / Nivel:</span>
|
||||
<strong id="summary-floor" class="text-body ms-1">-</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<span class="text-muted">🚪 Aula Seleccionada:</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">⏱️ Duración:</span>
|
||||
<strong id="summary-duration" class="text-body ms-1">-</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<span class="text-muted">👥 Capacidad:</span>
|
||||
<strong id="summary-capacity" class="text-body ms-1">-</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<span class="text-muted">📚 Comisión:</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">🎥 Enlace de Aula Virtual:</small>
|
||||
<span id="summary-virtual-link" class="small font-monospace text-primary text-break">Sin enlace configurado</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="/schedule/view_reservation" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-circle me-1"></i>Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-warning px-4 fw-bold">
|
||||
<i class="bi bi-check-circle me-1"></i>Guardar Modificaciones
|
||||
</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 %}
|
||||
@@ -0,0 +1,402 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Listado de Reservas - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="col-md-7">
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-calendar-check text-primary me-2"></i>Listado de Reservas
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">Registro histórico y programación completa de clases y asignación de aulas</p>
|
||||
</div>
|
||||
<div class="col-md-5 text-md-end mt-3 mt-md-0">
|
||||
{% if user.has_permission('reservations', 'read_write') %}
|
||||
<a href="/schedule/add_reservation" class="btn btn-success me-2">
|
||||
<i class="bi bi-plus-circle me-1"></i>Nueva Reserva
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/schedule/calendar_view" class="btn btn-outline-primary me-2">
|
||||
<i class="bi bi-calendar3 me-1"></i>Ver Calendario
|
||||
</a>
|
||||
<a href="/schedule/today_schedule" class="btn btn-outline-info">
|
||||
<i class="bi bi-calendar-day me-1"></i>Cartelera de Hoy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Card -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-3">
|
||||
<form method="GET" action="/schedule/list_reservations" class="row g-2 align-items-end">
|
||||
<!-- Search text -->
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<label class="form-label small fw-bold text-muted mb-1">
|
||||
<i class="bi bi-search me-1"></i>Buscar
|
||||
</label>
|
||||
<input type="text" name="search" class="form-control form-control-sm" placeholder="Materia, código o aula..." value="{{ search or '' }}">
|
||||
</div>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<div class="col-lg-2 col-md-3">
|
||||
<label class="form-label small fw-bold text-muted mb-1">
|
||||
<i class="bi bi-shield-check me-1"></i>Estado
|
||||
</label>
|
||||
<select name="status" class="form-select form-select-sm">
|
||||
<option value="all" {% if status_filter == 'all' or not status_filter %}selected{% endif %}>Todos los estados</option>
|
||||
<option value="confirmed" {% if status_filter == 'confirmed' %}selected{% endif %}>Confirmadas</option>
|
||||
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Pendientes</option>
|
||||
<option value="cancelled" {% if status_filter == 'cancelled' %}selected{% endif %}>Canceladas</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Floor Filter -->
|
||||
<div class="col-lg-2 col-md-3">
|
||||
<label class="form-label small fw-bold text-muted mb-1">
|
||||
<i class="bi bi-layers me-1"></i>Piso
|
||||
</label>
|
||||
<select name="floor" class="form-select form-select-sm">
|
||||
<option value="">Todos los pisos</option>
|
||||
{% for f in available_floors %}
|
||||
<option value="{{ f }}" {% if floor_filter == f|string %}selected{% endif %}>
|
||||
{% if f == 0 %}Planta Baja{% else %}Piso {{ f }}{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Aula Filter -->
|
||||
<div class="col-lg-3 col-md-4">
|
||||
<label class="form-label small fw-bold text-muted mb-1">
|
||||
<i class="bi bi-door-open text-primary me-1"></i>Aula / Espacio
|
||||
</label>
|
||||
<select name="classroom_id" class="form-select form-select-sm">
|
||||
<option value="">Todas las aulas</option>
|
||||
{% for c in all_classrooms %}
|
||||
<option value="{{ c.id }}" {% if classroom_id == c.id %}selected{% endif %}>{{ c.building_name }} - {{ c.code }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Shift Filter -->
|
||||
<div class="col-lg-2 col-md-3">
|
||||
<label class="form-label small fw-bold text-muted mb-1">
|
||||
<i class="bi bi-brightness-high text-warning me-1"></i>Turno
|
||||
</label>
|
||||
<select name="shift" class="form-select form-select-sm">
|
||||
<option value="">Todos los turnos</option>
|
||||
<option value="Mañana" {% if shift_filter == 'Mañana' %}selected{% endif %}>Turno Mañana</option>
|
||||
<option value="Tarde" {% if shift_filter == 'Tarde' %}selected{% endif %}>Turno Tarde</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Date Range From -->
|
||||
<div class="col-lg-2 col-md-3">
|
||||
<label class="form-label small fw-bold text-muted mb-1">
|
||||
<i class="bi bi-calendar-event me-1"></i>Desde Fecha
|
||||
</label>
|
||||
<input type="date" name="date_from" class="form-control form-control-sm" value="{{ date_from or '' }}">
|
||||
</div>
|
||||
|
||||
<!-- Date Range To -->
|
||||
<div class="col-lg-2 col-md-3">
|
||||
<label class="form-label small fw-bold text-muted mb-1">
|
||||
<i class="bi bi-calendar-event me-1"></i>Hasta Fecha
|
||||
</label>
|
||||
<input type="date" name="date_to" class="form-control form-control-sm" value="{{ date_to or '' }}">
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Hide Virtual, Submit, Clear -->
|
||||
<div class="col-lg-6 col-md-6 pt-2">
|
||||
<div class="form-check form-switch pt-1">
|
||||
<input class="form-check-input" type="checkbox" role="switch" name="hide_virtual" value="1" id="hideVirtualList" {% if hide_virtual %}checked{% endif %}>
|
||||
<label class="form-check-label fw-semibold small text-dark" for="hideVirtualList">
|
||||
<i class="bi bi-laptop me-1 text-primary"></i>Ocultar Aulas Virtuales
|
||||
<span class="badge bg-secondary-subtle text-secondary border ms-1" style="font-size: 0.72rem;">Capacidad ilimitada</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-md-6 pt-2 text-md-end d-flex justify-content-md-end gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm px-3">
|
||||
<i class="bi bi-funnel me-1"></i>Filtrar
|
||||
</button>
|
||||
<a href="/schedule/list_reservations" class="btn btn-outline-secondary btn-sm px-3">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>Limpiar
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header de Resumen y Selector de Vista -->
|
||||
{% if reservations and reservations.items %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-calendar-check text-primary me-1"></i>
|
||||
<span>Total: <strong class="text-body">{{ reservations.total }}</strong> reservas encontradas</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="small text-muted d-none d-sm-inline">Modo de visualización:</span>
|
||||
{% set context_id = 'reservations' %}
|
||||
{% set default_view = 'table' %}
|
||||
{% include 'partials/view_switcher.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. VISTA DE TABLA / GRILLA -->
|
||||
<div data-view-context="reservations" data-view-container="table">
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<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-table text-primary me-2"></i>Registros en Formato Tabla
|
||||
</h5>
|
||||
<small class="text-muted">Página {{ reservations.page }} de {{ reservations.pages }}</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light small text-uppercase">
|
||||
<tr>
|
||||
<th class="ps-3">Fecha y Horario</th>
|
||||
<th>Aula / Piso</th>
|
||||
<th>Asignatura / Código</th>
|
||||
<th>Turno / Comisión</th>
|
||||
<th>Capacidad</th>
|
||||
<th>Estado</th>
|
||||
<th class="text-end pe-3">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for reservation in reservations.items %}
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<div class="fw-bold">{{ reservation.start_time }}</div>
|
||||
<small class="text-muted font-monospace">{{ reservation.start_time }} - {{ reservation.end_time }} hs</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary-subtle text-secondary border font-monospace">
|
||||
{{ reservation.classroom.code if reservation.classroom else 'Sin Aula' }}
|
||||
</span>
|
||||
<small class="text-muted d-block">
|
||||
{% if reservation.classroom and reservation.classroom.floor == 0 %}PB{% elif reservation.classroom %}Piso {{ reservation.classroom.floor }}{% endif %}
|
||||
{% if reservation.classroom and reservation.classroom.building %} · {{ reservation.classroom.building }}{% endif %}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold text-primary">{{ reservation.subject_name }}</div>
|
||||
<small class="badge bg-secondary-subtle text-secondary font-monospace">{{ reservation.subject_code }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info-subtle text-info border me-1">
|
||||
{{ reservation.shift or 'Tarde' }}
|
||||
</span>
|
||||
<small class="text-muted">{{ reservation.commission.code if reservation.commission else 'C1' }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="small">{{ reservation.expected_attendees }} / {{ reservation.classroom.capacity if reservation.classroom else '-' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if reservation.status == 'CONFIRMED' or reservation.status.value == 'CONFIRMED' %}
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">
|
||||
<i class="bi bi-check-circle me-1"></i>Confirmada
|
||||
</span>
|
||||
{% elif reservation.status == 'PENDING' or reservation.status.value == 'PENDING' %}
|
||||
<span class="badge bg-warning-subtle text-warning border border-warning-subtle">
|
||||
<i class="bi bi-clock me-1"></i>Pendiente
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger-subtle text-danger border border-danger-subtle">
|
||||
Cancelada
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
{% if reservation.effective_virtual_link %}
|
||||
<a href="{{ reservation.effective_virtual_link }}" target="_blank"
|
||||
class="btn btn-sm btn-outline-success py-0 px-2 me-1" title="Unirse a Clase Virtual (Zoom/Meet)">
|
||||
<i class="bi bi-camera-video"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user.can_edit_reservation(reservation) %}
|
||||
<a href="/schedule/edit_reservation"
|
||||
class="btn btn-sm btn-outline-warning py-0 px-2 me-1" title="Modificar Reserva">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/schedule/view_reservation"
|
||||
class="btn btn-sm btn-outline-secondary py-0 px-2" title="Ver Ficha">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
{% if user.has_permission('reservations', 'read_write') and (reservation.status == 'PENDING' or reservation.status.value == 'PENDING') %}
|
||||
<a href="/schedule/confirm_reservation"
|
||||
class="btn btn-sm btn-outline-success py-0 px-2 ms-1" title="Confirmar">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. VISTA DE TARJETAS / MOSAICO -->
|
||||
<div data-view-context="reservations" data-view-container="cards" class="d-none">
|
||||
<div class="row g-3 mb-3">
|
||||
{% for reservation in reservations.items %}
|
||||
<div class="col-xxl-3 col-xl-4 col-lg-6 col-md-6 col-sm-12">
|
||||
<div class="card border shadow-sm h-100 reservation-card">
|
||||
<div class="card-body p-3 d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<!-- Header de tarjeta -->
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<span class="badge bg-primary text-white font-monospace mb-1">
|
||||
<i class="bi bi-calendar-event me-1"></i>{{ reservation.start_time }}
|
||||
</span>
|
||||
<div class="small fw-bold text-muted font-monospace">
|
||||
<i class="bi bi-clock me-1"></i>{{ reservation.start_time }} - {{ reservation.end_time }} hs
|
||||
</div>
|
||||
</div>
|
||||
{% if reservation.status == 'CONFIRMED' or reservation.status.value == 'CONFIRMED' %}
|
||||
<span class="badge rounded-pill bg-success-subtle text-success border border-success-subtle">Confirmada</span>
|
||||
{% elif reservation.status == 'PENDING' or reservation.status.value == 'PENDING' %}
|
||||
<span class="badge rounded-pill bg-warning-subtle text-warning border border-warning-subtle">Pendiente</span>
|
||||
{% else %}
|
||||
<span class="badge rounded-pill bg-danger-subtle text-danger border border-danger-subtle">Cancelada</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Asignatura y código -->
|
||||
<h6 class="card-title fw-bold text-primary mb-1 text-truncate" title="{{ reservation.subject_name }}">
|
||||
{{ reservation.subject_name }}
|
||||
</h6>
|
||||
<div class="d-flex flex-wrap gap-1 mb-2">
|
||||
<span class="badge bg-secondary-subtle text-secondary font-monospace" style="font-size: 0.7rem;">
|
||||
{{ reservation.subject_code }}
|
||||
</span>
|
||||
<span class="badge bg-info-subtle text-info border" style="font-size: 0.7rem;">
|
||||
{{ reservation.shift or 'Tarde' }}
|
||||
</span>
|
||||
{% if reservation.commission %}
|
||||
<span class="badge bg-body-tertiary text-body border" style="font-size: 0.7rem;">
|
||||
{{ reservation.commission.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Ubicación / Aula -->
|
||||
<div class="p-2 rounded bg-body-tertiary border mb-2 small">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<span class="text-muted"><i class="bi bi-door-open text-primary me-1"></i>Espacio:</span>
|
||||
{% if reservation.classroom %}
|
||||
<span class="fw-bold font-monospace text-body">{{ reservation.classroom.code }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">Sin asignar</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between small text-muted">
|
||||
<span>Sede: {{ reservation.classroom.building_name if reservation.classroom else '-' }}</span>
|
||||
<span>{{ reservation.classroom.floor_display if reservation.classroom else '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Docente y Asistentes -->
|
||||
<div class="small text-muted d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-truncate me-1" title="{{ reservation.user.name if reservation.user else 'No asignado' }}">
|
||||
<i class="bi bi-person me-1"></i>{{ reservation.user.name if reservation.user else 'Sin docente' }}
|
||||
</span>
|
||||
<span>
|
||||
<i class="bi bi-people me-1"></i>{{ reservation.expected_attendees }} alumnos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Acciones -->
|
||||
<div class="d-flex justify-content-end gap-1 pt-2 border-top">
|
||||
{% if reservation.effective_virtual_link %}
|
||||
<a href="{{ reservation.effective_virtual_link }}" target="_blank"
|
||||
class="btn btn-sm btn-outline-success py-1 px-2" title="Clase Virtual">
|
||||
<i class="bi bi-camera-video"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/schedule/view_reservation" class="btn btn-sm btn-outline-secondary py-1 px-2" title="Ver Ficha">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
{% if user.can_edit_reservation(reservation) %}
|
||||
<a href="/schedule/edit_reservation" class="btn btn-sm btn-outline-warning py-1 px-2" title="Modificar">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user.has_permission('reservations', 'read_write') and (reservation.status == 'PENDING' or reservation.status.value == 'PENDING') %}
|
||||
<a href="/schedule/confirm_reservation" class="btn btn-sm btn-outline-success py-1 px-2" title="Confirmar">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paginación Compartida (visible en ambas vistas) -->
|
||||
{% if reservations.pages > 1 %}
|
||||
<div class="card border-0 shadow-sm py-2 px-3 d-flex flex-row justify-content-between align-items-center mb-3">
|
||||
<small class="text-muted">Página {{ reservations.page }} de {{ reservations.pages }} ({{ reservations.total }} reservas)</small>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if reservations.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="/schedule/list_reservations">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for p in reservations.iter_pages(left_edge=2, left_current=2, right_current=2, right_edge=2) %}
|
||||
{% if p %}
|
||||
{% if p == reservations.page %}
|
||||
<li class="page-item active"><span class="page-link">{{ p }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="/schedule/list_reservations">{{ p }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if reservations.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="/schedule/list_reservations">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body text-center py-5">
|
||||
<i class="bi bi-calendar-x text-muted fs-1 d-block mb-3"></i>
|
||||
<h5 class="fw-bold">No se encontraron reservas</h5>
|
||||
<p class="text-muted small mb-3">No hay registros que coincidan con los criterios de búsqueda o filtros seleccionados.</p>
|
||||
<a href="/schedule/list_reservations" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>Restablecer Todos los Filtros
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,383 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Cartelera de Hoy - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4 align-items-center">
|
||||
<div class="col-md-7">
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-calendar-day text-info me-2"></i>Cartelera del Día
|
||||
<small class="text-muted ms-2 fs-6">{{ format_date(today, format='full')|capitalize }}</small>
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">Cronograma de clases y asignación de aulas activas para el día de hoy</p>
|
||||
</div>
|
||||
<div class="col-md-5 text-md-end mt-3 mt-md-0">
|
||||
{% if user.has_permission('reservations', 'read_write') %}
|
||||
<a href="/schedule/add_reservation" class="btn btn-success me-2">
|
||||
<i class="bi bi-plus-circle me-1"></i>Nueva Reserva
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/schedule/calendar_view" class="btn btn-outline-primary me-2">
|
||||
<i class="bi bi-calendar3 me-1"></i>Ver Calendario
|
||||
</a>
|
||||
<a href="/schedule/list_reservations" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-list me-1"></i>Todas las Reservas
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats Cards with Shortcuts -->
|
||||
<div class="row mb-4 g-3">
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="text-muted small fw-semibold text-uppercase">Total de Reservas</span>
|
||||
<h2 class="mb-0 fw-bold mt-1 text-info">{{ stats.total_reservations }}</h2>
|
||||
</div>
|
||||
<div class="bg-info bg-opacity-10 rounded-circle p-3 text-info">
|
||||
<i class="bi bi-calendar3 fs-3"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="text-muted small fw-semibold text-uppercase">Confirmadas</span>
|
||||
<h2 class="mb-0 fw-bold mt-1 text-success">{{ stats.confirmed }}</h2>
|
||||
</div>
|
||||
<div class="bg-success bg-opacity-10 rounded-circle p-3 text-success">
|
||||
<i class="bi bi-check-circle fs-3"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="text-muted small fw-semibold text-uppercase">Pendientes</span>
|
||||
<h2 class="mb-0 fw-bold mt-1 text-warning">{{ stats.pending }}</h2>
|
||||
</div>
|
||||
<div class="bg-warning bg-opacity-10 rounded-circle p-3 text-warning">
|
||||
<i class="bi bi-clock fs-3"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="text-muted small fw-semibold text-uppercase">Aulas Ocupadas</span>
|
||||
<h2 class="mb-0 fw-bold mt-1 text-primary">{{ stats.occupied_classrooms }}/{{ stats.total_classrooms }}</h2>
|
||||
</div>
|
||||
<div class="bg-primary bg-opacity-10 rounded-circle p-3 text-primary">
|
||||
<i class="bi bi-door-open fs-3"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Toolbar (Piso, Turno y Ocultar Virtuales) -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-3">
|
||||
<form method="GET" action="/schedule/today_schedule" class="row g-2 align-items-center">
|
||||
<!-- Filter by Floor -->
|
||||
<div class="col-lg-3 col-md-4">
|
||||
<label for="floorSelect" class="form-label small fw-bold text-muted mb-1">
|
||||
<i class="bi bi-layers text-secondary me-1"></i>Piso / Planta
|
||||
</label>
|
||||
<select name="floor" id="floorSelect" class="form-select form-select-sm">
|
||||
<option value="">Todos los pisos</option>
|
||||
{% for f in available_floors %}
|
||||
<option value="{{ f }}" {% if floor_filter == f|string %}selected{% endif %}>
|
||||
{% if f == 0 %}Planta Baja (Piso 0){% else %}Piso {{ f }}{% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Filter by Turno -->
|
||||
<div class="col-lg-3 col-md-4">
|
||||
<label for="shiftSelect" class="form-label small fw-bold text-muted mb-1">
|
||||
<i class="bi bi-brightness-high text-warning me-1"></i>Turno
|
||||
</label>
|
||||
<select name="shift" id="shiftSelect" class="form-select form-select-sm">
|
||||
<option value="">Todos los turnos</option>
|
||||
<option value="Mañana" {% if shift_filter == 'Mañana' %}selected{% endif %}>Turno Mañana</option>
|
||||
<option value="Tarde" {% if shift_filter == 'Tarde' %}selected{% endif %}>Turno Tarde / Vespertino</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Hide Virtual Classrooms Toggle -->
|
||||
<div class="col-lg-4 col-md-4 pt-md-3">
|
||||
<div class="form-check form-switch pt-1">
|
||||
<input class="form-check-input" type="checkbox" role="switch" name="hide_virtual" value="1" id="hideVirtualToday" {% if hide_virtual %}checked{% endif %} onchange="this.form.submit()">
|
||||
<label class="form-check-label fw-semibold small text-dark" for="hideVirtualToday">
|
||||
<i class="bi bi-laptop me-1 text-primary"></i>Ocultar Aulas Virtuales
|
||||
<span class="badge bg-secondary-subtle text-secondary border ms-1" style="font-size: 0.72rem;">Capacidad ilimitada</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="col-lg-2 col-md-12 pt-md-3 text-lg-end d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm flex-fill">
|
||||
<i class="bi bi-funnel me-1"></i>Filtrar
|
||||
</button>
|
||||
<a href="/schedule/today_schedule" class="btn btn-outline-secondary btn-sm" title="Limpiar Filtros">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Today's Schedule -->
|
||||
{% if time_blocks %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header border-0 py-3 bg-body-tertiary">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<h5 class="mb-0 h6 fw-bold">
|
||||
<i class="bi bi-clock-history text-info me-2"></i>Vista Cronológica por Horario
|
||||
</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary" id="refresh-btn">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Actualizar
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="toggle-view-btn">
|
||||
<i class="bi bi-grid me-1"></i>Vista en Tabla
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<!-- Timeline View -->
|
||||
<div id="timeline-view">
|
||||
{% for hour in range(7, 22) %}
|
||||
<div class="row border-bottom py-3 align-items-start {% if time_blocks[hour] %}bg-light-subtle{% endif %}">
|
||||
<div class="col-md-2 col-3">
|
||||
<div class="text-center p-2 rounded bg-body-tertiary border">
|
||||
<strong class="h5 text-primary mb-0 font-monospace">{% if hour < 10 %}0{% endif %}{{ hour }}:00</strong>
|
||||
<div class="small text-muted font-monospace">{{ hour+1 }}:00 hs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-10 col-9">
|
||||
{% if time_blocks[hour] %}
|
||||
<div class="row g-2">
|
||||
{% for item in time_blocks[hour] %}
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100 border-start border-4 border-primary">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<h6 class="card-title fw-bold mb-1 text-primary">
|
||||
{{ item.reservation.subject_name }}
|
||||
</h6>
|
||||
<span class="badge bg-secondary-subtle text-secondary font-monospace border me-1">
|
||||
{{ item.reservation.subject_code }}
|
||||
</span>
|
||||
<span class="badge bg-info-subtle text-info border">
|
||||
{{ item.reservation.shift or 'Tarde' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="badge bg-{% if item.reservation.status == 'CONFIRMED' or item.reservation.status.value == 'CONFIRMED' %}success-subtle text-success border border-success-subtle{% else %}warning-subtle text-warning border border-warning-subtle{% endif %}">
|
||||
{% if item.reservation.status == 'CONFIRMED' or item.reservation.status.value == 'CONFIRMED' %}
|
||||
Confirmada
|
||||
{% else %}
|
||||
Pendiente
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="small text-muted mb-2">
|
||||
<span class="fw-semibold text-dark me-2">
|
||||
<i class="bi bi-door-open me-1 text-success"></i>{{ item.classroom.code }}
|
||||
</span>
|
||||
<span class="me-2">
|
||||
<i class="bi bi-layers me-1"></i>{% if item.classroom.floor == 0 %}PB{% else %}Piso {{ item.classroom.floor }}{% endif %}
|
||||
</span>
|
||||
<span>
|
||||
<i class="bi bi-clock me-1 text-warning"></i>{{ item.reservation.start_time }} - {{ item.reservation.end_time }} hs
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-2 border-top">
|
||||
<span class="small text-muted">
|
||||
<i class="bi bi-people me-1"></i>{{ item.reservation.expected_attendees }} estudiantes
|
||||
</span>
|
||||
<div class="d-flex gap-1">
|
||||
{% if item.reservation.effective_virtual_link %}
|
||||
<a href="{{ item.reservation.effective_virtual_link }}" target="_blank" class="btn btn-sm btn-outline-success py-0 px-2 d-inline-flex align-items-center" title="Unirse a Videollamada (Zoom / Meet)">
|
||||
<i class="bi bi-camera-video me-1"></i>Zoom/Meet
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user.can_edit_reservation(item.reservation) %}
|
||||
<a href="/schedule/edit_reservation" class="btn btn-sm btn-outline-warning py-0 px-2" title="Modificar Reserva">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/schedule/view_reservation" class="btn btn-sm btn-outline-secondary py-0 px-2" title="Ver Ficha Detallada">
|
||||
<i class="bi bi-eye me-1"></i>Detalles
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted py-2 small ps-2">
|
||||
<i class="bi bi-dash-circle me-1"></i>Sin clases programadas en este horario
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Table / Grid View (hidden by default) -->
|
||||
<div id="grid-view" style="display: none;">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light small text-uppercase">
|
||||
<tr>
|
||||
<th class="ps-3">Horario</th>
|
||||
<th>Aula / Piso</th>
|
||||
<th>Asignatura / Código</th>
|
||||
<th>Turno</th>
|
||||
<th>Asistentes</th>
|
||||
<th>Estado</th>
|
||||
<th class="text-end pe-3">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for reservation in reservations %}
|
||||
<tr>
|
||||
<td class="ps-3 font-monospace fw-bold">
|
||||
{{ reservation.start_time }} - {{ reservation.end_time }} hs
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary-subtle text-secondary font-monospace border">
|
||||
{{ reservation.classroom.code if reservation.classroom else 'Sin Aula' }}
|
||||
</span>
|
||||
<small class="text-muted d-block">
|
||||
{% if reservation.classroom and reservation.classroom.floor == 0 %}Planta Baja{% elif reservation.classroom %}Piso {{ reservation.classroom.floor }}{% endif %}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold text-primary">{{ reservation.subject_name }}</div>
|
||||
<small class="text-muted font-monospace">{{ reservation.subject_code }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info-subtle text-info border">
|
||||
{{ reservation.shift or 'Tarde' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="small">{{ reservation.expected_attendees }} estudiantes</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if reservation.status == 'CONFIRMED' or reservation.status.value == 'CONFIRMED' %}
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">
|
||||
<i class="bi bi-check-circle me-1"></i>Confirmada
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning-subtle text-warning border border-warning-subtle">
|
||||
<i class="bi bi-clock me-1"></i>Pendiente
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
{% if reservation.effective_virtual_link %}
|
||||
<a href="{{ reservation.effective_virtual_link }}" target="_blank"
|
||||
class="btn btn-sm btn-outline-success py-0 px-2 me-1" title="Unirse a Clase Virtual (Zoom/Meet)">
|
||||
<i class="bi bi-camera-video"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if user.can_edit_reservation(reservation) %}
|
||||
<a href="/schedule/edit_reservation"
|
||||
class="btn btn-sm btn-outline-warning py-0 px-2 me-1" title="Modificar Reserva">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/schedule/view_reservation"
|
||||
class="btn btn-sm btn-outline-secondary py-0 px-2" title="Ver Ficha">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-calendar-x text-muted" style="font-size: 4rem;"></i>
|
||||
<h4 class="mt-3 text-muted">No hay reservas programadas para hoy</h4>
|
||||
<p class="text-muted">
|
||||
No se encontraron clases para los filtros seleccionados.
|
||||
</p>
|
||||
<div class="mt-3">
|
||||
{% if user.has_permission('reservations', 'read_write') %}
|
||||
<a href="/schedule/add_reservation" class="btn btn-success">
|
||||
<i class="bi bi-plus-circle me-1"></i>Crear Primera Reserva
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/schedule/calendar_view" class="btn btn-outline-primary ms-2">
|
||||
<i class="bi bi-calendar-week me-1"></i>Ver Calendario Semanal
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var toggleBtn = document.getElementById('toggle-view-btn');
|
||||
var timelineView = document.getElementById('timeline-view');
|
||||
var gridView = document.getElementById('grid-view');
|
||||
|
||||
if (toggleBtn && timelineView && gridView) {
|
||||
toggleBtn.addEventListener('click', function() {
|
||||
if (timelineView.style.display === 'none') {
|
||||
timelineView.style.display = 'block';
|
||||
gridView.style.display = 'none';
|
||||
toggleBtn.innerHTML = '<i class="bi bi-grid me-1"></i>Vista en Tabla';
|
||||
} else {
|
||||
timelineView.style.display = 'none';
|
||||
gridView.style.display = 'block';
|
||||
toggleBtn.innerHTML = '<i class="bi bi-clock-history me-1"></i>Vista Cronológica';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var refreshBtn = document.getElementById('refresh-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,290 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Reservation Details - 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">
|
||||
<h1 class="h3">
|
||||
<i class="bi bi-calendar-check text-info"></i> Reservation Details
|
||||
</h1>
|
||||
<div class="d-flex gap-2">
|
||||
{% if user.can_edit_reservation(reservation) %}
|
||||
<a href="/schedule/edit_reservation" class="btn btn-warning">
|
||||
<i class="bi bi-pencil-square me-1"></i>Modificar Reserva
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/schedule/list_reservations" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Main Reservation Info -->
|
||||
<div class="col-lg-8 mb-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header border-0 py-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-info-circle text-primary"></i> Reservation Information
|
||||
</h5>
|
||||
<span class="badge bg-{% if reservation.status.value == 'CONFIRMED' %}success{% elif reservation.status.value == 'PENDING' %}warning{% else %}secondary{% endif %}">
|
||||
{{ reservation.status.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Reservation ID</label>
|
||||
<p class="mb-0"><strong>#{{ reservation.id }}</strong></p>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Created by</label>
|
||||
<p class="mb-0">
|
||||
<i class="bi bi-person"></i>
|
||||
{% if reservation.user %}
|
||||
{{ reservation.user.first_name }} {{ reservation.user.last_name }}
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Date & Time</label>
|
||||
<p class="mb-0">
|
||||
<i class="bi bi-clock"></i>
|
||||
{{ format_date(reservation.start_time, format='long')|capitalize }}<br>
|
||||
{{ reservation.start_time }} - {{ reservation.end_time }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Duration</label>
|
||||
<p class="mb-0">
|
||||
<i class="bi bi-hourglass"></i>
|
||||
{% set duration = reservation.duration or 120 %}
|
||||
{{ (duration / 60) | round(1) }} hours
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if reservation.purpose %}
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small">Purpose</label>
|
||||
<p class="mb-0">{{ reservation.purpose }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if reservation.notes %}
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small">Notes</label>
|
||||
<p class="mb-0">{{ reservation.notes }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Classroom Details -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header border-0 py-3 bg-body-tertiary">
|
||||
<h5 class="mb-0 h6 fw-bold">
|
||||
<i class="bi bi-geo-alt text-primary me-2"></i>Ubicación y Espacio Asignado
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{% if reservation.classroom %}
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="p-3 rounded bg-body-tertiary border">
|
||||
<small class="text-muted d-block mb-1">🏢 Edificio / Sede</small>
|
||||
<h6 class="mb-0 fw-bold text-primary">{{ reservation.classroom.building_name }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="p-3 rounded bg-body-tertiary border">
|
||||
<small class="text-muted d-block mb-1">🪜 Piso / Nivel</small>
|
||||
<h6 class="mb-0 fw-bold text-secondary">
|
||||
{% if reservation.classroom.is_virtual %}
|
||||
<span class="badge bg-primary-subtle text-primary border">Campus Virtual</span>
|
||||
{% else %}
|
||||
{{ reservation.classroom.floor_display }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="p-3 rounded bg-body-tertiary border">
|
||||
<small class="text-muted d-block mb-1">🚪 Aula</small>
|
||||
<h6 class="mb-0 fw-bold text-success">{{ reservation.classroom.code }}</h6>
|
||||
<small class="text-muted">Capacidad: <strong>{{ reservation.classroom.capacity_display }}</strong></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if reservation.classroom.description %}
|
||||
<div class="mt-2">
|
||||
<label class="text-muted small">Descripción del Aula</label>
|
||||
<p class="mb-0 small">{{ reservation.classroom.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Banner de Aula Virtual (Zoom, Meet, Teams) -->
|
||||
{% if reservation.effective_virtual_link or (reservation.classroom and reservation.classroom.is_virtual) %}
|
||||
<div class="mt-3 p-3 rounded bg-primary bg-opacity-10 border border-primary border-opacity-25 d-flex flex-column flex-sm-row justify-content-between align-items-sm-center gap-3">
|
||||
<div>
|
||||
<div class="fw-bold text-primary mb-1">
|
||||
<i class="bi bi-camera-video me-1"></i>Aula Virtual / Videoconferencia
|
||||
</div>
|
||||
{% if reservation.effective_virtual_link %}
|
||||
<small class="text-secondary text-break font-monospace d-block">{{ reservation.effective_virtual_link }}</small>
|
||||
{% else %}
|
||||
<small class="text-muted fst-italic">Modalidad remota sin enlace específico registrado</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if reservation.effective_virtual_link %}
|
||||
<a href="{{ reservation.effective_virtual_link }}" target="_blank" class="btn btn-success btn-sm px-3 text-nowrap">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>Unirse a Clase (Zoom / Meet)
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">Classroom information not available</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Subject & Commission -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header border-0 py-3">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-book text-warning"></i> Subject & Commission
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if reservation.commission %}
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small">Commission Code</label>
|
||||
<p class="mb-0"><strong>{{ reservation.commission.get_full_code() }}</strong></p>
|
||||
</div>
|
||||
{% if reservation.commission.subject %}
|
||||
<div class="mb-3">
|
||||
<label class="text-muted small">Subject</label>
|
||||
<p class="mb-0">{{ reservation.commission.subject.name }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">No commission assigned</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attendees -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header border-0 py-3">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-people text-success"></i> Attendance
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center">
|
||||
<h3 class="mb-1">{{ reservation.expected_attendees }}</h3>
|
||||
<p class="text-muted small">Expected Attendees</p>
|
||||
{% if reservation.classroom %}
|
||||
{% if reservation.classroom.capacity and reservation.classroom.capacity > 0 %}
|
||||
<div class="progress mt-3" style="height: 8px;">
|
||||
{% set percentage = (reservation.expected_attendees / reservation.classroom.capacity * 100) %}
|
||||
<div class="progress-bar bg-{% if percentage > 90 %}danger{% elif percentage > 75 %}warning{% else %}success{% endif %}"
|
||||
role="progressbar"
|
||||
style="{{ 'width: ' ~ ([percentage|round(1), 100]|min|string) ~ '%;' }}"></div>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{{ reservation.expected_attendees }} / {{ reservation.classroom.capacity }} capacidad
|
||||
</small>
|
||||
{% else %}
|
||||
<div class="mt-2">
|
||||
<span class="badge bg-primary-subtle text-primary border">
|
||||
{% if reservation.classroom.is_virtual %}🌐 Campus Virtual (Sin límite físico){% else %}Capacidad no definida{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header border-0 py-3">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-gear text-secondary"></i> Actions
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
{% if user.can_edit_reservation(reservation) %}
|
||||
<a href="/schedule/edit_reservation" class="btn btn-warning">
|
||||
<i class="bi bi-pencil-square me-1"></i>Modificar Reserva
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if reservation.status.value == 'PENDING' %}
|
||||
<form method="POST" action="/schedule/confirm_reservation">
|
||||
<button type="submit" class="btn btn-success" onclick="return confirm('Confirm this reservation?')">
|
||||
<i class="bi bi-check-circle"></i> Confirm Reservation
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if reservation.can_be_cancelled() %}
|
||||
<form method="POST" action="/schedule/cancel_reservation">
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Cancel this reservation?')">
|
||||
<i class="bi bi-x-circle"></i> Cancel Reservation
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<a href="/schedule/list_reservations" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-list"></i> View All Reservations
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timestamps -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="row text-muted small">
|
||||
<div class="col-md-4">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
Created: {{ reservation.created_at }}
|
||||
</div>
|
||||
{% if reservation.updated_at %}
|
||||
<div class="col-md-4">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
Updated: {{ reservation.updated_at }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-4">
|
||||
<i class="bi bi-info-square"></i>
|
||||
Reservation ID: #{{ reservation.id }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user