feat: optimizacion zocalos aulas, metricas ocupacion dia/semana/mes, modificacion de reservas y permisos RBAC en gestion
This commit is contained in:
@@ -122,13 +122,80 @@ def list_classrooms():
|
||||
)
|
||||
)
|
||||
|
||||
classrooms = query.order_by(Classroom.building, Classroom.floor, Classroom.code).paginate(
|
||||
page=page, per_page=12, error_out=False
|
||||
classrooms = query.order_by(Classroom.building_id.asc(), Classroom.floor.asc(), Classroom.code.asc()).paginate(
|
||||
page=page, per_page=20, error_out=False
|
||||
)
|
||||
|
||||
buildings = Building.query.filter_by(is_active=True).order_by(Building.name.asc()).all()
|
||||
floors = [f[0] for f in db.session.query(Classroom.floor).filter(Classroom.is_active==True, Classroom.floor.isnot(None)).distinct().order_by(Classroom.floor.asc()).all()]
|
||||
|
||||
# Fechas y cálculo de métricas de ocupación (Día, Semana, Mes)
|
||||
import calendar
|
||||
from datetime import date, timedelta
|
||||
today = datetime.utcnow().date()
|
||||
today_start = datetime.combine(today, datetime.min.time())
|
||||
today_end = datetime.combine(today, datetime.max.time())
|
||||
|
||||
week_start = datetime.combine(today - timedelta(days=today.weekday()), datetime.min.time())
|
||||
week_end = datetime.combine(week_start.date() + timedelta(days=6), datetime.max.time())
|
||||
|
||||
_, last_day = calendar.monthrange(today.year, today.month)
|
||||
month_start = datetime(today.year, today.month, 1, 0, 0, 0)
|
||||
month_end = datetime(today.year, today.month, last_day, 23, 59, 59)
|
||||
|
||||
# Consultar en lote las reservas del mes para las aulas visibles
|
||||
from app.models.reservation import Reservation, ReservationStatus
|
||||
visible_ids = [c.id for c in classrooms.items]
|
||||
month_reservations = Reservation.query.filter(
|
||||
Reservation.classroom_id.in_(visible_ids),
|
||||
Reservation.start_time >= month_start,
|
||||
Reservation.start_time <= month_end,
|
||||
Reservation.status.in_([ReservationStatus.CONFIRMED.value, ReservationStatus.PENDING.value])
|
||||
).all() if visible_ids else []
|
||||
|
||||
# Horas base operativas académicas:
|
||||
# Día: 15 hs (07:00 a 22:00 = 900 min)
|
||||
# Semana: 6 días x 15 hs = 5400 min
|
||||
# Mes: ~24 días hábiles x 15 hs = 21600 min
|
||||
DAILY_OP_MINS = 15 * 60
|
||||
WEEKLY_OP_MINS = 6 * 15 * 60
|
||||
MONTHLY_OP_MINS = 24 * 15 * 60
|
||||
|
||||
occupancy_metrics = {}
|
||||
for c in classrooms.items:
|
||||
c_res = [r for r in month_reservations if r.classroom_id == c.id]
|
||||
|
||||
# Día
|
||||
day_res = [r for r in c_res if today_start <= r.start_time <= today_end]
|
||||
day_mins = sum((min(r.end_time, today_end) - max(r.start_time, today_start)).total_seconds() / 60 for r in day_res)
|
||||
day_pct = min(100, int(round((day_mins / DAILY_OP_MINS) * 100))) if not c.is_virtual else len(day_res)
|
||||
|
||||
# Semana
|
||||
week_res = [r for r in c_res if week_start <= r.start_time <= week_end]
|
||||
week_mins = sum((min(r.end_time, week_end) - max(r.start_time, week_start)).total_seconds() / 60 for r in week_res)
|
||||
week_pct = min(100, int(round((week_mins / WEEKLY_OP_MINS) * 100))) if not c.is_virtual else len(week_res)
|
||||
|
||||
# Mes
|
||||
month_mins = sum((min(r.end_time, month_end) - max(r.start_time, month_start)).total_seconds() / 60 for r in c_res)
|
||||
month_pct = min(100, int(round((month_mins / MONTHLY_OP_MINS) * 100))) if not c.is_virtual else len(c_res)
|
||||
|
||||
occupancy_metrics[c.id] = {
|
||||
'day_pct': day_pct,
|
||||
'day_count': len(day_res),
|
||||
'week_pct': week_pct,
|
||||
'week_count': len(week_res),
|
||||
'month_pct': month_pct,
|
||||
'month_count': len(c_res)
|
||||
}
|
||||
|
||||
date_filters = {
|
||||
'today': today.strftime('%Y-%m-%d'),
|
||||
'week_start': week_start.strftime('%Y-%m-%d'),
|
||||
'week_end': week_end.strftime('%Y-%m-%d'),
|
||||
'month_start': month_start.strftime('%Y-%m-%d'),
|
||||
'month_end': month_end.strftime('%Y-%m-%d')
|
||||
}
|
||||
|
||||
return render_template('classrooms/list.html',
|
||||
classrooms=classrooms,
|
||||
buildings=buildings,
|
||||
@@ -137,6 +204,8 @@ def list_classrooms():
|
||||
floor_filter=floor_filter,
|
||||
active_only=active_only,
|
||||
search=search,
|
||||
occupancy_metrics=occupancy_metrics,
|
||||
date_filters=date_filters,
|
||||
title='Classrooms')
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -139,6 +139,92 @@ def view_reservation(id):
|
||||
flash(f'Error loading reservation details: {str(e)}', 'error')
|
||||
return redirect(url_for('schedule.today_schedule'))
|
||||
|
||||
@schedule_bp.route('/reservation/<int:id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_reservation(id):
|
||||
"""Edit existing reservation"""
|
||||
from app.models.classroom import Classroom
|
||||
from app.models.subject import Commission
|
||||
from app.models.building import Building
|
||||
from flask_babel import gettext as _
|
||||
|
||||
reservation = Reservation.query.options(
|
||||
joinedload(Reservation.classroom),
|
||||
joinedload(Reservation.commission),
|
||||
joinedload(Reservation.user)
|
||||
).get_or_404(id)
|
||||
|
||||
if not current_user.can_edit_reservation(reservation):
|
||||
flash(_('No tienes los permisos necesarios para modificar esta reserva.'), 'error')
|
||||
return redirect(url_for('schedule.view_reservation', id=reservation.id))
|
||||
|
||||
form = ReservationForm(is_edit=True)
|
||||
|
||||
buildings = Building.query.filter_by(is_active=True).order_by(Building.name.asc()).all()
|
||||
classrooms = Classroom.query.filter_by(is_active=True).order_by(Classroom.building_id.asc(), Classroom.floor.asc(), Classroom.code.asc()).all()
|
||||
commissions = Commission.query.filter_by(active=True).all()
|
||||
|
||||
form.classroom_id.choices = [(c.id, f'{c.building_name} - {c.code} (Cap: {c.capacity_display})') 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 == 'GET':
|
||||
form.classroom_id.data = reservation.classroom_id
|
||||
form.commission_id.data = reservation.commission_id
|
||||
form.start_time.data = reservation.start_time
|
||||
form.end_time.data = reservation.end_time
|
||||
form.purpose.data = reservation.purpose
|
||||
form.expected_attendees.data = reservation.expected_attendees
|
||||
form.notes.data = reservation.notes or ''
|
||||
form.virtual_link.data = reservation.virtual_link or ''
|
||||
|
||||
elif request.method == 'POST':
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
# Verificar colisión de horario si no es virtual
|
||||
target_room = Classroom.query.get(form.classroom_id.data)
|
||||
if target_room and not target_room.is_virtual:
|
||||
conflict = Reservation.query.filter(
|
||||
Reservation.id != reservation.id,
|
||||
Reservation.classroom_id == form.classroom_id.data,
|
||||
Reservation.status == ReservationStatus.CONFIRMED.value,
|
||||
Reservation.start_time < form.end_time.data,
|
||||
Reservation.end_time > form.start_time.data
|
||||
).first()
|
||||
|
||||
if conflict:
|
||||
flash(_('El aula seleccionada ya cuenta con una reserva confirmada en esa franja horaria.'), 'danger')
|
||||
return render_template('schedule/edit.html', form=form, reservation=reservation, classroom_list=classrooms, commission_list=commissions, buildings=buildings)
|
||||
|
||||
# Asignar cambios
|
||||
reservation.classroom_id = form.classroom_id.data
|
||||
reservation.commission_id = form.commission_id.data
|
||||
reservation.start_time = form.start_time.data
|
||||
reservation.end_time = form.end_time.data
|
||||
reservation.purpose = form.purpose.data
|
||||
reservation.expected_attendees = form.expected_attendees.data
|
||||
reservation.notes = form.notes.data
|
||||
reservation.virtual_link = form.virtual_link.data.strip() if form.virtual_link.data else None
|
||||
|
||||
# Turno
|
||||
h = reservation.start_time.hour
|
||||
if h < 13:
|
||||
reservation.shift = 'Mañana'
|
||||
elif h < 18:
|
||||
reservation.shift = 'Tarde'
|
||||
elif h < 21:
|
||||
reservation.shift = 'Vespertino'
|
||||
else:
|
||||
reservation.shift = 'Noche'
|
||||
|
||||
db.session.commit()
|
||||
flash(_('Reserva #%(id)d modificada exitosamente.', id=reservation.id), 'success')
|
||||
return redirect(url_for('schedule.view_reservation', id=reservation.id))
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'Error al modificar reserva: {str(e)}', 'danger')
|
||||
|
||||
return render_template('schedule/edit.html', form=form, reservation=reservation, classroom_list=classrooms, commission_list=commissions, buildings=buildings)
|
||||
|
||||
@schedule_bp.route('/calendar')
|
||||
@login_required
|
||||
def calendar_view():
|
||||
@@ -518,6 +604,7 @@ def list_reservations():
|
||||
hide_virtual = request.args.get('hide_virtual', '0') in ['1', 'true', 'True']
|
||||
floor_filter = request.args.get('floor', '').strip()
|
||||
shift_filter = request.args.get('shift', '').strip()
|
||||
classroom_id = request.args.get('classroom_id', type=int)
|
||||
search = request.args.get('search', '').strip()
|
||||
|
||||
query = Reservation.query.options(
|
||||
@@ -526,6 +613,9 @@ def list_reservations():
|
||||
joinedload(Reservation.user)
|
||||
).join(Reservation.classroom).outerjoin(Reservation.commission).outerjoin(Commission.subject)
|
||||
|
||||
if classroom_id:
|
||||
query = query.filter(Reservation.classroom_id == classroom_id)
|
||||
|
||||
if status_filter and status_filter != 'all':
|
||||
query = query.filter(Reservation.status == status_filter.upper())
|
||||
|
||||
@@ -575,6 +665,7 @@ def list_reservations():
|
||||
|
||||
floors = db.session.query(Classroom.floor).filter(Classroom.is_active==True, Classroom.floor.isnot(None)).distinct().order_by(Classroom.floor.asc()).all()
|
||||
available_floors = [f[0] for f in floors]
|
||||
all_classrooms = Classroom.query.filter_by(is_active=True).order_by(Classroom.building_id.asc(), Classroom.floor.asc(), Classroom.code.asc()).all()
|
||||
|
||||
return render_template(
|
||||
'schedule/list.html',
|
||||
@@ -585,6 +676,8 @@ def list_reservations():
|
||||
hide_virtual=hide_virtual,
|
||||
floor_filter=floor_filter,
|
||||
shift_filter=shift_filter,
|
||||
classroom_id=classroom_id,
|
||||
all_classrooms=all_classrooms,
|
||||
search=search,
|
||||
available_floors=available_floors
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user