372 lines
16 KiB
HTML
372 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{% trans %}New Reservation{% endtrans %} - 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">
|
|
<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 %}
|
|
</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 border-0 py-3">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-calendar3 text-success"></i> {% trans %}Reservation Details{% endtrans %}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<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] }}
|
|
</div>
|
|
{% endif %}
|
|
<small class="text-muted">{% trans %}Select a classroom for your reservation{% endtrans %}</small>
|
|
</div>
|
|
|
|
<div class="col-md-6 mb-3">
|
|
{{ form.commission_id.label(class="form-label") }}
|
|
{{ 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>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
{{ form.start_time.label(class="form-label") }}
|
|
{{ 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>
|
|
</div>
|
|
|
|
<div class="col-md-6 mb-3">
|
|
{{ form.end_time.label(class="form-label") }}
|
|
{{ 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>
|
|
</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.")) }}
|
|
{% if form.purpose.errors %}
|
|
<div class="text-danger small mt-1">
|
|
{{ form.purpose.errors[0] }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="col-md-4 mb-3">
|
|
{{ form.expected_attendees.label(class="form-label") }}
|
|
{{ form.expected_attendees(class="form-control", 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>
|
|
</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...")) }}
|
|
{% if form.notes.errors %}
|
|
<div class="text-danger small mt-1">
|
|
{{ form.notes.errors[0] }}
|
|
</div>
|
|
{% 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>
|
|
</div>
|
|
</div>
|
|
|
|
<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 %}
|
|
</a>
|
|
{{ form.submit(class="btn btn-success") }}
|
|
</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 }}
|
|
}{% 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
|
|
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();
|
|
const commissionId = $('#commission-select').val();
|
|
const startTime = $('#start-time').val();
|
|
const endTime = $('#end-time').val();
|
|
|
|
// Update classroom summary
|
|
if (classroomId && classroomData[classroomId]) {
|
|
const capLabel = i18n.capacity || 'Capacidad';
|
|
$('#summary-classroom').text(`${classroomData[classroomId].code_display} (${capLabel}: ${classroomData[classroomId].capacity})`);
|
|
} else {
|
|
$('#summary-classroom').text(i18n.not_selected || 'No seleccionada');
|
|
}
|
|
|
|
// Update commission summary
|
|
if (commissionId) {
|
|
const commissionText = $('#commission-select option:selected').text();
|
|
$('#summary-commission').text(commissionText);
|
|
} else {
|
|
$('#summary-commission').text(i18n.not_selected || 'No seleccionada');
|
|
}
|
|
|
|
// Calculate duration
|
|
if (startTime && endTime) {
|
|
const start = new Date(startTime);
|
|
const end = new Date(endTime);
|
|
|
|
if (end > start) {
|
|
const diffMs = end - start;
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
|
|
let durationText = '';
|
|
if (diffHours > 0) {
|
|
const hLabel = diffHours > 1 ? (i18n.hours || 'horas') : (i18n.hour || 'hora');
|
|
durationText += `${diffHours} ${hLabel}`;
|
|
}
|
|
if (diffMins > 0) {
|
|
if (durationText) durationText += ' ';
|
|
const mLabel = diffMins > 1 ? (i18n.minutes || 'minutos') : (i18n.minute || 'minuto');
|
|
durationText += `${diffMins} ${mLabel}`;
|
|
}
|
|
|
|
$('#summary-duration').text(durationText || '0 min');
|
|
} else {
|
|
$('#summary-duration').text(i18n.invalid_time || 'Rango horario inválido');
|
|
}
|
|
} else {
|
|
$('#summary-duration').text(i18n.not_calculated || 'No calculada');
|
|
}
|
|
}
|
|
|
|
// Check capacity vs expected attendees
|
|
function checkCapacity() {
|
|
const classroomId = $('#classroom-select').val();
|
|
const attendees = parseInt($('#expected_attendees').val()) || 0;
|
|
|
|
if (classroomId && classroomData[classroomId] && attendees > 0) {
|
|
const capacity = classroomData[classroomId].capacity;
|
|
|
|
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">
|
|
${msg} (${attendees} > ${capacity})
|
|
</div>
|
|
`);
|
|
} else {
|
|
$('#expected_attendees').removeClass('is-invalid');
|
|
$('#expected_attendees').siblings('.invalid-feedback').remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update on change
|
|
$('#classroom-select, #commission-select, #start-time, #end-time, #expected_attendees').on('change', updateSummary);
|
|
$('#expected_attendees').on('input', checkCapacity);
|
|
|
|
// 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');
|
|
isValid = false;
|
|
} else {
|
|
$('#classroom-select').removeClass('is-invalid');
|
|
}
|
|
|
|
if (!$('#commission-select').val()) {
|
|
$('#commission-select').addClass('is-invalid');
|
|
isValid = false;
|
|
} else {
|
|
$('#commission-select').removeClass('is-invalid');
|
|
}
|
|
|
|
if (!$('#start-time').val()) {
|
|
$('#start-time').addClass('is-invalid');
|
|
isValid = false;
|
|
} else {
|
|
$('#start-time').removeClass('is-invalid');
|
|
}
|
|
|
|
if (!$('#end-time').val()) {
|
|
$('#end-time').addClass('is-invalid');
|
|
isValid = false;
|
|
} else {
|
|
$('#end-time').removeClass('is-invalid');
|
|
}
|
|
|
|
// Validate time range
|
|
const startTime = new Date($('#start-time').val());
|
|
const endTime = new Date($('#end-time').val());
|
|
|
|
if (endTime <= startTime) {
|
|
$('#end-time').addClass('is-invalid');
|
|
isValid = false;
|
|
}
|
|
|
|
// Check capacity
|
|
checkCapacity();
|
|
if ($('#expected_attendees').hasClass('is-invalid')) {
|
|
isValid = false;
|
|
}
|
|
|
|
return isValid;
|
|
});
|
|
|
|
// Set minimum datetime to current
|
|
const now = new Date();
|
|
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
|
|
$('#start-time, #end-time').attr('min', now.toISOString().slice(0, 16));
|
|
});
|
|
</script>
|
|
{% endblock %} |