feat: gestion RBAC granular, importacion sheets, filtrado de aulas virtuales y pisos, capacidad infinita y traduccion
This commit is contained in:
@@ -63,8 +63,19 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
{{ form.capacity.label(class="form-label") }}
|
||||
{{ form.capacity(class="form-control", placeholder="30") }}
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
{{ form.capacity.label(class="form-label mb-0") }}
|
||||
<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 ∞") }}
|
||||
<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 %}
|
||||
</div>
|
||||
{% if form.capacity.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{{ form.capacity.errors[0] }}
|
||||
@@ -180,14 +191,16 @@ $(document).ready(function() {
|
||||
$('#room_number').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
if (!$('#floor').val()) {
|
||||
var floorVal = $('#floor').val();
|
||||
if (floorVal === '' || isNaN(floorVal)) {
|
||||
$('#floor').addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
$('#floor').removeClass('is-invalid');
|
||||
}
|
||||
|
||||
if (!$('#capacity').val() || $('#capacity').val() < 1) {
|
||||
var capVal = $('#capacity').val() ? $('#capacity').val().trim() : '';
|
||||
if (!capVal || (capVal !== '∞' && capVal !== '0' && isNaN(capVal))) {
|
||||
$('#capacity').addClass('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
@@ -196,6 +209,10 @@ $(document).ready(function() {
|
||||
|
||||
return isValid;
|
||||
});
|
||||
|
||||
$('#btnInfinityAdd').on('click', function() {
|
||||
$('#capacity').val('∞').focus();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,162 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% trans %}Edit Classroom{% endtrans %} - {{ classroom.code_display }} - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-pencil-square text-primary me-2"></i>{% trans %}Edit Classroom{% endtrans %}
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">{{ classroom.building }} — {{ classroom.code }}</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('classrooms.view_classroom', id=classroom.id) }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-eye me-1"></i>{% trans %}Ver Ficha{% endtrans %}
|
||||
</a>
|
||||
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>{% trans %}Back to Classrooms{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
</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 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>{% trans %}Classroom Information{% endtrans %}
|
||||
</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>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
{{ form.building.label(class="form-label fw-semibold") }}
|
||||
{{ form.building(class="form-control" + (" is-invalid" if form.building.errors else ""), placeholder="Edificio Central") }}
|
||||
{% if form.building.errors %}
|
||||
<div class="invalid-feedback">{{ form.building.errors[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
{{ form.room_number.label(class="form-label fw-semibold") }}
|
||||
{{ form.room_number(class="form-control" + (" is-invalid" if form.room_number.errors else ""), placeholder="402") }}
|
||||
{% if form.room_number.errors %}
|
||||
<div class="invalid-feedback">{{ form.room_number.errors[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
{{ form.floor.label(class="form-label fw-semibold") }}
|
||||
{{ form.floor(class="form-control" + (" is-invalid" if form.floor.errors else "")) }}
|
||||
{% if form.floor.errors %}
|
||||
<div class="invalid-feedback">{{ form.floor.errors[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
{{ form.capacity.label(class="form-label fw-semibold mb-0") }}
|
||||
<button type="button" class="btn btn-sm btn-outline-primary py-0 px-2" id="btnInfinity" 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" + (" 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>{% trans %}Para aulas virtuales o eventos sin aforo restringido, ingrese <strong>∞</strong> o <strong>0</strong> (Capacidad ilimitada).{% endtrans %}
|
||||
</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-4">
|
||||
<div class="form-check form-switch pt-2">
|
||||
{{ form.is_active(class="form-check-input", role="switch") }}
|
||||
{{ form.is_active.label(class="form-check-label fw-semibold") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
{{ form.description.label(class="form-label fw-semibold") }}
|
||||
{{ form.description(class="form-control" + (" is-invalid" if form.description.errors else ""), rows="3", placeholder="Detalles sobre equipamiento o uso preferente del aula...") }}
|
||||
{% if form.description.errors %}
|
||||
<div class="invalid-feedback">{{ form.description.errors[0] }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Preview Card -->
|
||||
<div class="alert alert-info border-0 mt-4 mb-0">
|
||||
<h6 class="alert-heading fw-bold mb-2">
|
||||
<i class="bi bi-info-circle me-1"></i>{% trans %}Vista Previa de Identificador{% endtrans %}
|
||||
</h6>
|
||||
<div id="classroom-preview">
|
||||
<p class="mb-0">
|
||||
{% trans %}Código de visualización{% endtrans %}:
|
||||
<strong id="preview-code" class="text-primary font-monospace fs-6">{{ classroom.code_display }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2 mt-4 pt-3 border-top">
|
||||
<a href="{{ url_for('classrooms.view_classroom', id=classroom.id) }}" class="btn btn-outline-secondary">
|
||||
{% trans %}Cancelar{% endtrans %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-check-circle me-1"></i>{% trans %}Guardar Cambios{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var buildingInput = document.getElementById('building');
|
||||
var roomInput = document.getElementById('room_number');
|
||||
var previewCode = document.getElementById('preview-code');
|
||||
var capacityInput = document.getElementById('capacityInput');
|
||||
var btnInfinity = document.getElementById('btnInfinity');
|
||||
|
||||
function updatePreview() {
|
||||
var b = buildingInput ? buildingInput.value.trim() : '';
|
||||
var r = roomInput ? roomInput.value.trim() : '';
|
||||
if (b && r) {
|
||||
previewCode.textContent = b + '-' + r;
|
||||
} else if (r) {
|
||||
previewCode.textContent = r;
|
||||
} else {
|
||||
previewCode.textContent = '---';
|
||||
}
|
||||
}
|
||||
|
||||
if (buildingInput) buildingInput.addEventListener('input', updatePreview);
|
||||
if (roomInput) roomInput.addEventListener('input', updatePreview);
|
||||
|
||||
if (btnInfinity && capacityInput) {
|
||||
btnInfinity.addEventListener('click', function() {
|
||||
capacityInput.value = '∞';
|
||||
capacityInput.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -77,7 +77,13 @@
|
||||
<div class="row small">
|
||||
<div class="col-6">
|
||||
<span class="text-muted">{% trans %}Capacity{% endtrans %}:</span>
|
||||
<strong>{{ classroom.capacity }}</strong>
|
||||
<strong>
|
||||
{% if classroom.is_unlimited %}
|
||||
<span class="badge bg-primary-subtle text-primary border"><i class="bi bi-infinity me-1"></i>∞</span>
|
||||
{% else %}
|
||||
{{ classroom.capacity }}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<span class="text-muted">{% trans %}Building{% endtrans %}:</span>
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ classroom.code_display }} - {% trans %}Classroom Details{% endtrans %} - 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="{{ url_for('classrooms.list_classrooms') }}">{% trans %}Classrooms{% endtrans %}</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>{% trans %}Active{% endtrans %}
|
||||
</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>{% trans %}Inactive{% endtrans %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="col-md-5 text-md-end mt-3 mt-md-0">
|
||||
{% if current_user.has_permission('classrooms', 'read_write') %}
|
||||
<a href="{{ url_for('classrooms.edit_classroom', id=classroom.id) }}" class="btn btn-primary me-2">
|
||||
<i class="bi bi-pencil me-1"></i>{% trans %}Edit Classroom{% endtrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>{% trans %}Back to Classrooms{% endtrans %}
|
||||
</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>{% trans %}Ficha Técnica{% endtrans %}
|
||||
</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>{% trans %}Edificio / Sede{% endtrans %}</span>
|
||||
<span class="fw-bold">
|
||||
{% if classroom.building_entity %}
|
||||
<a href="{{ url_for('buildings.view_building', id=classroom.building_entity.id) }}" 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>{% trans %}Número / Nombre{% endtrans %}</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>{% trans %}Piso / Planta{% endtrans %}</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>{% trans %}Capacidad Máxima{% endtrans %}</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>{% trans %}Ilimitada (∞){% endtrans %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="fw-bold text-success fs-6">{{ classroom.capacity }} {% trans %}estudiantes{% endtrans %}</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">{% trans %}Descripción / Notas{% endtrans %}:</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>{% trans %}Próximas Clases y Reservas{% endtrans %}
|
||||
</h5>
|
||||
<a href="{{ url_for('schedule.calendar_view') }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-calendar-week me-1"></i>{% trans %}Ver en Calendario{% endtrans %}
|
||||
</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">{% trans %}Horario{% endtrans %}</th>
|
||||
<th>{% trans %}Materia / Motivo{% endtrans %}</th>
|
||||
<th>{% trans %}Comisión{% endtrans %}</th>
|
||||
<th>{% trans %}Asistentes{% endtrans %}</th>
|
||||
<th class="text-end pe-3">{% trans %}Acciones{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for res in upcoming_reservations %}
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<div class="fw-bold font-monospace text-primary fs-6">
|
||||
<i class="bi bi-clock text-warning me-1"></i>{{ res.start_time.strftime('%H:%M') }} - {{ res.end_time.strftime('%H:%M') }} 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">
|
||||
<a href="{{ url_for('schedule.view_reservation', id=res.id) }}" class="btn btn-outline-secondary btn-sm py-0 px-2" title="{% trans %}Ver Ficha{% endtrans %}">
|
||||
<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">{% trans %}Sin reservas programadas próximamente{% endtrans %}</h6>
|
||||
<p class="text-muted small mb-3">{% trans %}Esta aula no tiene clases asignadas en las próximas fechas.{% endtrans %}</p>
|
||||
{% if current_user.has_permission('reservations', 'read_write') %}
|
||||
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-circle me-1"></i>{% trans %}Asignar Nueva Reserva{% endtrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user