feat: refactorizar espacios fisicos desacoplando Edificio, Piso y Aula con selectores en cascada y filtros

This commit is contained in:
Carlos Tello
2026-09-03 05:18:01 -03:00
parent 5e0b905e55
commit 6ccc9e8c87
11 changed files with 1093 additions and 522 deletions
+358 -139
View File
@@ -7,80 +7,131 @@
<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-plus text-success"></i> {% trans %}New Reservation{% endtrans %}
</h1>
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> {% trans %}Back to Reservations{% endtrans %}
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1 small">
<li class="breadcrumb-item"><a href="{{ url_for('schedule.today_schedule') }}">{% trans %}Schedule{% endtrans %}</a></li>
<li class="breadcrumb-item active">{% trans %}New Reservation{% endtrans %}</li>
</ol>
</nav>
<h1 class="h3 mb-0">
<i class="bi bi-calendar-plus text-primary me-2"></i>{% trans %}New Reservation{% endtrans %}
</h1>
</div>
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>{% trans %}Back to Reservations{% endtrans %}
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="col-lg-10 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header border-0 py-3">
<h5 class="mb-0">
<i class="bi bi-calendar3 text-success"></i> {% trans %}Reservation Details{% endtrans %}
<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>{% trans %}Detalles de la Reserva y Espacio Físico{% endtrans %}
</h5>
</div>
<div class="card-body">
<div class="card-body p-4">
<form method="POST" id="reservation-form" novalidate>
{{ form.hidden_tag() }}
<div class="row">
<div class="col-md-6 mb-3">
{{ form.classroom_id.label(class="form-label") }}
{{ form.classroom_id(class="form-select", id="classroom-select") }}
{% if form.classroom_id.errors %}
<div class="text-danger small mt-1">
{{ form.classroom_id.errors[0] }}
<!-- 1. SELECTORES EN CASCADA: Edificio -> Piso -> Aula -->
<div class="mb-4">
<div class="p-3 rounded bg-body-tertiary border">
<label class="form-label fw-bold text-primary mb-2">
<i class="bi bi-geo-alt-fill me-1"></i>{% trans %}Selección de Espacio Físico / Modalidad{% endtrans %} <span class="text-danger">*</span>
</label>
<div class="row g-3">
<!-- Edificio / Sede -->
<div class="col-md-4">
<label for="cascade-building" class="form-label small fw-semibold text-muted mb-1">
<i class="bi bi-buildings text-primary me-1"></i>1. {% trans %}Edificio / Sede{% endtrans %}
</label>
<select id="cascade-building" class="form-select form-select-sm">
<option value="">{% trans %}-- Selecciona Edificio --{% endtrans %}</option>
{% for b in buildings %}
<option value="{{ b.id }}" data-virtual="{{ 'true' if b.code == 'VIRTUAL' or 'VIRTUAL' in (b.name|upper) else 'false' }}">
{% if b.code == 'VIRTUAL' or 'VIRTUAL' in (b.name|upper) %}🌐{% else %}🏢{% endif %} {{ b.name }}
</option>
{% endfor %}
</select>
<div class="form-text small text-muted">{% trans %}Sede física o Campus Virtual{% endtrans %}</div>
</div>
{% endif %}
<small class="text-muted">{% trans %}Select a classroom for your reservation{% endtrans %}</small>
<!-- Piso / Nivel -->
<div class="col-md-4">
<label for="cascade-floor" class="form-label small fw-semibold text-muted mb-1">
<i class="bi bi-layers text-secondary me-1"></i>2. {% trans %}Piso / Nivel{% endtrans %}
</label>
<select id="cascade-floor" class="form-select form-select-sm" disabled>
<option value="">{% trans %}-- Selecciona Piso --{% endtrans %}</option>
</select>
<div class="form-text small text-muted" id="floorHelpText">{% trans %}Filtra por planta o nivel{% endtrans %}</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. {% trans %}Aula / Espacio{% endtrans %} <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">{% trans %}Capacidad y disponibilidad{% endtrans %}</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
{{ form.commission_id.label(class="form-label") }}
</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 %}
<small class="text-muted">{% trans %}Select the subject/commission{% endtrans %}</small>
<div class="form-text small text-muted">{% trans %}Materia, código de comisión y cantidad de alumnos inscriptos{% endtrans %}</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
{{ form.start_time.label(class="form-label") }}
<!-- 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">{% trans %}When will the class start?{% endtrans %}</small>
<small class="text-muted">{% trans %}¿Cuándo comenzará la clase?{% endtrans %}</small>
</div>
<div class="col-md-6 mb-3">
{{ form.end_time.label(class="form-label") }}
<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">{% trans %}When will the class end?{% endtrans %}</small>
<small class="text-muted">{% trans %}¿Cuándo finalizará la clase?{% endtrans %}</small>
</div>
</div>
<div class="row">
<div class="col-md-8 mb-3">
{{ form.purpose.label(class="form-label") }}
{{ form.purpose(class="form-control", placeholder=_("Regular class, exam, study session, etc.")) }}
<!-- 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] }}
@@ -88,21 +139,22 @@
{% endif %}
</div>
<div class="col-md-4 mb-3">
{{ form.expected_attendees.label(class="form-label") }}
{{ form.expected_attendees(class="form-control", placeholder="30") }}
<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">{% trans %}Number of expected attendees{% endtrans %}</small>
<small class="text-muted">{% trans %}Cantidad de asistentes esperados{% endtrans %}</small>
</div>
</div>
<!-- 5. Notas -->
<div class="mb-4">
{{ form.notes.label(class="form-label") }}
{{ form.notes(class="form-control", rows="3", placeholder=_("Any additional notes or special requirements...")) }}
{{ 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] }}
@@ -110,83 +162,82 @@
{% endif %}
</div>
<!-- Summary Section -->
<div class="alert alert-info" id="reservation-summary">
<h6 class="alert-heading">
<i class="bi bi-info-circle"></i> {% trans %}Reservation Summary{% endtrans %}
</h6>
<div id="summary-content">
<p class="mb-2">
<strong>{% trans %}Selected Classroom:{% endtrans %}</strong>
<span id="summary-classroom">{% trans %}Not selected{% endtrans %}</span>
</p>
<p class="mb-2">
<strong>{% trans %}Duration:{% endtrans %}</strong>
<span id="summary-duration">{% trans %}Not calculated{% endtrans %}</span>
</p>
<p class="mb-0">
<strong>{% trans %}Commission:{% endtrans %}</strong>
<span id="summary-commission">{% trans %}Not selected{% endtrans %}</span>
</p>
<!-- 6. TARJETA DE RESUMEN DESGLOSADA: Edificio, Piso, Aula -->
<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>{% trans %}Resumen de la Reserva{% endtrans %}
</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">🏢 {% trans %}Edificio / Sede{% endtrans %}</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">🪜 {% trans %}Piso / Nivel{% endtrans %}</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">🚪 {% trans %}Aula Seleccionada{% endtrans %}</small>
<strong id="summary-classroom" class="text-success">{% trans %}No seleccionada{% endtrans %}</strong>
</div>
</div>
</div>
<!-- Duración y Comisión -->
<div class="row g-2 small">
<div class="col-md-4">
<span class="text-muted">⏱️ {% trans %}Duración:{% endtrans %}</span>
<strong id="summary-duration" class="text-body ms-1">{% trans %}No calculada{% endtrans %}</strong>
</div>
<div class="col-md-8">
<span class="text-muted">📚 {% trans %}Comisión:{% endtrans %}</span>
<strong id="summary-commission" class="text-body ms-1">{% trans %}No seleccionada{% endtrans %}</strong>
</div>
</div>
</div>
</div>
<!-- Botones -->
<div class="d-flex justify-content-between">
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> {% trans %}Cancel{% endtrans %}
<i class="bi bi-x-circle me-1"></i>{% trans %}Cancelar{% endtrans %}
</a>
{{ form.submit(class="btn btn-success") }}
<button type="submit" class="btn btn-success px-4">
<i class="bi bi-calendar-check me-1"></i>{% trans %}Realizar Reserva{% endtrans %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Instructions Card -->
<div class="row mt-4">
<div class="col-lg-8 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header border-0 py-3">
<h5 class="mb-0">
<i class="bi bi-question-circle text-info"></i> {% trans %}Reservation Guidelines{% endtrans %}
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-primary">📅 {% trans %}Booking Rules{% endtrans %}</h6>
<ul class="small">
<li>{% trans %}Reservations can be made up to 3 months in advance{% endtrans %}</li>
<li>{% trans %}Cannot book in the past{% endtrans %}</li>
<li>{% trans %}Maximum duration: 8 hours per reservation{% endtrans %}</li>
<li>{% trans %}Cancellations must be 24 hours before start time{% endtrans %}</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-warning">🚨 {% trans %}Important Notes{% endtrans %}</h6>
<ul class="small">
<li>{% trans %}Check classroom capacity before booking{% endtrans %}</li>
<li>{% trans %}Pending reservations require approval{% endtrans %}</li>
<li>{% trans %}Time slots must not overlap{% endtrans %}</li>
<li>{% trans %}System automatically detects conflicts{% endtrans %}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script type="application/json" id="classroom-data">
{
{% for classroom in classroom_list %}
"{{ classroom.id }}": {
"capacity": {{ classroom.capacity_display|tojson }},
"code_display": {{ (classroom.building ~ ' - ' ~ classroom.code)|tojson }}
{% 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 %}
}
@@ -194,53 +245,215 @@
<script type="application/json" id="i18n-strings">
{
"not_selected": "{% trans %}Not selected{% endtrans %}",
"not_calculated": "{% trans %}Not calculated{% endtrans %}",
"invalid_time": "{% trans %}Invalid time range{% endtrans %}",
"hour": "{% trans %}hour{% endtrans %}",
"hours": "{% trans %}hours{% endtrans %}",
"minute": "{% trans %}minute{% endtrans %}",
"minutes": "{% trans %}minutes{% endtrans %}",
"capacity": "{% trans %}Capacity{% endtrans %}",
"capacity_exceeded": "{% trans %}Expected attendees exceeds classroom capacity{% endtrans %}"
"not_selected": "{% trans %}No seleccionada{% endtrans %}",
"not_calculated": "{% trans %}No calculada{% endtrans %}",
"invalid_time": "{% trans %}Rango horario inválido{% endtrans %}",
"hour": "{% trans %}hora{% endtrans %}",
"hours": "{% trans %}horas{% endtrans %}",
"minute": "{% trans %}minuto{% endtrans %}",
"minutes": "{% trans %}minutos{% endtrans %}",
"capacity": "{% trans %}Capacidad{% endtrans %}",
"capacity_exceeded": "{% trans %}Los asistentes esperados superan la capacidad del aula{% endtrans %}"
}
</script>
<script>
$(document).ready(function() {
// Classroom data for capacity checking
let classroomData = {};
try {
const rawData = document.getElementById('classroom-data')?.textContent;
if (rawData) {
classroomData = JSON.parse(rawData);
}
if (rawData) classroomData = JSON.parse(rawData);
} catch (err) {
console.error('Error parsing classroomData:', err);
}
// I18n strings
let i18n = {};
try {
const rawI18n = document.getElementById('i18n-strings')?.textContent;
if (rawI18n) {
i18n = JSON.parse(rawI18n);
}
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-select').val();
const classroomId = $classroom.val();
const commissionId = $('#commission-select').val();
const startTime = $('#start-time').val();
const endTime = $('#end-time').val();
// Update classroom summary
// Update classroom badges
if (classroomId && classroomData[classroomId]) {
const capLabel = i18n.capacity || 'Capacidad';
$('#summary-classroom').text(`${classroomData[classroomId].code_display} (${capLabel}: ${classroomData[classroomId].capacity})`);
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');
}
@@ -284,20 +497,28 @@ $(document).ready(function() {
// Check capacity vs expected attendees
function checkCapacity() {
const classroomId = $('#classroom-select').val();
const classroomId = $classroom.val();
const attendees = parseInt($('#expected_attendees').val()) || 0;
if (classroomId && classroomData[classroomId] && attendees > 0) {
const capacity = classroomData[classroomId].capacity;
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 > capacity) {
if (attendees > c.capacity_num) {
$('#expected_attendees').addClass('is-invalid');
const msg = i18n.capacity_exceeded || 'Los asistentes esperados superan la capacidad del aula';
$('#expected_attendees').after(`
<div class="invalid-feedback">
${msg} (${attendees} > ${capacity})
</div>
`);
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();
@@ -305,23 +526,23 @@ $(document).ready(function() {
}
}
// Update on change
$('#classroom-select, #commission-select, #start-time, #end-time, #expected_attendees').on('change', updateSummary);
$('#expected_attendees').on('input', checkCapacity);
$('#commission-select, #start-time, #end-time').on('change', updateSummary);
$('#expected_attendees').on('input', function() {
checkCapacity();
updateSummary();
});
// Initialize summary
updateSummary();
// Form validation
$('#reservation-form').on('submit', function(e) {
let isValid = true;
// Check required fields
if (!$('#classroom-select').val()) {
$('#classroom-select').addClass('is-invalid');
if (!$classroom.val()) {
$classroom.addClass('is-invalid');
isValid = false;
} else {
$('#classroom-select').removeClass('is-invalid');
$classroom.removeClass('is-invalid');
}
if (!$('#commission-select').val()) {
@@ -345,7 +566,6 @@ $(document).ready(function() {
$('#end-time').removeClass('is-invalid');
}
// Validate time range
const startTime = new Date($('#start-time').val());
const endTime = new Date($('#end-time').val());
@@ -354,7 +574,6 @@ $(document).ready(function() {
isValid = false;
}
// Check capacity
checkCapacity();
if ($('#expected_attendees').hasClass('is-invalid')) {
isValid = false;
+52 -19
View File
@@ -80,8 +80,21 @@
</div>
</div>
<!-- Second row of filters: Floor & Hide Virtual -->
<!-- Second row of filters: Building, Floor & Hide Virtual -->
<div class="row g-2 align-items-center mt-2 pt-2 border-top">
<!-- Filter by Building -->
<div class="col-lg-3 col-md-4">
<label for="filterBuilding" class="form-label small fw-bold text-muted mb-1">
<i class="bi bi-buildings text-primary me-1"></i>{% trans %}Filtrar por Edificio{% endtrans %}
</label>
<select id="filterBuilding" class="form-select form-select-sm">
<option value="">{% trans %}Todos los edificios{% endtrans %}</option>
{% for b in buildings %}
<option value="{{ b.name }}">{{ b.name }}</option>
{% endfor %}
</select>
</div>
<!-- Filter by Floor -->
<div class="col-lg-3 col-md-4">
<label for="filterFloor" class="form-label small fw-bold text-muted mb-1">
@@ -90,26 +103,25 @@
<select id="filterFloor" class="form-select form-select-sm">
<option value="">{% trans %}Todos los pisos{% endtrans %}</option>
{% for f in available_floors %}
<option value="{{ f }}">{% if f == 0 %}{% trans %}Planta Baja (Piso 0){% endtrans %}{% else %}{% trans %}Piso{% endtrans %} {{ f }}{% endif %}</option>
<option value="{{ f }}">{% if f == '0' or f == 0 or f == 'PB' %}{% trans %}Planta Baja{% endtrans %}{% else %}{% trans %}Piso{% endtrans %} {{ f }}{% endif %}</option>
{% endfor %}
</select>
</div>
<!-- Hide Virtual Classrooms Toggle -->
<div class="col-lg-6 col-md-5 pt-md-3">
<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" id="filterHideVirtual">
<label class="form-check-label fw-semibold small text-dark" for="filterHideVirtual">
<i class="bi bi-laptop me-1 text-primary"></i>{% trans %}Ocultar Aulas Virtuales{% endtrans %}
<span class="badge bg-secondary-subtle text-secondary border ms-1" style="font-size: 0.72rem;">{% trans %}Capacidad ilimitada{% endtrans %}</span>
</label>
</div>
</div>
<!-- Reset Filters Button -->
<div class="col-lg-3 col-md-3 text-md-end pt-md-3">
<button type="button" id="btnResetFilters" class="btn btn-outline-secondary btn-sm px-3" title="{% trans %}Restablecer filtros{% endtrans %}">
<i class="bi bi-arrow-counterclockwise me-1"></i>{% trans %}Limpiar Filtros{% endtrans %}
<div class="col-lg-2 col-md-12 text-md-end pt-md-3">
<button type="button" class="btn btn-outline-secondary btn-sm w-100 w-md-auto" id="btnResetFilters">
<i class="bi bi-x-circle me-1"></i>{% trans %}Limpiar{% endtrans %}
</button>
</div>
</div>
@@ -209,16 +221,19 @@
</div>
</div>
<!-- 📍 Espacio / Aula / Virtual -->
<div class="d-flex align-items-center gap-3">
<div class="gc-icon-circle text-danger" id="previewLocationIcon">
<!-- 📍 Espacio / Aula / Virtual Desglosado -->
<div class="d-flex align-items-start gap-3">
<div class="gc-icon-circle text-danger mt-1" id="previewLocationIcon">
<i class="bi bi-geo-alt fs-5"></i>
</div>
<div class="flex-grow-1">
<div class="fw-semibold text-body" id="previewClassroom">Aula 304</div>
<small class="text-muted" id="previewBuilding">Piso 3 · Edificio Central (Perón 802)</small>
<div id="previewVirtualWrapper" class="d-none mt-1">
<span class="badge bg-primary-subtle text-primary border">
<div class="d-flex flex-wrap gap-1 mb-1" id="previewLocationBadges">
<span class="badge bg-primary-subtle text-primary border" id="previewBadgeBuilding">🏢 Edificio Central</span>
<span class="badge bg-secondary-subtle text-secondary border" id="previewBadgeFloor">🪜 Piso 3</span>
<span class="badge bg-success-subtle text-success border" id="previewBadgeRoom">🚪 Aula 304</span>
</div>
<div id="previewVirtualWrapper" class="d-none">
<span class="badge bg-info-subtle text-info border">
<i class="bi bi-camera-video me-1"></i>{% trans %}Campus Virtual UniCABA (Teams / Moodle){% endtrans %}
</span>
</div>
@@ -685,6 +700,7 @@ document.addEventListener('DOMContentLoaded', function() {
var filterSubj = document.getElementById('filterSubject');
var filterCar = document.getElementById('filterCareer');
var filterShift = document.getElementById('filterShift');
var filterBuilding = document.getElementById('filterBuilding');
var filterFloor = document.getElementById('filterFloor');
var filterHideVirtual = document.getElementById('filterHideVirtual');
var activeBadge = document.getElementById('activeFilterBadge');
@@ -704,6 +720,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (filterCar.value && filterCar.selectedOptions[0]) {
parts.push('Carrera: ' + filterCar.selectedOptions[0].text.trim());
}
if (filterBuilding && filterBuilding.value) {
parts.push('Edificio: ' + filterBuilding.value);
}
if (filterShift && filterShift.value) {
parts.push('Turno ' + filterShift.value);
}
@@ -763,6 +782,7 @@ document.addEventListener('DOMContentLoaded', function() {
subject_id: filterSubj.value || '',
career_id: filterCar.value || '',
shift: (filterShift ? filterShift.value : '') || '',
building: (filterBuilding ? filterBuilding.value : '') || '',
floor: (filterFloor ? filterFloor.value : '') || '',
hide_virtual: (filterHideVirtual && filterHideVirtual.checked) ? '1' : ''
});
@@ -922,18 +942,24 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('previewDateFull').textContent = `Horario académico registrado`;
}
// Espacio Asignado
// Espacio Asignado Desglosado con Badges
var locIcon = document.getElementById('previewLocationIcon');
var virtualWrap = document.getElementById('previewVirtualWrapper');
var badgeB = document.getElementById('previewBadgeBuilding');
var badgeF = document.getElementById('previewBadgeFloor');
var badgeR = document.getElementById('previewBadgeRoom');
if (isVirtual) {
if (locIcon) locIcon.innerHTML = '<i class="bi bi-camera-video fs-5 text-primary"></i>';
document.getElementById('previewClassroom').textContent = 'Campus Virtual';
document.getElementById('previewBuilding').textContent = 'Entorno Digital UniCABA';
if (badgeB) badgeB.textContent = '🏢 Campus Virtual';
if (badgeF) badgeF.textContent = '🪜 Remoto';
if (badgeR) badgeR.textContent = '🚪 ' + classroom;
if (virtualWrap) virtualWrap.classList.remove('d-none');
} else {
if (locIcon) locIcon.innerHTML = '<i class="bi bi-geo-alt fs-5 text-danger"></i>';
document.getElementById('previewClassroom').textContent = classroom;
document.getElementById('previewBuilding').textContent = `${ext.floor !== null && ext.floor !== undefined ? (ext.floor === 0 ? 'Planta Baja' : 'Piso ' + ext.floor) + ' · ' : ''}${building}`;
if (badgeB) badgeB.textContent = '🏢 ' + (building || 'Edificio Central');
if (badgeF) badgeF.textContent = '🪜 ' + (ext.floor || 'Planta Baja');
if (badgeR) badgeR.textContent = '🚪 ' + classroom;
if (virtualWrap) virtualWrap.classList.add('d-none');
}
@@ -987,6 +1013,12 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
if (filterBuilding) {
filterBuilding.addEventListener('change', function() {
calendar.refetchEvents();
});
}
if (filterFloor) {
filterFloor.addEventListener('change', function() {
calendar.refetchEvents();
@@ -1004,6 +1036,7 @@ document.addEventListener('DOMContentLoaded', function() {
filterSubj.value = '';
filterCar.value = '';
if (filterShift) filterShift.value = '';
if (filterBuilding) filterBuilding.value = '';
if (filterFloor) filterFloor.value = '';
if (filterHideVirtual) filterHideVirtual.checked = false;
calendar.refetchEvents();
+35 -21
View File
@@ -86,35 +86,49 @@
<!-- Classroom Details -->
<div class="card border-0 shadow-sm">
<div class="card-header border-0 py-3">
<h5 class="mb-0">
<i class="bi bi-building text-info"></i> Classroom Information
<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>{% trans %}Ubicación y Espacio Asignado{% endtrans %}
</h5>
</div>
<div class="card-body">
<div class="card-body p-4">
{% if reservation.classroom %}
<div class="row">
<div class="col-md-4 mb-3">
<label class="text-muted small">Classroom</label>
<p class="mb-0"><strong>{{ reservation.classroom.code_display }}</strong></p>
<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">🏢 {% trans %}Edificio / Sede{% endtrans %}</small>
<h6 class="mb-0 fw-bold text-primary">{{ reservation.classroom.building_name }}</h6>
</div>
</div>
<div class="col-md-4 mb-3">
<label class="text-muted small">Building</label>
<p class="mb-0">{{ reservation.classroom.building }}</p>
<div class="col-md-4">
<div class="p-3 rounded bg-body-tertiary border">
<small class="text-muted d-block mb-1">🪜 {% trans %}Piso / Nivel{% endtrans %}</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 mb-3">
<label class="text-muted small">Capacity</label>
<p class="mb-0">{{ reservation.classroom.capacity }} people</p>
<div class="col-md-4">
<div class="p-3 rounded bg-body-tertiary border">
<small class="text-muted d-block mb-1">🚪 {% trans %}Aula{% endtrans %}</small>
<h6 class="mb-0 fw-bold text-success">{{ reservation.classroom.code }}</h6>
<small class="text-muted">{% trans %}Capacidad:{% endtrans %} <strong>{{ reservation.classroom.capacity_display }}</strong></small>
</div>
</div>
{% if reservation.classroom.description %}
<div class="col-12">
<label class="text-muted small">Description</label>
<p class="mb-0">{{ reservation.classroom.description }}</p>
</div>
{% endif %}
</div>
{% if reservation.classroom.description %}
<div class="mt-2">
<label class="text-muted small">{% trans %}Descripción del Aula{% endtrans %}</label>
<p class="mb-0 small">{{ reservation.classroom.description }}</p>
</div>
{% endif %}
{% else %}
<p class="text-muted mb-0">Classroom information not available</p>
<p class="text-muted mb-0">{% trans %}Classroom information not available{% endtrans %}</p>
{% endif %}
</div>
</div>