feat: gestion RBAC granular, importacion sheets, filtrado de aulas virtuales y pisos, capacidad infinita y traduccion

This commit is contained in:
Carlos Tello
2026-09-03 03:08:49 -03:00
parent d1d4be0428
commit 5cc33cc7a0
138 changed files with 14419 additions and 1490 deletions
+95 -53
View File
@@ -1,6 +1,6 @@
{% extends "base.html" %}
{% block title %}Add Reservation - Edu-Space Admin{% endblock %}
{% block title %}{% trans %}New Reservation{% endtrans %} - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
@@ -8,10 +8,10 @@
<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> New Reservation
<i class="bi bi-calendar-plus text-success"></i> {% trans %}New Reservation{% endtrans %}
</h1>
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Reservations
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> {% trans %}Back to Reservations{% endtrans %}
</a>
</div>
</div>
@@ -20,9 +20,9 @@
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<div class="card-header border-0 py-3">
<h5 class="mb-0">
<i class="bi bi-calendar3 text-success"></i> Reservation Details
<i class="bi bi-calendar3 text-success"></i> {% trans %}Reservation Details{% endtrans %}
</h5>
</div>
<div class="card-body">
@@ -38,7 +38,7 @@
{{ form.classroom_id.errors[0] }}
</div>
{% endif %}
<small class="text-muted">Select a classroom for your reservation</small>
<small class="text-muted">{% trans %}Select a classroom for your reservation{% endtrans %}</small>
</div>
<div class="col-md-6 mb-3">
@@ -49,7 +49,7 @@
{{ form.commission_id.errors[0] }}
</div>
{% endif %}
<small class="text-muted">Select the subject/commission</small>
<small class="text-muted">{% trans %}Select the subject/commission{% endtrans %}</small>
</div>
</div>
@@ -62,7 +62,7 @@
{{ form.start_time.errors[0] }}
</div>
{% endif %}
<small class="text-muted">When will the class start?</small>
<small class="text-muted">{% trans %}When will the class start?{% endtrans %}</small>
</div>
<div class="col-md-6 mb-3">
@@ -73,14 +73,14 @@
{{ form.end_time.errors[0] }}
</div>
{% endif %}
<small class="text-muted">When will the class end?</small>
<small class="text-muted">{% trans %}When will the class end?{% 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.") }}
{{ form.purpose(class="form-control", placeholder=_("Regular class, exam, study session, etc.")) }}
{% if form.purpose.errors %}
<div class="text-danger small mt-1">
{{ form.purpose.errors[0] }}
@@ -96,13 +96,13 @@
{{ form.expected_attendees.errors[0] }}
</div>
{% endif %}
<small class="text-muted">Number of expected attendees</small>
<small class="text-muted">{% trans %}Number of expected attendees{% endtrans %}</small>
</div>
</div>
<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(class="form-control", rows="3", placeholder=_("Any additional notes or special requirements...")) }}
{% if form.notes.errors %}
<div class="text-danger small mt-1">
{{ form.notes.errors[0] }}
@@ -113,27 +113,27 @@
<!-- Summary Section -->
<div class="alert alert-info" id="reservation-summary">
<h6 class="alert-heading">
<i class="bi bi-info-circle"></i> Reservation Summary
<i class="bi bi-info-circle"></i> {% trans %}Reservation Summary{% endtrans %}
</h6>
<div id="summary-content">
<p class="mb-2">
<strong>Selected Classroom:</strong>
<span id="summary-classroom">Not selected</span>
<strong>{% trans %}Selected Classroom:{% endtrans %}</strong>
<span id="summary-classroom">{% trans %}Not selected{% endtrans %}</span>
</p>
<p class="mb-2">
<strong>Duration:</strong>
<span id="summary-duration">Not calculated</span>
<strong>{% trans %}Duration:{% endtrans %}</strong>
<span id="summary-duration">{% trans %}Not calculated{% endtrans %}</span>
</p>
<p class="mb-0">
<strong>Commission:</strong>
<span id="summary-commission">Not selected</span>
<strong>{% trans %}Commission:{% endtrans %}</strong>
<span id="summary-commission">{% trans %}Not selected{% endtrans %}</span>
</p>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Cancel
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> {% trans %}Cancel{% endtrans %}
</a>
{{ form.submit(class="btn btn-success") }}
</div>
@@ -147,29 +147,29 @@
<div class="row mt-4">
<div class="col-lg-8 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<div class="card-header border-0 py-3">
<h5 class="mb-0">
<i class="bi bi-question-circle text-info"></i> Reservation Guidelines
<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">📅 Booking Rules</h6>
<h6 class="text-primary">📅 {% trans %}Booking Rules{% endtrans %}</h6>
<ul class="small">
<li>Reservations can be made up to 3 months in advance</li>
<li>Cannot book in the past</li>
<li>Maximum duration: 8 hours per reservation</li>
<li>Cancellations must be 24 hours before start time</li>
<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">🚨 Important Notes</h6>
<h6 class="text-warning">🚨 {% trans %}Important Notes{% endtrans %}</h6>
<ul class="small">
<li>Check classroom capacity before booking</li>
<li>Pending reservations require approval</li>
<li>Time slots must not overlap</li>
<li>System automatically detects conflicts</li>
<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>
@@ -181,17 +181,55 @@
{% endblock %}
{% block extra_js %}
<script type="application/json" id="classroom-data">
{
{% for classroom in classroom_list %}
"{{ classroom.id }}": {
"capacity": {{ classroom.capacity }},
"code_display": {{ classroom.code_display|tojson }},
"name": {{ classroom.name|tojson }}
}{% if not loop.last %},{% endif %}
{% endfor %}
}
</script>
<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 %}"
}
</script>
<script>
$(document).ready(function() {
// Classroom data for capacity checking
const classroomData = {};
{% for classroom in classroom_list %}
classroomData[{{ classroom.id }}] = {
capacity: {{ classroom.capacity }},
code_display: "{{ classroom.code_display }}",
name: "{{ classroom.name }}"
};
{% endfor %}
// Classroom data for capacity checking
let classroomData = {};
try {
const rawData = document.getElementById('classroom-data')?.textContent;
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);
}
} catch (err) {
console.error('Error parsing i18n:', err);
}
function updateSummary() {
const classroomId = $('#classroom-select').val();
@@ -199,11 +237,12 @@ $(document).ready(function() {
const startTime = $('#start-time').val();
const endTime = $('#end-time').val();
// Update classroom summary
// Update classroom summary
if (classroomId && classroomData[classroomId]) {
$('#summary-classroom').text(`${classroomData[classroomId].code_display} - ${classroomData[classroomId].name} (Capacity: ${classroomData[classroomId].capacity})`);
const capLabel = i18n.capacity || 'Capacidad';
$('#summary-classroom').text(`${classroomData[classroomId].code_display} - ${classroomData[classroomId].name} (${capLabel}: ${classroomData[classroomId].capacity})`);
} else {
$('#summary-classroom').text('Not selected');
$('#summary-classroom').text(i18n.not_selected || 'No seleccionada');
}
// Update commission summary
@@ -211,7 +250,7 @@ $(document).ready(function() {
const commissionText = $('#commission-select option:selected').text();
$('#summary-commission').text(commissionText);
} else {
$('#summary-commission').text('Not selected');
$('#summary-commission').text(i18n.not_selected || 'No seleccionada');
}
// Calculate duration
@@ -226,19 +265,21 @@ $(document).ready(function() {
let durationText = '';
if (diffHours > 0) {
durationText += `${diffHours} hour${diffHours > 1 ? 's' : ''}`;
const hLabel = diffHours > 1 ? (i18n.hours || 'horas') : (i18n.hour || 'hora');
durationText += `${diffHours} ${hLabel}`;
}
if (diffMins > 0) {
if (durationText) durationText += ' ';
durationText += `${diffMins} minute${diffMins > 1 ? 's' : ''}`;
const mLabel = diffMins > 1 ? (i18n.minutes || 'minutos') : (i18n.minute || 'minuto');
durationText += `${diffMins} ${mLabel}`;
}
$('#summary-duration').text(durationText);
$('#summary-duration').text(durationText || '0 min');
} else {
$('#summary-duration').text('Invalid time range');
$('#summary-duration').text(i18n.invalid_time || 'Rango horario inválido');
}
} else {
$('#summary-duration').text('Not calculated');
$('#summary-duration').text(i18n.not_calculated || 'No calculada');
}
}
@@ -252,9 +293,10 @@ $(document).ready(function() {
if (attendees > capacity) {
$('#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">
Expected attendees (${attendees}) exceeds classroom capacity (${capacity})
${msg} (${attendees} > ${capacity})
</div>
`);
} else {