feat: refactorizar espacios fisicos desacoplando Edificio, Piso y Aula con selectores en cascada y filtros
This commit is contained in:
+165
-127
@@ -7,11 +7,19 @@
|
||||
<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-plus-circle text-primary"></i> {% trans %}Add Classroom{% endtrans %}
|
||||
</h1>
|
||||
<div>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-1 small">
|
||||
<li class="breadcrumb-item"><a href="{{ url_for('classrooms.list_classrooms') }}">{% trans %}Classrooms{% endtrans %}</a></li>
|
||||
<li class="breadcrumb-item active">{% trans %}Add Classroom{% endtrans %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="h3 mb-0">
|
||||
<i class="bi bi-plus-circle text-primary me-2"></i>{% trans %}Add Classroom{% endtrans %}
|
||||
</h1>
|
||||
</div>
|
||||
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> {% trans %}Back to Classrooms{% endtrans %}
|
||||
<i class="bi bi-arrow-left me-1"></i>{% trans %}Back to Classrooms{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -22,59 +30,81 @@
|
||||
<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> {% trans %}Classroom Information{% endtrans %}
|
||||
<i class="bi bi-door-open text-primary me-2"></i>{% trans %}Classroom Information{% endtrans %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-body p-4">
|
||||
<form method="POST" id="classroom-form" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
{{ form.building.label(class="form-label") }}
|
||||
{{ form.building(class="form-control", placeholder="A") }}
|
||||
{% if form.building.errors %}
|
||||
<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>{% trans %}Edificio / Sede{% endtrans %} <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.errors[0] }}
|
||||
{{ 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>{% trans %}Nombre del Nuevo Edificio / Sede{% endtrans %}:
|
||||
</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">{% trans %}Se registrará automáticamente en el catálogo de sedes.{% endtrans %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
{{ form.room_number.label(class="form-label") }}
|
||||
{{ form.room_number(class="form-control", placeholder="101") }}
|
||||
{% if form.room_number.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.room_number.errors[0] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
{{ form.floor.label(class="form-label") }}
|
||||
{{ form.floor(class="form-control") }}
|
||||
|
||||
<!-- 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>{% trans %}Piso / Nivel{% endtrans %} <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">{% trans %}Sugerido o nuevo{% endtrans %}</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>{% trans %}Aula / Sala{% endtrans %} <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">
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
{{ form.capacity.label(class="form-label mb-0") }}
|
||||
<label for="capacity" class="form-label fw-semibold mb-0">{% trans %}Capacidad de Alumnos{% endtrans %} <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="{% trans %}Asignar capacidad ilimitada{% endtrans %}">
|
||||
<i class="bi bi-infinity me-1"></i>{% trans %}Ilimitada (∞){% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
{{ form.capacity(class="form-control", id="capacity", placeholder="ej. 30 o ∞") }}
|
||||
{{ 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>{% trans %}Para aulas virtuales o sin límite de aforo, ingrese <strong>∞</strong> o <strong>0</strong> (Capacidad ilimitada).{% endtrans %}
|
||||
<i class="bi bi-info-circle text-info me-1"></i>{% trans %}Para aulas virtuales o sin límite de aforo, ingrese <strong>∞</strong> o <strong>0</strong>.{% endtrans %}
|
||||
</div>
|
||||
{% if form.capacity.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
@@ -83,18 +113,17 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label d-block"> </label>
|
||||
<div class="form-check pt-2">
|
||||
{{ form.is_active(class="form-check-input") }}
|
||||
{{ form.is_active.label(class="form-check-label") }}
|
||||
<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">{% trans %}Aula Habilitada para Reservas{% endtrans %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.description.label(class="form-label") }}
|
||||
{{ form.description(class="form-control", rows="3", placeholder=_("Optional: Add any additional details about this classroom...")) }}
|
||||
{{ 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] }}
|
||||
@@ -102,27 +131,36 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Preview Section -->
|
||||
<div class="alert alert-info">
|
||||
<h6 class="alert-heading">
|
||||
<i class="bi bi-info-circle"></i> {% trans %}Preview{% endtrans %}
|
||||
</h6>
|
||||
<div id="classroom-preview">
|
||||
<p class="mb-1">{% trans %}Fill in the form to see a preview of the classroom code:{% endtrans %}
|
||||
<span class="fw-bold" id="preview-code">-</span>
|
||||
</p>
|
||||
<p class="mb-0">{% trans %}Full location:{% endtrans %}
|
||||
<span class="fw-bold" id="preview-location">-</span>
|
||||
</p>
|
||||
<!-- 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>{% trans %}Vista Previa de Ubicación{% endtrans %}
|
||||
</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted d-block">🏢 {% trans %}Edificio{% endtrans %}</small>
|
||||
<strong id="preview-building" class="text-primary">-</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted d-block">🪜 {% trans %}Piso{% endtrans %}</small>
|
||||
<strong id="preview-floor" class="text-secondary">-</strong>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<small class="text-muted d-block">🚪 {% trans %}Aula / Código{% endtrans %}</small>
|
||||
<strong id="preview-room" class="text-success">-</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-circle"></i> {% trans %}Cancel{% endtrans %}
|
||||
{% trans %}Cancelar{% endtrans %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-save me-1"></i> {{ form.submit.label.text }}
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-check-circle me-1"></i>{% trans %}Crear Aula{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -130,89 +168,89 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resources Section -->
|
||||
<div class="row mt-4 mb-4">
|
||||
<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-tools text-warning me-2"></i> {% trans %}Resources (Optional){% endtrans %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-0">{% trans %}You can add classroom resources after creating the classroom.{% endtrans %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
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() {
|
||||
const building = $('#building').val();
|
||||
const roomNumber = $('#room_number').val();
|
||||
const floor = $('#floor').val();
|
||||
const floorLabel = '{{ _("Floor") }}';
|
||||
|
||||
if (building && roomNumber && floor) {
|
||||
$('#preview-code').text(`${building}-${roomNumber}`);
|
||||
$('#preview-location').text(`${building} - ${floorLabel} ${floor}`);
|
||||
} else {
|
||||
$('#preview-code').text('-');
|
||||
$('#preview-location').text('-');
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Update preview when inputs change
|
||||
$('#building, #room_number, #floor').on('input', updatePreview);
|
||||
|
||||
// Initialize preview
|
||||
updatePreview();
|
||||
|
||||
// Form validation
|
||||
$('#classroom-form').on('submit', function(e) {
|
||||
let isValid = true;
|
||||
|
||||
if (!$('#building').val().trim()) {
|
||||
$('#building').addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
$('#building').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
if (!$('#room_number').val().trim()) {
|
||||
$('#room_number').addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
$('#room_number').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
var floorVal = $('#floor').val();
|
||||
if (floorVal === '' || isNaN(floorVal)) {
|
||||
$('#floor').addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
$('#floor').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
var capVal = $('#capacity').val() ? $('#capacity').val().trim() : '';
|
||||
if (!capVal || (capVal !== '∞' && capVal !== '0' && isNaN(capVal))) {
|
||||
$('#capacity').addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
$('#capacity').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
return isValid;
|
||||
});
|
||||
|
||||
$('#btnInfinityAdd').on('click', function() {
|
||||
$('#capacity').val('∞').focus();
|
||||
});
|
||||
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 %}
|
||||
Reference in New Issue
Block a user