feat: separacion de aulas fisicas y campus virtual a demanda con criterio cloud de concurrencia ilimitada por comision
This commit is contained in:
+160
-58
@@ -89,49 +89,34 @@ def add_classroom():
|
||||
@classrooms_bp.route('/list')
|
||||
@login_required
|
||||
def list_classrooms():
|
||||
"""List all classrooms with Building and Floor filters"""
|
||||
"""List all classrooms with separation between Physical spaces and Virtual on-demand rooms"""
|
||||
try:
|
||||
from app.models.building import Building
|
||||
page = request.args.get('page', 1, type=int)
|
||||
active_only = request.args.get('active_only', 'true').lower() == 'true'
|
||||
search = request.args.get('search', '', type=str)
|
||||
building_filter = request.args.get('building_id', '', type=str).strip()
|
||||
floor_filter = request.args.get('floor', '', type=str).strip()
|
||||
|
||||
query = Classroom.query
|
||||
|
||||
if active_only:
|
||||
query = query.filter_by(is_active=True)
|
||||
|
||||
if building_filter and building_filter.isdigit():
|
||||
query = query.filter(Classroom.building_id == int(building_filter))
|
||||
|
||||
if floor_filter:
|
||||
flr = str(floor_filter).strip()
|
||||
if flr.upper() in ['0', 'PB', 'PLANTA BAJA']:
|
||||
query = query.filter(db.or_(Classroom.floor == '0', Classroom.floor.ilike('%PB%'), Classroom.floor.ilike('%Planta Baja%')))
|
||||
else:
|
||||
query = query.filter(Classroom.floor == flr)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
Classroom.building.ilike(f'%{search}%'),
|
||||
Classroom.code.ilike(f'%{search}%'),
|
||||
Classroom.description.ilike(f'%{search}%')
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
from app.models.reservation import Reservation, ReservationStatus
|
||||
from app.models.subject import Commission, Subject
|
||||
import calendar
|
||||
from datetime import date, timedelta
|
||||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
active_only = request.args.get('active_only', 'true').lower() == 'true'
|
||||
search = request.args.get('search', '', type=str).strip()
|
||||
building_filter = request.args.get('building_id', '', type=str).strip()
|
||||
floor_filter = request.args.get('floor', '', type=str).strip()
|
||||
view_mode = request.args.get('view_mode', '').strip().lower()
|
||||
|
||||
# Identificar edificio virtual y aula virtual base
|
||||
virtual_building = Building.query.filter(
|
||||
db.or_(Building.code == 'VIRTUAL', Building.name.ilike('%Virtual%'))
|
||||
).first()
|
||||
virtual_bldg_id = virtual_building.id if virtual_building else None
|
||||
|
||||
# Si el usuario seleccionó "Campus Virtual" en el filtro de edificios, conmutar a pestaña virtual
|
||||
if building_filter and str(building_filter) == str(virtual_bldg_id):
|
||||
view_mode = 'virtual'
|
||||
elif not view_mode:
|
||||
view_mode = 'physical'
|
||||
|
||||
# Fechas operativas (Día, Semana, Mes)
|
||||
today = datetime.utcnow().date()
|
||||
today_start = datetime.combine(today, datetime.min.time())
|
||||
today_end = datetime.combine(today, datetime.max.time())
|
||||
@@ -143,8 +128,62 @@ def list_classrooms():
|
||||
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
|
||||
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')
|
||||
}
|
||||
|
||||
# --- AULAS FÍSICAS (Presenciales) ---
|
||||
physical_query = Classroom.query
|
||||
if virtual_bldg_id:
|
||||
physical_query = physical_query.filter(Classroom.building_id != virtual_bldg_id)
|
||||
else:
|
||||
physical_query = physical_query.filter(~Classroom.code.ilike('%VIRTUAL%'))
|
||||
|
||||
if active_only:
|
||||
physical_query = physical_query.filter_by(is_active=True)
|
||||
|
||||
physical_count = physical_query.count()
|
||||
|
||||
if building_filter and building_filter.isdigit() and str(building_filter) != str(virtual_bldg_id):
|
||||
physical_query = physical_query.filter(Classroom.building_id == int(building_filter))
|
||||
|
||||
if floor_filter:
|
||||
flr = str(floor_filter).strip()
|
||||
if flr.upper() in ['0', 'PB', 'PLANTA BAJA']:
|
||||
physical_query = physical_query.filter(db.or_(Classroom.floor == '0', Classroom.floor.ilike('%PB%'), Classroom.floor.ilike('%Planta Baja%')))
|
||||
else:
|
||||
physical_query = physical_query.filter(Classroom.floor == flr)
|
||||
|
||||
if search and view_mode == 'physical':
|
||||
physical_query = physical_query.filter(
|
||||
db.or_(
|
||||
Classroom.building.ilike(f'%{search}%'),
|
||||
Classroom.code.ilike(f'%{search}%'),
|
||||
Classroom.description.ilike(f'%{search}%')
|
||||
)
|
||||
)
|
||||
|
||||
classrooms = physical_query.order_by(Classroom.building_id.asc(), Classroom.floor.asc(), Classroom.code.asc()).paginate(
|
||||
page=page, per_page=20, error_out=False
|
||||
)
|
||||
|
||||
# Edificios y Pisos estrictamente físicos para el filtro
|
||||
buildings = Building.query.filter_by(is_active=True)
|
||||
if virtual_bldg_id:
|
||||
buildings = buildings.filter(Building.id != virtual_bldg_id)
|
||||
buildings = buildings.order_by(Building.name.asc()).all()
|
||||
|
||||
floors = [f[0] for f in db.session.query(Classroom.floor).filter(
|
||||
Classroom.is_active == True,
|
||||
Classroom.building_id != virtual_bldg_id if virtual_bldg_id else True,
|
||||
Classroom.floor.isnot(None)
|
||||
).distinct().order_by(Classroom.floor.asc()).all()]
|
||||
|
||||
# Métricas de ocupación física
|
||||
visible_ids = [c.id for c in classrooms.items]
|
||||
month_reservations = Reservation.query.filter(
|
||||
Reservation.classroom_id.in_(visible_ids),
|
||||
@@ -153,10 +192,6 @@ def list_classrooms():
|
||||
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
|
||||
@@ -164,20 +199,16 @@ def list_classrooms():
|
||||
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)
|
||||
day_pct = min(100, int(round((day_mins / DAILY_OP_MINS) * 100)))
|
||||
|
||||
# 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)
|
||||
week_pct = min(100, int(round((week_mins / WEEKLY_OP_MINS) * 100)))
|
||||
|
||||
# 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)
|
||||
month_pct = min(100, int(round((month_mins / MONTHLY_OP_MINS) * 100)))
|
||||
|
||||
occupancy_metrics[c.id] = {
|
||||
'day_pct': day_pct,
|
||||
@@ -188,12 +219,77 @@ def list_classrooms():
|
||||
'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')
|
||||
# --- AULAS VIRTUALES (Campus Virtual a Demanda - Infinitas) ---
|
||||
vc = Classroom.query.filter(
|
||||
Classroom.building_id == virtual_bldg_id if virtual_bldg_id else Classroom.code.ilike('%VIRTUAL%')
|
||||
).first()
|
||||
vc_id = vc.id if vc else 1
|
||||
|
||||
all_vres = Reservation.query.filter(Reservation.classroom_id == vc_id).all() if vc_id else []
|
||||
|
||||
# Agrupar las salas virtuales por comisión
|
||||
comm_dict = {}
|
||||
for r in all_vres:
|
||||
c = r.commission
|
||||
cid = c.id if c else 0
|
||||
if cid not in comm_dict:
|
||||
vlink = (c.virtual_link if c and c.virtual_link else None) or r.effective_virtual_link
|
||||
comm_dict[cid] = {
|
||||
'commission_id': cid,
|
||||
'commission_code': c.code if c else 'General',
|
||||
'subject_name': c.subject.name if (c and c.subject) else (r.purpose or 'Clase Virtual'),
|
||||
'subject_code': c.subject.code if (c and c.subject) else '',
|
||||
'career_name': c.subject.career.name if (c and c.subject and hasattr(c.subject, 'career') and c.subject.career) else '',
|
||||
'max_students': c.max_students if c else (r.expected_attendees or 0),
|
||||
'virtual_link': vlink,
|
||||
'reservations': []
|
||||
}
|
||||
comm_dict[cid]['reservations'].append(r)
|
||||
if not comm_dict[cid]['virtual_link'] and r.effective_virtual_link:
|
||||
comm_dict[cid]['virtual_link'] = r.effective_virtual_link
|
||||
|
||||
virtual_rooms = []
|
||||
for cid, data in comm_dict.items():
|
||||
res_list = data['reservations']
|
||||
day_cnt = sum(1 for r in res_list if today_start <= r.start_time <= today_end)
|
||||
week_cnt = sum(1 for r in res_list if week_start <= r.start_time <= week_end)
|
||||
month_cnt = sum(1 for r in res_list if month_start <= r.start_time <= month_end)
|
||||
shift = res_list[0].shift if res_list else 'A Demanda'
|
||||
|
||||
if search and view_mode == 'virtual':
|
||||
s_low = search.lower()
|
||||
matches = (
|
||||
s_low in data['subject_name'].lower() or
|
||||
s_low in data['commission_code'].lower() or
|
||||
s_low in data['subject_code'].lower() or
|
||||
s_low in data['career_name'].lower()
|
||||
)
|
||||
if not matches:
|
||||
continue
|
||||
|
||||
virtual_rooms.append({
|
||||
'commission_id': data['commission_id'],
|
||||
'commission_code': data['commission_code'],
|
||||
'subject_name': data['subject_name'],
|
||||
'subject_code': data['subject_code'],
|
||||
'career_name': data['career_name'],
|
||||
'max_students': data['max_students'],
|
||||
'virtual_link': data['virtual_link'],
|
||||
'shift': shift,
|
||||
'day_count': day_cnt,
|
||||
'week_count': week_cnt,
|
||||
'month_count': month_cnt,
|
||||
'total_count': len(res_list)
|
||||
})
|
||||
|
||||
virtual_rooms.sort(key=lambda x: (x['subject_name'], x['commission_code']))
|
||||
|
||||
global_virtual_metrics = {
|
||||
'today_count': sum(1 for r in all_vres if today_start <= r.start_time <= today_end),
|
||||
'week_count': sum(1 for r in all_vres if week_start <= r.start_time <= week_end),
|
||||
'month_count': sum(1 for r in all_vres if month_start <= r.start_time <= month_end),
|
||||
'total_count': len(all_vres),
|
||||
'commissions_count': len(comm_dict)
|
||||
}
|
||||
|
||||
return render_template('classrooms/list.html',
|
||||
@@ -204,6 +300,12 @@ def list_classrooms():
|
||||
floor_filter=floor_filter,
|
||||
active_only=active_only,
|
||||
search=search,
|
||||
view_mode=view_mode,
|
||||
physical_count=physical_count,
|
||||
virtual_count=len(comm_dict),
|
||||
virtual_rooms=virtual_rooms,
|
||||
virtual_classroom_id=vc_id,
|
||||
global_virtual_metrics=global_virtual_metrics,
|
||||
occupancy_metrics=occupancy_metrics,
|
||||
date_filters=date_filters,
|
||||
title='Classrooms')
|
||||
|
||||
Reference in New Issue
Block a user