fix spanish
This commit is contained in:
@@ -55,6 +55,70 @@ class Classroom(db.Model):
|
|||||||
location += f" ({self.campus_location})"
|
location += f" ({self.campus_location})"
|
||||||
return location
|
return location
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def find_best_classroom(max_students, start_time, end_time, exclude_reservation_id=None):
|
||||||
|
"""Find the best suitable classroom for a commission based on capacity and availability"""
|
||||||
|
from app.models.reservation import Reservation, ReservationStatus
|
||||||
|
|
||||||
|
# Get all active classrooms with sufficient capacity
|
||||||
|
suitable_classrooms = Classroom.query.filter(
|
||||||
|
Classroom.is_active == True,
|
||||||
|
Classroom.capacity >= max_students
|
||||||
|
).order_by(Classroom.capacity.asc()).all() # Start with smallest suitable
|
||||||
|
|
||||||
|
# Check availability for each classroom
|
||||||
|
available_classrooms = []
|
||||||
|
for classroom in suitable_classrooms:
|
||||||
|
# Check for conflicts
|
||||||
|
conflicts = Reservation.find_conflicts(
|
||||||
|
classroom.id, start_time, end_time, exclude_reservation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not conflicts:
|
||||||
|
# Calculate efficiency score (how well the capacity matches)
|
||||||
|
efficiency = classroom.capacity / max_students if max_students > 0 else float('inf')
|
||||||
|
available_classrooms.append({
|
||||||
|
'classroom': classroom,
|
||||||
|
'efficiency': efficiency,
|
||||||
|
'wasted_capacity': classroom.capacity - max_students
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by efficiency (closest capacity match first)
|
||||||
|
available_classrooms.sort(key=lambda x: x['efficiency'])
|
||||||
|
|
||||||
|
return available_classrooms
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_availability_status(start_time, end_time, exclude_reservation_id=None):
|
||||||
|
"""Get all classrooms classified as available or occupied for a time slot"""
|
||||||
|
from app.models.reservation import Reservation, ReservationStatus
|
||||||
|
|
||||||
|
all_classrooms = Classroom.query.filter_by(is_active=True).all()
|
||||||
|
available = []
|
||||||
|
occupied = []
|
||||||
|
|
||||||
|
for classroom in all_classrooms:
|
||||||
|
conflicts = Reservation.find_conflicts(
|
||||||
|
classroom.id, start_time, end_time, exclude_reservation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
classroom_info = {
|
||||||
|
'classroom': classroom,
|
||||||
|
'conflicts': conflicts
|
||||||
|
}
|
||||||
|
|
||||||
|
if conflicts:
|
||||||
|
occupied.append(classroom_info)
|
||||||
|
else:
|
||||||
|
available.append(classroom_info)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'available': available,
|
||||||
|
'occupied': occupied,
|
||||||
|
'total_available': len(available),
|
||||||
|
'total_occupied': len(occupied)
|
||||||
|
}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Classroom {self.code_display}>'
|
return f'<Classroom {self.code_display}>'
|
||||||
|
|
||||||
|
|||||||
+158
-2
@@ -118,12 +118,80 @@ def calendar_view():
|
|||||||
"""Show calendar view"""
|
"""Show calendar view"""
|
||||||
return render_template('schedule/calendar.html', title='Calendar View')
|
return render_template('schedule/calendar.html', title='Calendar View')
|
||||||
|
|
||||||
@schedule_bp.route('/add')
|
@schedule_bp.route('/commissions')
|
||||||
|
@login_required
|
||||||
|
def commissions_view():
|
||||||
|
"""View commissions by subject for easy mapping"""
|
||||||
|
from app.models.subject import Subject, Commission
|
||||||
|
|
||||||
|
# Get all active subjects with their commissions
|
||||||
|
subjects = Subject.query.filter_by(is_active=True).options(
|
||||||
|
db.joinedload(Subject.commissions).joinedload(Commission.teacher)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
return render_template('schedule/commissions.html', title='Commissions Management', subjects=subjects)
|
||||||
|
|
||||||
|
@schedule_bp.route('/add', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def add_reservation():
|
def add_reservation():
|
||||||
"""Add new reservation"""
|
"""Add new reservation"""
|
||||||
|
from app.models.classroom import Classroom
|
||||||
|
from app.models.subject import Commission
|
||||||
|
|
||||||
form = ReservationForm()
|
form = ReservationForm()
|
||||||
return render_template('schedule/create.html', form=form, title='New Reservation')
|
|
||||||
|
# Get active classrooms for the dropdown
|
||||||
|
classrooms = Classroom.query.filter_by(is_active=True).all()
|
||||||
|
|
||||||
|
# Get active commissions for the dropdown
|
||||||
|
commissions = Commission.query.filter_by(active=True).all()
|
||||||
|
|
||||||
|
# Populate form choices
|
||||||
|
form.classroom_id.choices = [(c.id, f'{c.code_display} - {c.name} (Cap: {c.capacity})') for c in classrooms]
|
||||||
|
form.commission_id.choices = [(c.id, f'{c.get_full_code()} - {c.subject.name if c.subject else "No Subject"} (Students: {c.max_students})') for c in commissions]
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
if form.validate_on_submit():
|
||||||
|
try:
|
||||||
|
# Check for existing reservations at the same time
|
||||||
|
from app.models.reservation import ReservationStatus
|
||||||
|
existing_reservation = Reservation.query.filter_by(
|
||||||
|
classroom_id=form.classroom_id.data,
|
||||||
|
status=ReservationStatus.CONFIRMED
|
||||||
|
).filter(
|
||||||
|
Reservation.start_time < form.end_time.data,
|
||||||
|
Reservation.end_time > form.start_time.data
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_reservation:
|
||||||
|
flash('This classroom is already booked for the selected time slot.', 'danger')
|
||||||
|
return render_template('schedule/add.html', form=form, title='New Reservation', classroom_list=classrooms, commission_list=commissions)
|
||||||
|
|
||||||
|
# Create new reservation
|
||||||
|
reservation = Reservation(
|
||||||
|
classroom_id=form.classroom_id.data,
|
||||||
|
commission_id=form.commission_id.data,
|
||||||
|
user_id=current_user.id,
|
||||||
|
start_time=form.start_time.data,
|
||||||
|
end_time=form.end_time.data,
|
||||||
|
purpose=form.purpose.data,
|
||||||
|
notes=form.notes.data,
|
||||||
|
status=ReservationStatus.PENDING,
|
||||||
|
expected_attendees=form.expected_attendees.data
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(reservation)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash('Reservation request submitted successfully! It will be reviewed by an administrator.', 'success')
|
||||||
|
return redirect(url_for('schedule.list_reservations'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"Error creating reservation: {str(e)}")
|
||||||
|
flash(f'Error creating reservation: {str(e)}', 'danger')
|
||||||
|
|
||||||
|
return render_template('schedule/add.html', form=form, title='New Reservation', classroom_list=classrooms, commission_list=commissions)
|
||||||
|
|
||||||
@schedule_bp.route('/list')
|
@schedule_bp.route('/list')
|
||||||
@login_required
|
@login_required
|
||||||
@@ -154,6 +222,94 @@ def confirm_reservation(id):
|
|||||||
return redirect(request.referrer)
|
return redirect(request.referrer)
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
|
@schedule_bp.route('/api/classroom-availability')
|
||||||
|
@login_required
|
||||||
|
def classroom_availability_api():
|
||||||
|
"""Get classroom availability for a specific date/time"""
|
||||||
|
try:
|
||||||
|
date_str = request.args.get('date')
|
||||||
|
start_time_str = request.args.get('start_time')
|
||||||
|
end_time_str = request.args.get('end_time')
|
||||||
|
max_students = request.args.get('max_students', type=int)
|
||||||
|
|
||||||
|
if not all([date_str, start_time_str, end_time_str]):
|
||||||
|
return jsonify({'error': 'Missing required parameters: date, start_time, end_time'}), 400
|
||||||
|
|
||||||
|
# Parse datetime strings
|
||||||
|
from datetime import datetime
|
||||||
|
start_time = datetime.strptime(f"{date_str} {start_time_str}", "%Y-%m-%d %H:%M")
|
||||||
|
end_time = datetime.strptime(f"{date_str} {end_time_str}", "%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
|
# Get availability status
|
||||||
|
from app.models.classroom import Classroom
|
||||||
|
availability = Classroom.get_availability_status(start_time, end_time)
|
||||||
|
|
||||||
|
# Get best classroom recommendation if max_students provided
|
||||||
|
best_recommendation = None
|
||||||
|
if max_students:
|
||||||
|
best_classrooms = Classroom.find_best_classroom(max_students, start_time, end_time)
|
||||||
|
if best_classrooms:
|
||||||
|
best_recommendation = best_classrooms[0] # Most efficient one
|
||||||
|
|
||||||
|
# Format response
|
||||||
|
response = {
|
||||||
|
'availability': {
|
||||||
|
'available': [
|
||||||
|
{
|
||||||
|
'id': item['classroom'].id,
|
||||||
|
'code': item['classroom'].code_display,
|
||||||
|
'name': item['classroom'].name,
|
||||||
|
'capacity': item['classroom'].capacity,
|
||||||
|
'location': item['classroom'].get_full_location()
|
||||||
|
}
|
||||||
|
for item in availability['available']
|
||||||
|
],
|
||||||
|
'occupied': [
|
||||||
|
{
|
||||||
|
'id': item['classroom'].id,
|
||||||
|
'code': item['classroom'].code_display,
|
||||||
|
'name': item['classroom'].name,
|
||||||
|
'capacity': item['classroom'].capacity,
|
||||||
|
'location': item['classroom'].get_full_location(),
|
||||||
|
'conflicts': [
|
||||||
|
{
|
||||||
|
'start_time': conflict.start_time.strftime('%H:%M'),
|
||||||
|
'end_time': conflict.end_time.strftime('%H:%M'),
|
||||||
|
'commission': conflict.commission.get_full_code() if conflict.commission else 'Unknown'
|
||||||
|
}
|
||||||
|
for conflict in item['conflicts']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
for item in availability['occupied']
|
||||||
|
],
|
||||||
|
'stats': {
|
||||||
|
'total_available': availability['total_available'],
|
||||||
|
'total_occupied': availability['total_occupied']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if best_recommendation:
|
||||||
|
response['recommendation'] = {
|
||||||
|
'classroom': {
|
||||||
|
'id': best_recommendation['classroom'].id,
|
||||||
|
'code': best_recommendation['classroom'].code_display,
|
||||||
|
'name': best_recommendation['classroom'].name,
|
||||||
|
'capacity': best_recommendation['classroom'].capacity,
|
||||||
|
'location': best_recommendation['classroom'].get_full_location()
|
||||||
|
},
|
||||||
|
'efficiency': best_recommendation['efficiency'],
|
||||||
|
'wasted_capacity': best_recommendation['wasted_capacity'],
|
||||||
|
'reason': 'Best match based on capacity and availability'
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'error': f'Invalid date/time format: {str(e)}'}), 400
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@schedule_bp.route('/calendar-data')
|
@schedule_bp.route('/calendar-data')
|
||||||
@login_required
|
@login_required
|
||||||
def calendar_data():
|
def calendar_data():
|
||||||
|
|||||||
@@ -110,6 +110,9 @@
|
|||||||
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-outline-primary">
|
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-outline-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Add Classroom
|
<i class="bi bi-plus-circle"></i> Add Classroom
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('schedule.commissions_view') }}" class="btn btn-outline-warning">
|
||||||
|
<i class="bi bi-people"></i> Manage Commissions
|
||||||
|
</a>
|
||||||
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-outline-success">
|
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-outline-success">
|
||||||
<i class="bi bi-calendar-plus"></i> New Reservation
|
<i class="bi bi-calendar-plus"></i> New Reservation
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -188,7 +188,8 @@ $(document).ready(function() {
|
|||||||
{% for classroom in classroom_list %}
|
{% for classroom in classroom_list %}
|
||||||
classroomData[{{ classroom.id }}] = {
|
classroomData[{{ classroom.id }}] = {
|
||||||
capacity: {{ classroom.capacity }},
|
capacity: {{ classroom.capacity }},
|
||||||
code: "{{ classroom.code }}"
|
code_display: "{{ classroom.code_display }}",
|
||||||
|
name: "{{ classroom.name }}"
|
||||||
};
|
};
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
@@ -200,7 +201,7 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
// Update classroom summary
|
// Update classroom summary
|
||||||
if (classroomId && classroomData[classroomId]) {
|
if (classroomId && classroomData[classroomId]) {
|
||||||
$('#summary-classroom').text(`${classroomData[classroomId].code} (Capacity: ${classroomData[classroomId].capacity})`);
|
$('#summary-classroom').text(`${classroomData[classroomId].code_display} - ${classroomData[classroomId].name} (Capacity: ${classroomData[classroomId].capacity})`);
|
||||||
} else {
|
} else {
|
||||||
$('#summary-classroom').text('Not selected');
|
$('#summary-classroom').text('Not selected');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,388 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Commissions Management - 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-diagram-3 text-info"></i> Commissions Management
|
||||||
|
</h1>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-success">
|
||||||
|
<i class="bi bi-plus-circle"></i> New Reservation
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-calendar-day"></i> Today's Schedule
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics Cards -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h2 class="text-primary">{{ subjects|length }}</h2>
|
||||||
|
<p class="mb-0">Active Subjects</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h2 class="text-success">{{ subjects|map(attribute='commissions')|map('length')|sum }}</h2>
|
||||||
|
<p class="mb-0">Total Commissions</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h2 class="text-info">{{ subjects|map(attribute='commissions')|list|flatten|sum(attribute='max_students')|sum }}</h2>
|
||||||
|
<p class="mb-0">Total Student Capacity</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h2 class="text-warning">{{ subjects|selectattr('commissions')|list|length }}</h2>
|
||||||
|
<p class="mb-0">Subjects with Commissions</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subjects and Commissions -->
|
||||||
|
<div class="row">
|
||||||
|
{% for subject in subjects %}
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-header bg-white border-0">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">
|
||||||
|
<span class="badge bg-primary me-2">{{ subject.code }}</span>
|
||||||
|
{{ subject.name }}
|
||||||
|
</h5>
|
||||||
|
<p class="text-muted mb-0 small">
|
||||||
|
<i class="bi bi-building"></i> {{ subject.department }} •
|
||||||
|
<i class="bi bi-award"></i> {{ subject.credits }} credits
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="badge bg-info">{{ subject.commissions|length }} commission{{ 's' if subject.commissions|length != 1 else '' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if subject.commissions %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Commission</th>
|
||||||
|
<th>Semester</th>
|
||||||
|
<th>Teacher</th>
|
||||||
|
<th>Students</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for commission in subject.commissions %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>{{ commission.code }}</strong>
|
||||||
|
{% if commission.schedule %}
|
||||||
|
<br><small class="text-muted">{{ commission.schedule }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-light text-dark">{{ commission.semester }} {{ commission.year }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if commission.teacher %}
|
||||||
|
<small>{{ commission.teacher.first_name }} {{ commission.teacher.last_name }}</small>
|
||||||
|
{% else %}
|
||||||
|
<small class="text-muted">Not assigned</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<small>{{ commission.current_students }}/{{ commission.max_students }}</small>
|
||||||
|
{% set fill_percentage = (commission.current_students / commission.max_students * 100) %}
|
||||||
|
<div class="progress ms-2" style="height: 4px; width: 30px;">
|
||||||
|
<div class="progress-bar
|
||||||
|
{% if fill_percentage > 90 %}bg-danger
|
||||||
|
{% elif fill_percentage > 75 %}bg-warning
|
||||||
|
{% else %}bg-success
|
||||||
|
{% endif %}"
|
||||||
|
style="width: {{ fill_percentage }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if commission.active %}
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<button class="btn btn-outline-primary quick-reserve-btn"
|
||||||
|
data-commission-id="{{ commission.id }}"
|
||||||
|
data-max-students="{{ commission.max_students }}"
|
||||||
|
data-commission-name="{{ commission.get_full_code() }}"
|
||||||
|
data-subject-name="{{ subject.name }}"
|
||||||
|
title="Quick Reservation">
|
||||||
|
<i class="bi bi-calendar-plus"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-info view-schedule-btn"
|
||||||
|
data-commission-id="{{ commission.id }}"
|
||||||
|
title="View Schedule">
|
||||||
|
<i class="bi bi-calendar-week"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center text-muted py-3">
|
||||||
|
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
|
||||||
|
No commissions registered for this subject
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if subject.description %}
|
||||||
|
<div class="card-footer bg-light">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="bi bi-info-circle"></i> {{ subject.description|truncatewords(20) }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="bi bi-inbox fs-1 text-muted d-block mb-3"></i>
|
||||||
|
<h4 class="text-muted">No Subjects Found</h4>
|
||||||
|
<p class="text-muted">There are no active subjects in the system.</p>
|
||||||
|
<a href="{{ url_for('classrooms.index') }}" class="btn btn-primary">
|
||||||
|
<i class="bi bi-plus-circle"></i> Add New Subject
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Reservation Modal -->
|
||||||
|
<div class="modal fade" id="quickReservationModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<i class="bi bi-calendar-plus text-success"></i> Quick Reservation
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="quick-reservation-form">
|
||||||
|
<input type="hidden" id="quick-commission-id" name="commission_id">
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-info-circle"></i> Commission Details
|
||||||
|
</h6>
|
||||||
|
<div id="commission-details"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="quick-date" class="form-label">Date</label>
|
||||||
|
<input type="date" class="form-control" id="quick-date" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="quick-start-time" class="form-label">Start Time</label>
|
||||||
|
<input type="time" class="form-control" id="quick-start-time" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="quick-end-time" class="form-label">End Time</label>
|
||||||
|
<input type="time" class="form-control" id="quick-end-time" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-3">
|
||||||
|
<label for="quick-purpose" class="form-label">Purpose</label>
|
||||||
|
<input type="text" class="form-control" id="quick-purpose" placeholder="Regular class, exam, review, etc." required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Classroom Selection & Availability -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Select Classroom</label>
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<select class="form-select" id="quick-classroom-select" required>
|
||||||
|
<option value="">Choose date and time first</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="btn btn-outline-primary ms-2" id="check-availability-btn" disabled>
|
||||||
|
<i class="bi bi-search"></i> Check
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="availability-results" class="mt-3"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-success" id="create-reservation-btn" disabled>
|
||||||
|
<i class="bi bi-plus-circle"></i> Create Reservation
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Quick Reservation Button Click
|
||||||
|
$('.quick-reserve-btn').on('click', function() {
|
||||||
|
const commissionId = $(this).data('commission-id');
|
||||||
|
const maxStudents = $(this).data('max-students');
|
||||||
|
const commissionName = $(this).data('commission-name');
|
||||||
|
const subjectName = $(this).data('subject-name');
|
||||||
|
|
||||||
|
// Set commission details in modal
|
||||||
|
$('#quick-commission-id').val(commissionId);
|
||||||
|
$('#commission-details').html(`
|
||||||
|
<strong>${commissionName}</strong><br>
|
||||||
|
<small>Subject: ${subjectName}</small><br>
|
||||||
|
<small>Max Students: ${maxStudents}</small>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
$('#quick-date').val('');
|
||||||
|
$('#quick-start-time').val('');
|
||||||
|
$('#quick-end-time').val('');
|
||||||
|
$('#quick-purpose').val('');
|
||||||
|
$('#quick-classroom-select').html('<option value="">Choose date and time first</option>').prop('disabled', true);
|
||||||
|
$('#check-availability-btn').prop('disabled', true);
|
||||||
|
$('#create-reservation-btn').prop('disabled', true);
|
||||||
|
$('#availability-results').empty();
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
$('#quickReservationModal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable date/time inputs to check availability
|
||||||
|
function validateDateTimeInputs() {
|
||||||
|
const date = $('#quick-date').val();
|
||||||
|
const startTime = $('#quick-start-time').val();
|
||||||
|
const endTime = $('#quick-end-time').val();
|
||||||
|
|
||||||
|
const isValid = date && startTime && endTime && startTime < endTime;
|
||||||
|
$('#check-availability-btn').prop('disabled', !isValid);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
loadClassroomOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#quick-date, #quick-start-time, #quick-end-time').on('change', validateDateTimeInputs);
|
||||||
|
|
||||||
|
function loadClassroomOptions() {
|
||||||
|
$.get('/schedule/api/classroom-availability', {
|
||||||
|
date: $('#quick-date').val(),
|
||||||
|
start_time: $('#quick-start-time').val(),
|
||||||
|
end_time: $('#quick-end-time').val(),
|
||||||
|
max_students: parseInt($('#quick-commission-id').data('max-students'))
|
||||||
|
}, function(response) {
|
||||||
|
const classroomSelect = $('#quick-classroom-select');
|
||||||
|
classroomSelect.empty().append('<option value="">Select a classroom</option>');
|
||||||
|
|
||||||
|
// Add available classrooms
|
||||||
|
response.availability.available.forEach(function(classroom) {
|
||||||
|
classroomSelect.append(`<option value="${classroom.id}">${classroom.code} - ${classroom.name} (Cap: ${classroom.capacity}) - ${classroom.location}</option>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable selection
|
||||||
|
classroomSelect.prop('disabled', response.availability.available.length === 0);
|
||||||
|
|
||||||
|
// Show recommendation if available
|
||||||
|
if (response.recommendation) {
|
||||||
|
const rec = response.recommendation.classroom;
|
||||||
|
$('#availability-results').html(`
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<h6><i class="bi bi-star-fill"></i> Recommended Classroom</h6>
|
||||||
|
<strong>${rec.code} - ${rec.name}</strong><br>
|
||||||
|
<small>Capacity: ${rec.capacity} | Location: ${rec.location}</small><br>
|
||||||
|
<small class="text-muted">${response.recommendation.reason}</small>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show statistics
|
||||||
|
$('#availability-results').append(`
|
||||||
|
<div class="alert alert-info mt-2">
|
||||||
|
<small>
|
||||||
|
<strong>Availability:</strong> ${response.availability.stats.total_available} available,
|
||||||
|
${response.availability.stats.total_occupied} occupied classrooms
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check availability button click
|
||||||
|
$('#check-availability-btn').on('click', loadClassroomOptions);
|
||||||
|
|
||||||
|
// Classroom selection enables create button
|
||||||
|
$('#quick-classroom-select').on('change', function() {
|
||||||
|
const isValid = $(this).val() && $('#quick-purpose').val();
|
||||||
|
$('#create-reservation-btn').prop('disabled', !isValid);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Purpose input validation
|
||||||
|
$('#quick-purpose').on('input', function() {
|
||||||
|
const isValid = $('#quick-classroom-select').val() && $(this).val();
|
||||||
|
$('#create-reservation-btn').prop('disabled', !isValid);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create reservation
|
||||||
|
$('#create-reservation-btn').on('click', function() {
|
||||||
|
const formData = {
|
||||||
|
commission_id: $('#quick-commission-id').val(),
|
||||||
|
classroom_id: $('#quick-classroom-select').val(),
|
||||||
|
purpose: $('#quick-purpose').val(),
|
||||||
|
start_time: `${$('#quick-date').val()} ${$('#quick-start-time').val()}`,
|
||||||
|
end_time: `${$('#quick-date').val()} ${$('#quick-end-time').val()}`,
|
||||||
|
expected_attendees: $('#quick-commission-id').data('max-students')
|
||||||
|
};
|
||||||
|
|
||||||
|
$.post('/schedule/reservation/add', formData)
|
||||||
|
.done(function() {
|
||||||
|
$('#quickReservationModal').modal('hide');
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
.fail(function(xhr) {
|
||||||
|
alert('Error creating reservation: ' + xhr.responseJSON.error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Add Reservation - 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> New Reservation
|
|
||||||
</h1>
|
|
||||||
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary">
|
|
||||||
<i class="bi bi-arrow-left"></i> Back to Reservations
|
|
||||||
</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 bg-white border-0">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-calendar3 text-success"></i> Reservation Details
|
|
||||||
</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">Select a classroom for your reservation</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">Select the subject/commission</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">When will the class start?</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">When will the class end?</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">Number of expected attendees</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> Reservation Summary
|
|
||||||
</h6>
|
|
||||||
<div id="summary-content">
|
|
||||||
<p class="mb-2">
|
|
||||||
<strong>Selected Classroom:</strong>
|
|
||||||
<span id="summary-classroom">Not selected</span>
|
|
||||||
</p>
|
|
||||||
<p class="mb-2">
|
|
||||||
<strong>Duration:</strong>
|
|
||||||
<span id="summary-duration">Not calculated</span>
|
|
||||||
</p>
|
|
||||||
<p class="mb-0">
|
|
||||||
<strong>Commission:</strong>
|
|
||||||
<span id="summary-commission">Not selected</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>
|
|
||||||
{{ 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 bg-white border-0">
|
|
||||||
<h5 class="mb-0">
|
|
||||||
<i class="bi bi-question-circle text-info"></i> Reservation Guidelines
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h6 class="text-primary">📅 Booking Rules</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>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h6 class="text-warning">🚨 Important Notes</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>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
|
||||||
$(document).ready(function() {
|
|
||||||
// Classroom data for capacity checking
|
|
||||||
const classroomData = {};
|
|
||||||
{% for classroom in classroom_list %}
|
|
||||||
classroomData[{{ classroom.id }}] = {
|
|
||||||
capacity: {{ classroom.capacity }},
|
|
||||||
code: "{{ classroom.code }}"
|
|
||||||
};
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
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]) {
|
|
||||||
$('#summary-classroom').text(`${classroomData[classroomId].code} (Capacity: ${classroomData[classroomId].capacity})`);
|
|
||||||
} else {
|
|
||||||
$('#summary-classroom').text('Not selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update commission summary
|
|
||||||
if (commissionId) {
|
|
||||||
const commissionText = $('#commission-select option:selected').text();
|
|
||||||
$('#summary-commission').text(commissionText);
|
|
||||||
} else {
|
|
||||||
$('#summary-commission').text('Not selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
durationText += `${diffHours} hour${diffHours > 1 ? 's' : ''}`;
|
|
||||||
}
|
|
||||||
if (diffMins > 0) {
|
|
||||||
if (durationText) durationText += ' ';
|
|
||||||
durationText += `${diffMins} minute${diffMins > 1 ? 's' : ''}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#summary-duration').text(durationText);
|
|
||||||
} else {
|
|
||||||
$('#summary-duration').text('Invalid time range');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$('#summary-duration').text('Not calculated');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
$('#expected_attendees').after(`
|
|
||||||
<div class="invalid-feedback">
|
|
||||||
Expected attendees (${attendees}) exceeds classroom capacity (${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 %}
|
|
||||||
Reference in New Issue
Block a user