feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia

This commit is contained in:
2026-09-18 23:04:48 -03:00
parent 47b50ede7e
commit 1080acd43a
240 changed files with 8354 additions and 8886 deletions
+255
View File
@@ -0,0 +1,255 @@
{% extends "base.html" %}
{% block title %}Add Classroom - 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="/classrooms/list_classrooms">Classrooms</a></li>
<li class="breadcrumb-item active">Add Classroom</li>
</ol>
</nav>
<h1 class="h3 mb-0">
<i class="bi bi-plus-circle text-primary me-2"></i>Add Classroom
</h1>
</div>
<a href="/classrooms/list_classrooms" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Classrooms
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 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-door-open text-primary me-2"></i>Classroom Information
</h5>
</div>
<div class="card-body p-4">
<form method="POST" id="classroom-form" novalidate>
<div class="row g-3 mb-3">
<!-- Selector de Edificio / Sede -->
<div class="col-md-6">
<label for="building_id" class="form-label fw-semibold">
<i class="bi bi-buildings text-primary me-1"></i>Edificio / Sede <span class="text-danger">*</span>
</label>
{{ form.building_id(class="form-select", id="building_id") }}
{% if form.building_id.errors %}
<div class="text-danger small mt-1">
{{ form.building_id.errors[0] }}
</div>
{% endif %}
<!-- Input emergente si selecciona "+ Crear nuevo edificio" -->
<div id="newBuildingContainer" class="mt-2 p-2 rounded bg-body-tertiary border border-success-subtle" style="display: none;">
<label for="new_building_name" class="form-label small fw-bold text-success mb-1">
<i class="bi bi-plus-circle me-1"></i>Nombre del Nuevo Edificio / Sede:
</label>
{{ form.new_building_name(class="form-control form-control-sm", id="new_building_name", placeholder="ej. Anexo Belgrano, Pabellón III") }}
<div class="form-text small text-muted">Se registrará automáticamente en el catálogo de sedes.</div>
</div>
</div>
<!-- Selector / Input de Piso con sugerencias -->
<div class="col-md-3">
<label for="floor" class="form-label fw-semibold">
<i class="bi bi-layers text-secondary me-1"></i>Piso / Nivel <span class="text-danger">*</span>
</label>
{{ form.floor(class="form-control", id="floor", list="floorSuggestions", placeholder="ej. PB, 1, 2, 4°P") }}
<datalist id="floorSuggestions">
<!-- Poblado dinámicamente por JS según el edificio -->
</datalist>
<div class="form-text small text-muted">Sugerido o nuevo</div>
{% if form.floor.errors %}
<div class="text-danger small mt-1">
{{ form.floor.errors[0] }}
</div>
{% endif %}
</div>
<!-- Número / Nombre de Aula -->
<div class="col-md-3">
<label for="room_number" class="form-label fw-semibold">
<i class="bi bi-door-open text-info me-1"></i>Aula / Sala <span class="text-danger">*</span>
</label>
{{ form.room_number(class="form-control", id="room_number", placeholder="ej. Aula 304, Lab 1") }}
{% if form.room_number.errors %}
<div class="text-danger small mt-1">
{{ form.room_number.errors[0] }}
</div>
{% endif %}
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-1">
<label for="capacity" class="form-label fw-semibold mb-0">Capacidad de Alumnos <span class="text-danger">*</span></label>
<button type="button" class="btn btn-sm btn-outline-primary py-0 px-2" id="btnInfinityAdd" style="font-size: 0.8rem;" title="Asignar capacidad ilimitada">
<i class="bi bi-infinity me-1"></i>Ilimitada (∞)
</button>
</div>
<div class="input-group">
{{ form.capacity(class="form-control", id="capacity", placeholder="ej. 35 o ∞") }}
<span class="input-group-text bg-body-tertiary text-muted font-monospace"><i class="bi bi-people"></i></span>
</div>
<div class="form-text small text-muted mt-1">
<i class="bi bi-info-circle text-info me-1"></i>Para aulas virtuales o sin límite de aforo, ingrese <strong>∞</strong> o <strong>0</strong>.
</div>
{% if form.capacity.errors %}
<div class="text-danger small mt-1">
{{ form.capacity.errors[0] }}
</div>
{% endif %}
</div>
<div class="col-md-6 d-flex align-items-center pt-md-3">
<div class="form-check form-switch pt-2">
{{ form.is_active(class="form-check-input", id="is_active") }}
<label class="form-check-label fw-semibold" for="is_active">Aula Habilitada para Reservas</label>
</div>
</div>
</div>
<div class="mb-4">
{{ form.description.label(class="form-label fw-semibold") }}
{{ form.description(class="form-control", rows="3", placeholder=_("Opcional: Detalles adicionales de equipamiento, tomas de corriente, proyector...")) }}
{% if form.description.errors %}
<div class="text-danger small mt-1">
{{ form.description.errors[0] }}
</div>
{% endif %}
</div>
<!-- Preview Card -->
<div class="card bg-body-tertiary border-0 shadow-xs mb-4">
<div class="card-body py-3 px-4">
<h6 class="card-subtitle mb-2 text-muted small fw-bold">
<i class="bi bi-eye me-1"></i>Vista Previa de Ubicación
</h6>
<div class="row g-2">
<div class="col-md-4">
<small class="text-muted d-block">🏢 Edificio</small>
<strong id="preview-building" class="text-primary">-</strong>
</div>
<div class="col-md-4">
<small class="text-muted d-block">🪜 Piso</small>
<strong id="preview-floor" class="text-secondary">-</strong>
</div>
<div class="col-md-4">
<small class="text-muted d-block">🚪 Aula / Código</small>
<strong id="preview-room" class="text-success">-</strong>
</div>
</div>
</div>
</div>
<!-- Buttons -->
<div class="d-flex justify-content-between">
<a href="/classrooms/list_classrooms" class="btn btn-outline-secondary">
Cancelar
</a>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-check-circle me-1"></i>Crear Aula
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Map of existing floors per building
var buildingFloors = {{ building_floors|tojson }};
var buildingSelect = document.getElementById('building_id');
var newBuildingContainer = document.getElementById('newBuildingContainer');
var newBuildingNameInput = document.getElementById('new_building_name');
var floorInput = document.getElementById('floor');
var floorDatalist = document.getElementById('floorSuggestions');
var roomInput = document.getElementById('room_number');
var previewBuilding = document.getElementById('preview-building');
var previewFloor = document.getElementById('preview-floor');
var previewRoom = document.getElementById('preview-room');
function updateFloorsDatalist(bId) {
if (!floorDatalist) return;
floorDatalist.innerHTML = '';
var floors = buildingFloors[bId] || [];
floors.forEach(function(f) {
var opt = document.createElement('option');
opt.value = f;
opt.textContent = (f === '0' || f === 'PB') ? 'Planta Baja' : 'Piso ' + f;
floorDatalist.appendChild(opt);
});
}
function updatePreview() {
var bText = '-';
if (buildingSelect) {
if (buildingSelect.value === '__new__') {
bText = newBuildingNameInput.value.trim() || '{{ _("Nuevo Edificio") }}';
} else if (buildingSelect.selectedIndex >= 0) {
bText = buildingSelect.options[buildingSelect.selectedIndex].text;
}
}
var fText = floorInput ? (floorInput.value.trim() || '-') : '-';
var rText = roomInput ? (roomInput.value.trim() || '-') : '-';
if (previewBuilding) previewBuilding.textContent = bText;
if (previewFloor) previewFloor.textContent = (fText === '0' || fText.toUpperCase() === 'PB') ? 'Planta Baja' : fText;
if (previewRoom) previewRoom.textContent = rText;
}
if (buildingSelect) {
buildingSelect.addEventListener('change', function() {
if (this.value === '__new__') {
newBuildingContainer.style.display = 'block';
if (newBuildingNameInput) newBuildingNameInput.focus();
} else {
newBuildingContainer.style.display = 'none';
updateFloorsDatalist(this.value);
}
updatePreview();
});
// Initial call
if (buildingSelect.value === '__new__') {
newBuildingContainer.style.display = 'block';
} else if (buildingSelect.value) {
updateFloorsDatalist(buildingSelect.value);
}
}
if (newBuildingNameInput) newBuildingNameInput.addEventListener('input', updatePreview);
if (floorInput) floorInput.addEventListener('input', updatePreview);
if (roomInput) roomInput.addEventListener('input', updatePreview);
updatePreview();
// Infinity button
var btnInf = document.getElementById('btnInfinityAdd');
var capInput = document.getElementById('capacity');
if (btnInf && capInput) {
btnInf.addEventListener('click', function() {
capInput.value = '∞';
capInput.focus();
});
}
});
</script>
{% endblock %}
+246
View File
@@ -0,0 +1,246 @@
{% extends "base.html" %}
{% block title %}Edit Classroom - {{ classroom.code_display }}{% 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">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1 small">
<li class="breadcrumb-item"><a href="/classrooms/list_classrooms">Classrooms</a></li>
<li class="breadcrumb-item"><a href="/classrooms/view_classroom">{{ classroom.code_display }}</a></li>
<li class="breadcrumb-item active">Edit</li>
</ol>
</nav>
<h1 class="h3 mb-1">
<i class="bi bi-pencil-square text-primary me-2"></i>Edit Classroom
</h1>
<p class="text-muted small mb-0">{{ classroom.location_display }}</p>
</div>
<div class="col-md-5 text-md-end mt-3 mt-md-0">
<a href="/classrooms/view_classroom" class="btn btn-outline-secondary me-2">
<i class="bi bi-eye me-1"></i>Ver Detalle
</a>
<a href="/classrooms/list_classrooms" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Volver al Listado
</a>
</div>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<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-door-open text-primary me-2"></i>Classroom Information
</h5>
<span class="badge bg-secondary-subtle text-secondary font-monospace border">ID #{{ classroom.id }}</span>
</div>
<div class="card-body p-4">
<form method="POST" id="classroom-form" novalidate>
<div class="row g-3 mb-3">
<!-- Edificio -->
<div class="col-md-6">
<label for="building_id" class="form-label fw-semibold">
<i class="bi bi-buildings text-primary me-1"></i>Edificio / Sede <span class="text-danger">*</span>
</label>
{{ form.building_id(class="form-select", id="building_id") }}
{% if form.building_id.errors %}
<div class="text-danger small mt-1">{{ form.building_id.errors[0] }}</div>
{% endif %}
<div id="newBuildingContainer" class="mt-2 p-2 rounded bg-body-tertiary border border-success-subtle" style="display: none;">
<label for="new_building_name" class="form-label small fw-bold text-success mb-1">
<i class="bi bi-plus-circle me-1"></i>Nombre del Nuevo Edificio:
</label>
{{ form.new_building_name(class="form-control form-control-sm", id="new_building_name", placeholder="ej. Pabellón III") }}
</div>
</div>
<!-- Piso -->
<div class="col-md-3">
<label for="floor" class="form-label fw-semibold">
<i class="bi bi-layers text-secondary me-1"></i>Piso / Nivel <span class="text-danger">*</span>
</label>
{{ form.floor(class="form-control", id="floor", list="floorSuggestions", placeholder="ej. PB, 1, 2, 4°P") }}
<datalist id="floorSuggestions">
<!-- sugerencias dinámicas -->
</datalist>
<div class="form-text small text-muted">Sugerido o nuevo</div>
{% if form.floor.errors %}
<div class="text-danger small mt-1">{{ form.floor.errors[0] }}</div>
{% endif %}
</div>
<!-- Aula -->
<div class="col-md-3">
<label for="room_number" class="form-label fw-semibold">
<i class="bi bi-door-open text-info me-1"></i>Aula / Sala <span class="text-danger">*</span>
</label>
{{ form.room_number(class="form-control", id="room_number", placeholder="ej. Aula 304, Lab 1") }}
{% if form.room_number.errors %}
<div class="text-danger small mt-1">{{ form.room_number.errors[0] }}</div>
{% endif %}
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-1">
<label for="capacityInput" class="form-label fw-semibold mb-0">Capacidad de Alumnos <span class="text-danger">*</span></label>
<button type="button" class="btn btn-sm btn-outline-primary py-0 px-2" id="btnInfinity" style="font-size: 0.8rem;" title="Asignar capacidad ilimitada">
<i class="bi bi-infinity me-1"></i>Ilimitada (∞)
</button>
</div>
<div class="input-group">
{{ form.capacity(class="form-control" + (" is-invalid" if form.capacity.errors else ""), id="capacityInput", placeholder="ej. 35 o ∞") }}
<span class="input-group-text bg-body-tertiary text-muted font-monospace"><i class="bi bi-people"></i></span>
</div>
<div class="form-text small text-muted mt-1">
<i class="bi bi-info-circle text-info me-1"></i>Para aulas virtuales o eventos sin aforo restringido, ingrese <strong>∞</strong> o <strong>0</strong>.
</div>
{% if form.capacity.errors %}
<div class="invalid-feedback d-block">{{ form.capacity.errors[0] }}</div>
{% endif %}
</div>
<div class="col-md-6 d-flex align-items-center pt-md-3">
<div class="form-check form-switch pt-2">
{{ form.is_active(class="form-check-input", id="is_active") }}
<label class="form-check-label fw-semibold" for="is_active">
Aula Habilitada para Reservas
</label>
</div>
</div>
</div>
<div class="mb-4">
{{ form.description.label(class="form-label fw-semibold") }}
{{ form.description(class="form-control" + (" is-invalid" if form.description.errors else ""), rows="3", placeholder="Opcional: Detalles adicionales del equipamiento...") }}
{% if form.description.errors %}
<div class="invalid-feedback">{{ form.description.errors[0] }}</div>
{% endif %}
</div>
<!-- Preview Card -->
<div class="card bg-body-tertiary border-0 shadow-xs mb-4">
<div class="card-body py-3 px-4">
<h6 class="card-subtitle mb-2 text-muted small fw-bold">
<i class="bi bi-eye me-1"></i>Vista Previa de Ubicación
</h6>
<div class="row g-2">
<div class="col-md-4">
<small class="text-muted d-block">🏢 Edificio</small>
<strong id="preview-building" class="text-primary">{{ classroom.building_name }}</strong>
</div>
<div class="col-md-4">
<small class="text-muted d-block">🪜 Piso</small>
<strong id="preview-floor" class="text-secondary">{{ classroom.floor_display }}</strong>
</div>
<div class="col-md-4">
<small class="text-muted d-block">🚪 Aula / Código</small>
<strong id="preview-room" class="text-success">{{ classroom.code }}</strong>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="/classrooms/view_classroom" class="btn btn-outline-secondary">
Cancelar
</a>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-check-circle me-1"></i>Guardar Cambios
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
var buildingFloors = {{ building_floors|tojson }};
var buildingSelect = document.getElementById('building_id');
var newBuildingContainer = document.getElementById('newBuildingContainer');
var newBuildingNameInput = document.getElementById('new_building_name');
var floorInput = document.getElementById('floor');
var floorDatalist = document.getElementById('floorSuggestions');
var roomInput = document.getElementById('room_number');
var previewBuilding = document.getElementById('preview-building');
var previewFloor = document.getElementById('preview-floor');
var previewRoom = document.getElementById('preview-room');
function updateFloorsDatalist(bId) {
if (!floorDatalist) return;
floorDatalist.innerHTML = '';
var floors = buildingFloors[bId] || [];
floors.forEach(function(f) {
var opt = document.createElement('option');
opt.value = f;
opt.textContent = (f === '0' || f === 'PB') ? 'Planta Baja' : 'Piso ' + f;
floorDatalist.appendChild(opt);
});
}
function updatePreview() {
var bText = '-';
if (buildingSelect) {
if (buildingSelect.value === '__new__') {
bText = newBuildingNameInput.value.trim() || '{{ _("Nuevo Edificio") }}';
} else if (buildingSelect.selectedIndex >= 0) {
bText = buildingSelect.options[buildingSelect.selectedIndex].text;
}
}
var fText = floorInput ? (floorInput.value.trim() || '-') : '-';
var rText = roomInput ? (roomInput.value.trim() || '-') : '-';
if (previewBuilding) previewBuilding.textContent = bText;
if (previewFloor) previewFloor.textContent = (fText === '0' || fText.toUpperCase() === 'PB') ? 'Planta Baja' : fText;
if (previewRoom) previewRoom.textContent = rText;
}
if (buildingSelect) {
buildingSelect.addEventListener('change', function() {
if (this.value === '__new__') {
newBuildingContainer.style.display = 'block';
if (newBuildingNameInput) newBuildingNameInput.focus();
} else {
newBuildingContainer.style.display = 'none';
updateFloorsDatalist(this.value);
}
updatePreview();
});
if (buildingSelect.value === '__new__') {
newBuildingContainer.style.display = 'block';
} else if (buildingSelect.value) {
updateFloorsDatalist(buildingSelect.value);
}
}
if (newBuildingNameInput) newBuildingNameInput.addEventListener('input', updatePreview);
if (floorInput) floorInput.addEventListener('input', updatePreview);
if (roomInput) roomInput.addEventListener('input', updatePreview);
updatePreview();
var btnInf = document.getElementById('btnInfinity');
var capInput = document.getElementById('capacityInput');
if (btnInf && capInput) {
btnInf.addEventListener('click', function() {
capInput.value = '∞';
capInput.focus();
});
}
});
</script>
{% endblock %}
+959
View File
@@ -0,0 +1,959 @@
{% extends "base.html" %}
{% block title %}Aulas y Espacios - Edu-Space Admin{% endblock %}
{% block extra_css %}
<style>
.custom-space-pills .nav-link {
font-weight: 600;
font-size: 0.9rem;
padding: 0.5rem 1.1rem;
border-radius: 0.5rem;
color: var(--bs-body-color);
background-color: var(--bs-tertiary-bg);
border: 1px solid var(--bs-border-color);
transition: all 0.15s ease;
}
.custom-space-pills .nav-link.active {
background-color: var(--bs-primary);
color: #fff !important;
border-color: var(--bs-primary);
box-shadow: 0 4px 12px rgba(180, 62, 142, 0.25);
}
.custom-space-pills .nav-link:hover:not(.active) {
background-color: var(--bs-secondary-bg);
transform: translateY(-1px);
}
.classroom-card, .virtual-room-card {
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
border-radius: 0.65rem;
}
.classroom-card:hover, .virtual-room-card:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.08) !important;
}
.virtual-banner {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(180, 62, 142, 0.08) 100%);
border: 1px solid rgba(99, 102, 241, 0.25) !important;
border-radius: 0.75rem;
}
[data-bs-theme="dark"] .virtual-banner {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.16) 0%, rgba(180, 62, 142, 0.16) 100%);
border: 1px solid rgba(99, 102, 241, 0.35) !important;
}
.virtual-stat-box {
background-color: var(--bs-body-bg);
border: 1px solid var(--bs-border-color);
border-radius: 0.5rem;
padding: 0.6rem 0.85rem;
transition: transform 0.15s ease, border-color 0.15s ease;
text-decoration: none !important;
display: block;
}
.virtual-stat-box:hover {
transform: translateY(-2px);
border-color: var(--bs-primary) !important;
}
.metric-pill {
padding: 0.35rem 0.2rem;
border-radius: 0.45rem;
transition: all 0.15s ease;
cursor: pointer;
line-height: 1.2;
}
.metric-pill:hover {
transform: translateY(-1px);
filter: brightness(1.06);
}
.metric-label {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
margin-bottom: 2px;
}
.metric-val {
font-size: 0.85rem;
font-weight: 700;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
/* Semantics light */
.metric-high {
background-color: #FEE2E2;
border-color: #FCA5A5 !important;
color: #991B1B !important;
}
.metric-medium {
background-color: #FEF3C7;
border-color: #FCD34D !important;
color: #92400E !important;
}
.metric-low {
background-color: #D1FAE5;
border-color: #6EE7B7 !important;
color: #065F46 !important;
}
.metric-zero {
background-color: rgba(148, 163, 184, 0.1);
border-color: rgba(148, 163, 184, 0.25) !important;
color: #64748B !important;
}
/* Semantics dark */
[data-bs-theme="dark"] .metric-high {
background-color: rgba(239, 68, 68, 0.18);
border-color: rgba(239, 68, 68, 0.4) !important;
color: #FCA5A5 !important;
}
[data-bs-theme="dark"] .metric-medium {
background-color: rgba(245, 158, 11, 0.18);
border-color: rgba(245, 158, 11, 0.4) !important;
color: #FCD34D !important;
}
[data-bs-theme="dark"] .metric-low {
background-color: rgba(16, 185, 129, 0.18);
border-color: rgba(16, 185, 129, 0.4) !important;
color: #6EE7B7 !important;
}
[data-bs-theme="dark"] .metric-zero {
background-color: rgba(148, 163, 184, 0.08);
border-color: rgba(148, 163, 184, 0.18) !important;
color: #94A3B8 !important;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<!-- 1. Encabezado y Selector de Entorno (Presencial vs Virtual) -->
<div class="row mb-3">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<h1 class="h3 mb-1">
<i class="bi bi-door-open text-primary me-2"></i>Aulas y Espacios de Aprendizaje
</h1>
<p class="text-muted small mb-0">Administre las aulas físicas de sedes presenciales y las aulas virtuales a demanda del Campus Virtual.</p>
</div>
<div>
{% if view_mode == 'virtual' %}
<a href="/schedule/add_reservation?is_virtual=1" class="btn btn-success shadow-sm">
<i class="bi bi-camera-video me-1"></i>Nueva Clase Virtual a Demanda
</a>
{% else %}
<a href="/classrooms/add_classroom" class="btn btn-primary shadow-sm">
<i class="bi bi-plus-circle me-1"></i>Agregar Aula Física
</a>
{% endif %}
</div>
</div>
<!-- Pestañas de Selección de Modalidad -->
<div class="mt-3">
<ul class="nav nav-pills custom-space-pills gap-2">
<li class="nav-item">
<a class="nav-link {% if view_mode == 'physical' %}active{% endif %}"
href="/classrooms/list_classrooms?view_mode=physical">
<i class="bi bi-buildings me-2"></i>Aulas Presenciales
<span class="badge rounded-pill ms-2 {% if view_mode == 'physical' %}bg-light text-primary{% else %}bg-secondary-subtle text-secondary border{% endif %}">
{{ physical_count }} aulas
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if view_mode == 'virtual' %}active{% endif %}"
href="/classrooms/list_classrooms?view_mode=virtual">
<i class="bi bi-globe2 me-2 text-info"></i>Campus Virtual &amp; Aulas a Demanda
<span class="badge rounded-pill ms-2 {% if view_mode == 'virtual' %}bg-light text-primary{% else %}bg-primary-subtle text-primary border{% endif %}">
<i class="bi bi-infinity me-1"></i>{{ virtual_count }} salas activas
</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<!-- ========================================================================= -->
<!-- VISTA 1: AULAS FÍSICAS (PRESENCIALES) -->
<!-- ========================================================================= -->
{% if view_mode == 'physical' %}
<!-- Filtros de Aulas Físicas -->
<div class="row mb-3">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body py-2">
<form method="GET" class="row g-2 align-items-center">
<input type="hidden" name="view_mode" value="physical">
<div class="col-md-3">
<div class="input-group input-group-sm">
<span class="input-group-text bg-body-tertiary"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control"
placeholder="Buscar aula, descripción..."
value="{{ search }}">
</div>
</div>
<!-- Filtro por Edificio Físico -->
<div class="col-md-3">
<div class="input-group input-group-sm">
<span class="input-group-text bg-body-tertiary"><i class="bi bi-buildings text-primary"></i></span>
<select name="building_id" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">Todos los edificios presenciales</option>
{% for b in buildings %}
<option value="{{ b.id }}" {% if building_filter == (b.id|string) %}selected{% endif %}>{{ b.name }}</option>
{% endfor %}
</select>
</div>
</div>
<!-- Filtro por Piso Real -->
<div class="col-md-2">
<div class="input-group input-group-sm">
<span class="input-group-text bg-body-tertiary"><i class="bi bi-layers text-secondary"></i></span>
<select name="floor" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">Todos los pisos</option>
{% for f in floors %}
<option value="{{ f }}" {% if floor_filter == (f|string) %}selected{% endif %}>
{% if f == '0' or f == 'PB' %}Planta Baja{% else %}Piso {{ f }}{% endif %}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-md-2">
<div class="form-check pt-1">
<input class="form-check-input" type="checkbox" name="active_only" value="true"
id="activeOnly" {% if active_only %}checked{% endif %} onchange="this.form.submit()">
<label class="form-check-label small" for="activeOnly">
Solo activas
</label>
</div>
</div>
<div class="col-md-2 text-md-end">
<button type="submit" class="btn btn-sm btn-primary me-1">
<i class="bi bi-filter"></i> Filtrar
</button>
<a href="/classrooms/list_classrooms" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-x-circle"></i> Limpiar
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Barra de Resumen y Selector de Vista -->
<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-door-open me-1"></i>
<span>Total: <strong class="text-body">{{ classrooms.total if classrooms else 0 }}</strong> aulas 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 = 'classrooms' %}
{% set default_view = 'cards' %}
{% include 'partials/view_switcher.html' %}
</div>
</div>
<!-- Grilla y Tabla de Aulas Físicas -->
{% if classrooms and classrooms.items %}
<!-- 1. VISTA DE TARJETAS / MOSAICO -->
<div data-view-context="classrooms" data-view-container="cards">
<div class="row g-3 mt-1">
{% for classroom in classrooms.items %}
<div class="col-xxl-3 col-xl-3 col-lg-4 col-md-6 col-sm-12 mb-2">
<div class="card classroom-card border shadow-sm h-100">
<div class="card-body p-3 d-flex flex-column justify-content-between">
<div>
<!-- Encabezado con Código y Estado -->
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="text-truncate me-2">
<h5 class="card-title mb-0 fw-bold text-primary text-truncate fs-6" title="{{ classroom.code }}">
<i class="bi bi-door-open me-1"></i>{{ classroom.code }}
</h5>
</div>
<span class="badge rounded-pill {% if classroom.is_active %}bg-success-subtle text-success border border-success-subtle{% else %}bg-secondary-subtle text-secondary border{% endif %}" style="font-size: 0.68rem;">
{% if classroom.is_active %}Activa{% else %}Inactiva{% endif %}
</span>
</div>
<!-- Ubicación: Edificio y Piso -->
<div class="d-flex flex-wrap gap-1 mb-2">
<span class="badge bg-primary-subtle text-primary border" style="font-size: 0.7rem;">
<i class="bi bi-buildings me-1"></i>{{ classroom.building_name }}
</span>
<span class="badge bg-secondary-subtle text-secondary border" style="font-size: 0.7rem;">
<i class="bi bi-layers me-1"></i>{{ classroom.floor_display }}
</span>
</div>
<!-- Capacidad y Recursos -->
<div class="d-flex justify-content-between align-items-center small py-1 px-2 bg-body-tertiary rounded mb-2" style="font-size: 0.75rem;">
<div>
<span class="text-muted">Capacidad:</span>
<strong class="text-body ms-1">{{ classroom.capacity }} asientos</strong>
</div>
<div>
<span class="text-muted"><i class="bi bi-tools me-1"></i>{{ classroom.resources|length if classroom.resources else 0 }}</span>
</div>
</div>
<!-- Barra de Métricas de Ocupación FÍSICA (%) -->
{% set metrics = (occupancy_metrics[classroom.id] if occupancy_metrics else null) or {'day_pct': 0, 'day_count': 0, 'week_pct': 0, 'week_count': 0, 'month_pct': 0, 'month_count': 0} %}
<div class="classroom-occupancy-bar p-2 rounded bg-body-tertiary border mb-2">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small fw-semibold text-muted" style="font-size: 0.72rem;">
<i class="bi bi-graph-up me-1 text-primary"></i>Ocupación
</span>
<span class="text-muted" style="font-size: 0.65rem;" title="Haz clic en cualquier métrica para ver las reservas de ese período">
<i class="bi bi-info-circle me-1"></i>Ver reservas
</span>
</div>
<div class="row g-1 text-center">
<!-- Día -->
<div class="col-4">
<a href="/schedule/list_reservations"
class="d-block p-1 text-decoration-none border metric-pill metric-{% if metrics.day_pct > 80 %}high{% elif metrics.day_pct > 40 %}medium{% elif metrics.day_pct > 0 %}low{% else %}zero{% endif %}"
title="Ocupación Hoy: {{ metrics.day_count }} clases registradas">
<div class="metric-label">Hoy</div>
<div class="metric-val">{{ metrics.day_pct }}%</div>
</a>
</div>
<!-- Semana -->
<div class="col-4">
<a href="/schedule/list_reservations"
class="d-block p-1 text-decoration-none border metric-pill metric-{% if metrics.week_pct > 80 %}high{% elif metrics.week_pct > 40 %}medium{% elif metrics.week_pct > 0 %}low{% else %}zero{% endif %}"
title="Ocupación Semana: {{ metrics.week_count }} clases registradas">
<div class="metric-label">Semana</div>
<div class="metric-val">{{ metrics.week_pct }}%</div>
</a>
</div>
<!-- Mes -->
<div class="col-4">
<a href="/schedule/list_reservations"
class="d-block p-1 text-decoration-none border metric-pill metric-{% if metrics.month_pct > 80 %}high{% elif metrics.month_pct > 40 %}medium{% elif metrics.month_pct > 0 %}low{% else %}zero{% endif %}"
title="Ocupación Mes: {{ metrics.month_count }} clases registradas">
<div class="metric-label">Mes</div>
<div class="metric-val">{{ metrics.month_pct }}%</div>
</a>
</div>
</div>
</div>
</div>
<!-- Footer con Botones Compactos -->
<div class="pt-2 border-top d-flex justify-content-between align-items-center">
<div class="btn-group btn-group-sm">
<a href="/classrooms/view_classroom"
class="btn btn-outline-primary py-0 px-2" title="Ver Ficha" style="font-size: 0.75rem;">
<i class="bi bi-eye me-1"></i>Ficha
</a>
<a href="/classrooms/edit_classroom"
class="btn btn-outline-secondary py-0 px-2" title="Editar Aula" style="font-size: 0.75rem;">
<i class="bi bi-pencil"></i>
</a>
</div>
<form method="POST" action="/classrooms/toggle_classroom" class="d-inline mb-0">
<button type="submit" class="btn btn-sm py-0 px-2 {% if classroom.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}"
title="{% if classroom.is_active %}Desactivar{% else %}Activar{% endif %}" style="font-size: 0.75rem;">
{% if classroom.is_active %}
<i class="bi bi-pause"></i>
{% else %}
<i class="bi bi-play"></i>
{% endif %}
</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- 2. VISTA DE TABLA / GRILLA (AULAS FÍSICAS) -->
<div data-view-context="classrooms" data-view-container="table" class="d-none">
<div class="card border-0 shadow-sm mt-1">
<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">Aula / Código</th>
<th>Edificio / Sede</th>
<th>Piso</th>
<th>Capacidad</th>
<th>Equipamiento</th>
<th>Ocupación (D / S / M)</th>
<th>Estado</th>
<th class="text-end pe-3">Acciones</th>
</tr>
</thead>
<tbody>
{% for classroom in classrooms.items %}
{% set metrics = (occupancy_metrics[classroom.id] if occupancy_metrics else null) or {'day_pct': 0, 'day_count': 0, 'week_pct': 0, 'week_count': 0, 'month_pct': 0, 'month_count': 0} %}
<tr>
<td class="ps-3">
<a href="/classrooms/view_classroom" class="fw-bold text-primary text-decoration-none">
<i class="bi bi-door-open me-1"></i>{{ classroom.code }}
</a>
{% if classroom.description %}
<small class="text-muted d-block text-truncate" style="max-width: 200px;">{{ classroom.description }}</small>
{% endif %}
</td>
<td>
<span class="badge bg-primary-subtle text-primary border">
<i class="bi bi-buildings me-1"></i>{{ classroom.building_name }}
</span>
</td>
<td>
<span class="badge bg-secondary-subtle text-secondary border">
{{ classroom.floor_display }}
</span>
</td>
<td>
<strong class="text-body">{{ classroom.capacity }}</strong> <span class="small text-muted">asientos</span>
</td>
<td>
<span class="badge bg-body-tertiary text-body border">
<i class="bi bi-tools me-1"></i>{{ classroom.resources|length if classroom.resources else 0 }} recursos
</span>
</td>
<td>
<div class="d-flex align-items-center gap-1">
<span class="badge border {% if metrics.day_pct >= 80 %}metric-high{% elif metrics.day_pct >= 40 %}metric-medium{% else %}metric-low{% endif %}" title="Ocupación Día">
D: {{ metrics.day_pct }}%
</span>
<span class="badge border {% if metrics.week_pct >= 80 %}metric-high{% elif metrics.week_pct >= 40 %}metric-medium{% else %}metric-low{% endif %}" title="Ocupación Semana">
S: {{ metrics.week_pct }}%
</span>
<span class="badge border {% if metrics.month_pct >= 80 %}metric-high{% elif metrics.month_pct >= 40 %}metric-medium{% else %}metric-low{% endif %}" title="Ocupación Mes">
M: {{ metrics.month_pct }}%
</span>
</div>
</td>
<td>
<span class="badge rounded-pill {% if classroom.is_active %}bg-success-subtle text-success border border-success-subtle{% else %}bg-secondary-subtle text-secondary border{% endif %}">
{% if classroom.is_active %}Activa{% else %}Inactiva{% endif %}
</span>
</td>
<td class="text-end pe-3">
<div class="btn-group btn-group-sm">
<a href="/classrooms/view_classroom" class="btn btn-outline-primary" title="Ver Detalles">
<i class="bi bi-eye"></i>
</a>
{% if user.has_permission('classrooms', 'read_write') %}
<a href="/classrooms/edit_classroom" class="btn btn-outline-secondary" title="Editar">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
<form method="POST" action="/classrooms/toggle_classroom" class="d-inline mb-0">
<button type="submit" class="btn btn-outline-{% if classroom.is_active %}warning{% else %}success{% endif %}"
title="{% if classroom.is_active %}Desactivar{% else %}Activar{% endif %}">
<i class="bi bi-{% if classroom.is_active %}pause{% else %}play{% endif %}"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Paginación Aulas Físicas -->
{% if classrooms.pages > 1 %}
<nav aria-label="Classroom pagination" class="mt-4">
<ul class="pagination pagination-sm justify-content-center">
{% if classrooms.has_prev %}
<li class="page-item">
<a class="page-link" href="/classrooms/list_classrooms">
Anterior
</a>
</li>
{% endif %}
{% for page_num in classrooms.iter_pages() %}
{% if page_num %}
{% if page_num != classrooms.page %}
<li class="page-item">
<a class="page-link" href="/classrooms/list_classrooms">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">…</span>
</li>
{% endif %}
{% endfor %}
{% if classrooms.has_next %}
<li class="page-item">
<a class="page-link" href="/classrooms/list_classrooms">
Siguiente
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="row mt-4">
<div class="col-12">
<div class="text-center py-5">
<i class="bi bi-door-open text-muted" style="font-size: 4rem;"></i>
<h4 class="mt-3 text-muted">No se encontraron aulas físicas</h4>
<p class="text-muted">
{% if search %}
Ningún aula coincide con los criterios de búsqueda.
{% else %}
No hay aulas físicas registradas en esta sede.
{% endif %}
</p>
<a href="/classrooms/add_classroom" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Agregar Primera Aula
</a>
</div>
</div>
</div>
{% endif %}
<!-- ========================================================================= -->
<!-- VISTA 2: CAMPUS VIRTUAL (AULAS A DEMANDA · INFINITAS) -->
<!-- ========================================================================= -->
{% else %}
<!-- Banner de Infraestructura Cloud y Concurrencia Ilimitada -->
<div class="card virtual-banner p-3 mb-4 shadow-sm">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-3">
<div>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-primary text-white px-2 py-1"><i class="bi bi-cloud-check-fill me-1"></i>Cloud Infrastructure</span>
<h5 class="mb-0 fw-bold text-primary">
Campus Virtual UniCABA · Aulas a Demanda
</h5>
</div>
<p class="small text-muted mb-0 mt-1">
A diferencia de los espacios físicos, las aulas virtuales se crean y escalan de manera <strong>infinita y concurrente</strong> según la necesidad pedagógica de cada comisión.
</p>
</div>
<div class="d-flex gap-2 align-items-center">
<span class="badge bg-success-subtle text-success border border-success-subtle px-3 py-2 fs-6">
<i class="bi bi-infinity me-1"></i>Capacidad Ilimitada
</span>
<a href="/schedule/add_reservation?is_virtual=1" class="btn btn-sm btn-primary">
<i class="bi bi-plus-lg me-1"></i>Programar Clase Virtual
</a>
</div>
</div>
<!-- Indicadores de Demanda Global -->
<div class="row g-2">
<div class="col-lg-3 col-sm-6">
<a href="/schedule/today_schedule" class="virtual-stat-box">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.7rem;">Sesiones Hoy</span>
<h4 class="mb-0 fw-bold text-primary">{{ global_virtual_metrics.today_count }}</h4>
</div>
<div class="text-primary fs-4"><i class="bi bi-calendar-event"></i></div>
</div>
<small class="text-muted d-block mt-1" style="font-size: 0.72rem;">Clases virtuales programadas hoy</small>
</a>
</div>
<div class="col-lg-3 col-sm-6">
<a href="/schedule/list_reservations" class="virtual-stat-box">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.7rem;">Sesiones Semana</span>
<h4 class="mb-0 fw-bold text-info">{{ global_virtual_metrics.week_count }}</h4>
</div>
<div class="text-info fs-4"><i class="bi bi-calendar-week"></i></div>
</div>
<small class="text-muted d-block mt-1" style="font-size: 0.72rem;">Clases virtuales esta semana</small>
</a>
</div>
<div class="col-lg-3 col-sm-6">
<a href="/schedule/list_reservations" class="virtual-stat-box">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.7rem;">Sesiones del Mes</span>
<h4 class="mb-0 fw-bold text-success">{{ global_virtual_metrics.month_count }}</h4>
</div>
<div class="text-success fs-4"><i class="bi bi-calendar3"></i></div>
</div>
<small class="text-muted d-block mt-1" style="font-size: 0.72rem;">Total clases remotas en el mes</small>
</a>
</div>
<div class="col-lg-3 col-sm-6">
<div class="virtual-stat-box">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.7rem;">Salas Activas</span>
<h4 class="mb-0 fw-bold text-warning">{{ global_virtual_metrics.commissions_count }}</h4>
</div>
<div class="text-warning fs-4"><i class="bi bi-diagram-3"></i></div>
</div>
<small class="text-muted d-block mt-1" style="font-size: 0.72rem;">Comisiones con entorno virtual</small>
</div>
</div>
</div>
</div>
<!-- Barra de Búsqueda de Salas Virtuales -->
<div class="row mb-3">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body py-2">
<form method="GET" class="row g-2 align-items-center">
<input type="hidden" name="view_mode" value="virtual">
<div class="col-md-6">
<div class="input-group input-group-sm">
<span class="input-group-text bg-body-tertiary"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control"
placeholder="Buscar sala virtual por asignatura, comisión o carrera..."
value="{{ search }}">
</div>
</div>
<div class="col-md-6 text-md-end">
<button type="submit" class="btn btn-sm btn-primary me-1">
<i class="bi bi-funnel me-1"></i>Buscar Salas
</button>
<a href="/classrooms/list_classrooms" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-counterclockwise me-1"></i>Limpiar
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Catálogo de Aulas Virtuales por Comisión -->
{% if virtual_rooms %}
<div class="row g-3">
{% for room in virtual_rooms %}
<div class="col-xxl-3 col-xl-3 col-lg-4 col-md-6 col-sm-12 mb-2">
<div class="card virtual-room-card border shadow-sm h-100">
<div class="card-body p-3 d-flex flex-column justify-content-between">
<div>
<!-- Encabezado de la Sala Virtual -->
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="text-truncate me-2">
<h5 class="card-title mb-0 fw-bold text-primary text-truncate fs-6" title="{{ room.subject_name }}">
<i class="bi bi-laptop me-1 text-info"></i>{{ room.subject_name }}
</h5>
</div>
<span class="badge rounded-pill bg-info-subtle text-info border border-info-subtle" style="font-size: 0.68rem;">
<i class="bi bi-infinity me-1"></i>A Demanda
</span>
</div>
<!-- Badges: Comisión, Carrera y Turno -->
<div class="d-flex flex-wrap gap-1 mb-2">
<span class="badge bg-primary-subtle text-primary border" style="font-size: 0.7rem;">
<i class="bi bi-diagram-3 me-1"></i>Com. {{ room.commission_code }}
</span>
{% if room.subject_code %}
<span class="badge bg-secondary-subtle text-secondary border" style="font-size: 0.7rem;">
{{ room.subject_code }}
</span>
{% endif %}
<span class="badge bg-light text-dark border" style="font-size: 0.7rem;">
{{ room.shift }}
</span>
</div>
<!-- Cupo e Información Pedagógica -->
<div class="d-flex justify-content-between align-items-center small py-1 px-2 bg-body-tertiary rounded mb-2" style="font-size: 0.75rem;">
<div>
<span class="text-muted">Alumnos:</span>
<strong class="text-body ms-1">{{ room.max_students }}</strong>
</div>
<div>
<span class="text-muted">Modalidad:</span>
<strong class="text-info ms-1">Remoto ∞</strong>
</div>
</div>
<!-- Botón de Videollamada (Zoom / Meet / Teams) -->
<div class="mb-2">
{% if room.virtual_link %}
<a href="{{ room.virtual_link }}" target="_blank"
class="btn btn-sm btn-outline-success w-100 py-1 d-flex align-items-center justify-content-center"
style="font-size: 0.75rem;" title="Abrir enlace de videollamada">
<i class="bi bi-camera-video-fill me-1"></i>Unirse a Videollamada
</a>
{% else %}
<div class="p-1 rounded bg-body-tertiary border text-center small text-muted" style="font-size: 0.72rem;">
<i class="bi bi-link-45deg me-1"></i>Enlace a demanda por clase
</div>
{% endif %}
</div>
<!-- Barra de Demanda de la Sala Virtual (Hoy, Semana, Mes) -->
<div class="p-2 rounded bg-body-tertiary border mb-2">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small fw-semibold text-muted" style="font-size: 0.72rem;">
<i class="bi bi-calendar3 me-1 text-primary"></i>Clases a Demanda
</span>
<span class="text-muted" style="font-size: 0.65rem;">
{{ room.total_count }} totales
</span>
</div>
<div class="row g-1 text-center">
<!-- Hoy -->
<div class="col-4">
<div class="p-1 border metric-pill {% if room.day_count > 0 %}metric-low{% else %}metric-zero{% endif %}"
data-bs-toggle="modal" data-bs-target="#modalPreviewVirtual{{ room.commission_id }}"
role="button" title="Ver vista previa de clases de hoy">
<div class="metric-label">Hoy</div>
<div class="metric-val">{{ room.day_count }} cl.</div>
</div>
</div>
<!-- Semana -->
<div class="col-4">
<div class="p-1 border metric-pill {% if room.week_count > 0 %}metric-medium{% else %}metric-zero{% endif %}"
data-bs-toggle="modal" data-bs-target="#modalPreviewVirtual{{ room.commission_id }}"
role="button" title="Ver vista previa de clases de la semana">
<div class="metric-label">Semana</div>
<div class="metric-val">{{ room.week_count }} cl.</div>
</div>
</div>
<!-- Mes -->
<div class="col-4">
<div class="p-1 border metric-pill {% if room.month_count > 0 %}metric-high{% else %}metric-zero{% endif %}"
data-bs-toggle="modal" data-bs-target="#modalPreviewVirtual{{ room.commission_id }}"
role="button" title="Ver vista previa de clases del mes">
<div class="metric-label">Mes</div>
<div class="metric-val">{{ room.month_count }} cl.</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer con Botones -->
<div class="pt-2 border-top d-flex justify-content-between align-items-center gap-1">
<button type="button" class="btn btn-sm btn-primary py-1 px-2 d-flex align-items-center"
data-bs-toggle="modal" data-bs-target="#modalPreviewVirtual{{ room.commission_id }}"
style="font-size: 0.75rem;" title="Vista previa y modificar sesiones">
<i class="bi bi-eye me-1"></i>Vista Previa
<span class="badge bg-white text-primary ms-1" style="font-size: 0.65rem;">{{ room.total_count }}</span>
</button>
<a href="/schedule/add_reservation?commission_id={{ room.commission_id }}&is_virtual=1"
class="btn btn-sm btn-outline-success py-1 px-2" style="font-size: 0.75rem;" title="Programar clase en esta sala virtual">
<i class="bi bi-plus-lg me-1"></i>Programar
</a>
</div>
</div>
</div>
<!-- Modal Vista Previa y Modificación de la Sala Virtual -->
<div class="modal fade text-start" id="modalPreviewVirtual{{ room.commission_id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title h6 fw-bold mb-1">
<i class="bi bi-laptop text-primary me-2"></i>{{ room.subject_name }}
</h5>
<div class="d-flex flex-wrap gap-1">
<span class="badge bg-primary-subtle text-primary border" style="font-size: 0.7rem;">
<i class="bi bi-diagram-3 me-1"></i>Comisión {{ room.commission_code }}
</span>
{% if room.subject_code %}
<span class="badge bg-secondary-subtle text-secondary border font-monospace" style="font-size: 0.7rem;">
{{ room.subject_code }}
</span>
{% endif %}
<span class="badge bg-info-subtle text-info border" style="font-size: 0.7rem;">
<i class="bi bi-infinity me-1"></i>A Demanda · Capacidad Ilimitada
</span>
<span class="badge bg-light text-dark border" style="font-size: 0.7rem;">
{{ room.shift }}
</span>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- 1. Enlace de Videollamada de la Comisión (Edición directa) -->
<div class="card bg-body-tertiary border mb-3">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-semibold small text-body">
<i class="bi bi-camera-video text-primary me-1"></i>Enlace de Videollamada (Zoom / Meet / Teams / Moodle)
</span>
{% if room.virtual_link %}
<a href="{{ room.virtual_link }}" target="_blank" class="btn btn-sm btn-success py-0 px-2" style="font-size: 0.75rem;">
<i class="bi bi-box-arrow-up-right me-1"></i>Abrir Reunión
</a>
{% endif %}
</div>
<form method="POST" action="/classrooms/update_virtual_link">
<div class="input-group input-group-sm">
<span class="input-group-text bg-body border"><i class="bi bi-link-45deg"></i></span>
<input type="url" name="virtual_link" class="form-control"
placeholder="Ingresar URL de Zoom, Google Meet, Teams, etc."
value="{{ room.virtual_link or '' }}">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Guardar Enlace
</button>
</div>
<small class="text-muted d-block mt-1" style="font-size: 0.7rem;">
Este enlace se aplicará a todas las clases y cursadas virtuales de esta comisión.
</small>
</form>
</div>
</div>
<!-- 2. Tabla de Clases / Sesiones Programadas -->
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="fw-bold mb-0 text-body small">
<i class="bi bi-calendar3 me-1 text-primary"></i>Sesiones Programadas en el Campus Virtual ({{ room.reservations|length }})
</h6>
<a href="/schedule/add_reservation?commission_id={{ room.commission_id }}&is_virtual=1" class="btn btn-sm btn-outline-success py-0 px-2" style="font-size: 0.72rem;">
<i class="bi bi-plus-lg me-1"></i>Nueva Clase
</a>
</div>
{% if room.reservations %}
<div class="table-responsive border rounded mb-2">
<table class="table table-sm table-hover align-middle mb-0 small" style="font-size: 0.78rem;">
<thead class="table-light">
<tr>
<th>#ID</th>
<th>Fecha y Horario</th>
<th>Turno / Propósito</th>
<th>Docente</th>
<th>Videollamada</th>
<th>Estado</th>
<th class="text-end">Acciones</th>
</tr>
</thead>
<tbody>
{% for r in room.reservations %}
<tr>
<td class="text-muted">#{{ r.id }}</td>
<td>
<span class="fw-bold text-body">{{ r.start_time }}</span>
<span class="text-muted d-block font-monospace" style="font-size: 0.72rem;">
{{ r.start_time }} - {{ r.end_time }} hs
</span>
</td>
<td>
<span class="badge bg-light text-dark border">{{ r.shift or 'Tarde' }}</span>
<small class="text-muted d-block text-truncate" style="max-width: 130px;">{{ r.purpose or 'Clase regular' }}</small>
</td>
<td>
<small class="text-body">{{ r.user.name if r.user else 'Docente Asignado' }}</small>
</td>
<td>
{% if r.effective_virtual_link %}
<a href="{{ r.effective_virtual_link }}" target="_blank" class="badge bg-success-subtle text-success border border-success-subtle text-decoration-none" title="{{ r.effective_virtual_link }}">
<i class="bi bi-camera-video me-1"></i>Link Activo
</a>
{% else %}
<span class="text-muted small">Hereda comisión</span>
{% endif %}
</td>
<td>
<span class="badge bg-success-subtle text-success border border-success-subtle" style="font-size: 0.68rem;">
{{ r.status }}
</span>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm">
<a href="/schedule/edit_reservation"
class="btn btn-sm btn-primary py-0 px-2" style="font-size: 0.72rem;"
title="Modificar Fecha, Horario, Aula o Detalles">
<i class="bi bi-pencil me-1"></i>Modificar
</a>
<a href="/schedule/view_reservation"
class="btn btn-sm btn-outline-secondary py-0 px-2" style="font-size: 0.72rem;"
title="Ver Ficha Completa">
<i class="bi bi-eye"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info py-2 small mb-2">
<i class="bi bi-info-circle me-1"></i>No hay clases programadas para esta sala virtual en el período actual.
</div>
{% endif %}
</div>
<div class="modal-footer d-flex justify-content-between">
<a href="/schedule/list_reservations"
class="btn btn-sm btn-outline-secondary">
<i class="bi bi-list-check me-1"></i>Ver en Listado de Reservas (Filtrado)
</a>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cerrar</button>
<a href="/schedule/add_reservation?commission_id={{ room.commission_id }}&is_virtual=1" class="btn btn-sm btn-success">
<i class="bi bi-plus-lg me-1"></i>Programar Nueva Clase
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="row mt-4">
<div class="col-12">
<div class="text-center py-5">
<i class="bi bi-globe text-muted" style="font-size: 4rem;"></i>
<h4 class="mt-3 text-muted">No se encontraron salas virtuales</h4>
<p class="text-muted">
{% if search %}
Ninguna sala virtual coincide con los términos de búsqueda.
{% else %}
No hay clases virtuales programadas en el Campus Virtual.
{% endif %}
</p>
<a href="/schedule/add_reservation?is_virtual=1" class="btn btn-primary">
<i class="bi bi-camera-video me-1"></i>Crear Primera Reserva Virtual
</a>
</div>
</div>
</div>
{% endif %}
{% endif %}
</div>
{% endblock %}
+173
View File
@@ -0,0 +1,173 @@
{% extends "base.html" %}
{% block title %}{{ classroom.code_display }} - Classroom Details - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<!-- Breadcrumb & Header -->
<div class="row mb-4 align-items-center">
<div class="col-md-7">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1 small">
<li class="breadcrumb-item"><a href="/classrooms/list_classrooms">Classrooms</a></li>
<li class="breadcrumb-item active">{{ classroom.code_display }}</li>
</ol>
</nav>
<h1 class="h3 mb-0 d-flex align-items-center flex-wrap gap-2">
<i class="bi bi-door-open text-primary"></i> {{ classroom.code_display }}
{% if classroom.is_active %}
<span class="badge bg-success-subtle text-success border border-success-subtle fs-6">
<i class="bi bi-check-circle me-1"></i>Active
</span>
{% else %}
<span class="badge bg-danger-subtle text-danger border border-danger-subtle fs-6">
<i class="bi bi-x-circle me-1"></i>Inactive
</span>
{% endif %}
</h1>
</div>
<div class="col-md-5 text-md-end mt-3 mt-md-0">
{% if user.has_permission('classrooms', 'read_write') %}
<a href="/classrooms/edit_classroom" class="btn btn-primary me-2">
<i class="bi bi-pencil me-1"></i>Edit Classroom
</a>
{% endif %}
<a href="/classrooms/list_classrooms" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Classrooms
</a>
</div>
</div>
<!-- Classroom Details Cards -->
<div class="row g-3 mb-4">
<div class="col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-body-tertiary border-0 py-3">
<h5 class="mb-0 h6 fw-bold">
<i class="bi bi-info-circle text-primary me-2"></i>Ficha Técnica
</h5>
</div>
<div class="card-body p-3">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<span class="text-muted"><i class="bi bi-building me-2"></i>Edificio / Sede</span>
<span class="fw-bold">
{% if classroom.building_entity %}
<a href="/buildings/view_building" class="text-decoration-none">
{{ classroom.building_entity.name }}
</a>
{% else %}
{{ classroom.building }}
{% endif %}
</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<span class="text-muted"><i class="bi bi-door-closed me-2"></i>Número / Nombre</span>
<span class="fw-bold font-monospace">{{ classroom.code }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<span class="text-muted"><i class="bi bi-layers me-2"></i>Piso / Planta</span>
<span class="fw-bold">{% if classroom.floor == 0 %}Planta Baja{% else %}Piso {{ classroom.floor }}{% endif %}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<span class="text-muted"><i class="bi bi-people me-2"></i>Capacidad Máxima</span>
{% if classroom.is_unlimited %}
<span class="badge bg-primary-subtle text-primary border px-2 py-1 fs-6">
<i class="bi bi-infinity me-1"></i>Ilimitada (∞)
</span>
{% else %}
<span class="fw-bold text-success fs-6">{{ classroom.capacity }} estudiantes</span>
{% endif %}
</li>
</ul>
{% if classroom.description %}
<div class="mt-3 pt-3 border-top">
<small class="text-muted fw-bold d-block mb-1">Descripción / Notas:</small>
<p class="small text-secondary mb-0">{{ classroom.description }}</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-8">
<!-- Upcoming Schedule / Reservations -->
<div class="card border-0 shadow-sm h-100">
<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-calendar-check text-primary me-2"></i>Próximas Clases y Reservas
</h5>
<a href="/schedule/calendar_view" class="btn btn-outline-primary btn-sm">
<i class="bi bi-calendar-week me-1"></i>Ver en Calendario
</a>
</div>
<div class="card-body p-0">
{% if upcoming_reservations %}
<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>Materia / Motivo</th>
<th>Comisión</th>
<th>Asistentes</th>
<th class="text-end pe-3">Acciones</th>
</tr>
</thead>
<tbody>
{% for res in upcoming_reservations %}
<tr>
<td class="ps-3">
<div class="fw-bold font-monospace text-primary fs-6 mb-1">
<i class="bi bi-calendar-event text-primary me-1"></i>{{ res.start_time }}
</div>
<div class="small font-monospace text-body-secondary">
<i class="bi bi-clock text-warning me-1"></i>{{ res.start_time }} - {{ res.end_time }} hs
</div>
</td>
<td>
<div class="fw-bold text-primary">{{ res.subject_name }}</div>
<small class="badge bg-secondary-subtle text-secondary font-monospace">{{ res.subject_code }}</small>
</td>
<td>
<span class="badge bg-info-subtle text-info border">
{{ res.commission.code if res.commission else 'S/C' }}
</span>
</td>
<td>
<span class="small">{{ res.expected_attendees }} / {{ classroom.capacity }}</span>
</td>
<td class="text-end pe-3">
{% if res.effective_virtual_link %}
<a href="{{ res.effective_virtual_link }}" target="_blank" class="btn btn-outline-success btn-sm py-0 px-2 me-1" title="Unirse a Clase Virtual (Zoom / Meet)">
<i class="bi bi-camera-video"></i>
</a>
{% endif %}
<a href="/schedule/view_reservation" class="btn btn-outline-secondary btn-sm py-0 px-2" title="Ver Ficha">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-calendar-x text-muted fs-1 mb-2"></i>
<h6 class="fw-bold">Sin reservas programadas próximamente</h6>
<p class="text-muted small mb-3">Esta aula no tiene clases asignadas en las próximas fechas.</p>
{% if user.has_permission('reservations', 'read_write') %}
<a href="/schedule/add_reservation" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle me-1"></i>Asignar Nueva Reserva
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}