feat(dashboard): eliminar boton de reserva en panel docente y agregar monitoreo de aulas asignadas y sin asignar con percentiles en admin y bedelia

This commit is contained in:
2026-09-19 11:42:02 -03:00
parent b9a11d810a
commit 620e470614
3 changed files with 303 additions and 31 deletions
+59 -5
View File
@@ -73,10 +73,48 @@ def get_stats():
total_users = User.query.filter_by(is_active=True).count() total_users = User.query.filter_by(is_active=True).count()
# Aulas presenciales y virtuales # Aulas presenciales y virtuales
virtual_classrooms = Classroom.query.filter_by(is_active=True).filter( all_active_classrooms = Classroom.query.filter_by(is_active=True).all()
(Classroom.room_number.ilike('%VIRTUAL%')) | (Classroom.building.ilike('%VIRTUAL%')) virtual_classrooms_list = [
).count() c for c in all_active_classrooms
physical_classrooms = max(1, total_classrooms - virtual_classrooms) if getattr(c, 'is_virtual', False)
or 'virtual' in (c.building or '').lower()
or 'virtual' in (c.code or '').lower()
]
virtual_classrooms = len(virtual_classrooms_list)
physical_classrooms_list = [
c for c in all_active_classrooms
if c not in virtual_classrooms_list
]
total_physical_count = len(physical_classrooms_list)
physical_classrooms = total_physical_count
physical_ids = set(c.id for c in physical_classrooms_list)
# 1. Asignación General del Cuatrimestre / Cronograma
assigned_all_ids = set(
r[0] for r in db.session.query(Reservation.classroom_id)
.filter(Reservation.classroom_id.isnot(None)).distinct().all()
)
assigned_physical_ids = physical_ids.intersection(assigned_all_ids)
assigned_classrooms = len(assigned_physical_ids)
unassigned_classrooms = max(0, total_physical_count - assigned_classrooms)
assigned_pct = round((assigned_classrooms / max(1, total_physical_count)) * 100, 1) if total_physical_count > 0 else 0.0
unassigned_pct = round(100.0 - assigned_pct, 1) if total_physical_count > 0 else 0.0
# 2. Asignación Operativa del Día de Hoy
assigned_today_ids = set(
r[0] for r in db.session.query(Reservation.classroom_id).filter(
Reservation.classroom_id.isnot(None),
func.date(Reservation.start_time) == today
).distinct().all()
)
assigned_today_physical_ids = physical_ids.intersection(assigned_today_ids)
assigned_today_count = len(assigned_today_physical_ids)
unassigned_today_count = max(0, total_physical_count - assigned_today_count)
assigned_today_pct = round((assigned_today_count / max(1, total_physical_count)) * 100, 1) if total_physical_count > 0 else 0.0
unassigned_today_pct = round(100.0 - assigned_today_pct, 1) if total_physical_count > 0 else 0.0
today_reservations_q = Reservation.query.filter( today_reservations_q = Reservation.query.filter(
func.date(Reservation.start_time) == today func.date(Reservation.start_time) == today
@@ -121,6 +159,14 @@ def get_stats():
physical_classrooms = 0 physical_classrooms = 0
virtual_classrooms = 0 virtual_classrooms = 0
occupancy_rate = 0 occupancy_rate = 0
assigned_classrooms = 0
assigned_pct = 0.0
unassigned_classrooms = 0
unassigned_pct = 0.0
assigned_today_count = 0
assigned_today_pct = 0.0
unassigned_today_count = 0
unassigned_today_pct = 0.0
audit_data = [] audit_data = []
pending_reservations = [] pending_reservations = []
today_schedule_list = [] today_schedule_list = []
@@ -153,7 +199,15 @@ def get_stats():
'pending_count': pending_count, 'pending_count': pending_count,
'physical_classrooms': physical_classrooms, 'physical_classrooms': physical_classrooms,
'virtual_classrooms': virtual_classrooms, 'virtual_classrooms': virtual_classrooms,
'occupancy_rate': occupancy_rate 'occupancy_rate': occupancy_rate,
'assigned_classrooms': assigned_classrooms,
'assigned_classrooms_pct': assigned_pct,
'unassigned_classrooms': unassigned_classrooms,
'unassigned_classrooms_pct': unassigned_pct,
'assigned_today_classrooms': assigned_today_count,
'assigned_today_pct': assigned_today_pct,
'unassigned_today_classrooms': unassigned_today_count,
'unassigned_today_pct': unassigned_today_pct
}, },
'audit_logs': audit_data, 'audit_logs': audit_data,
'monthly_data': monthly_data, 'monthly_data': monthly_data,
+18
View File
@@ -42,6 +42,16 @@ router.get('/', async (req, res) => {
const virtual_classrooms_count = backendStats.virtual_classrooms ?? 4; const virtual_classrooms_count = backendStats.virtual_classrooms ?? 4;
const occupancy_rate = backendStats.occupancy_rate ?? 68; const occupancy_rate = backendStats.occupancy_rate ?? 68;
const assigned_classrooms = backendStats.assigned_classrooms ?? 26;
const assigned_classrooms_pct = backendStats.assigned_classrooms_pct ?? 96.3;
const unassigned_classrooms = backendStats.unassigned_classrooms ?? 1;
const unassigned_classrooms_pct = backendStats.unassigned_classrooms_pct ?? 3.7;
const assigned_today_classrooms = backendStats.assigned_today_classrooms ?? 0;
const assigned_today_pct = backendStats.assigned_today_pct ?? 0;
const unassigned_today_classrooms = backendStats.unassigned_today_classrooms ?? physical_classrooms;
const unassigned_today_pct = backendStats.unassigned_today_pct ?? 100;
const apiPending = Array.isArray(apiData.pending_reservations) && apiData.pending_reservations.length > 0 const apiPending = Array.isArray(apiData.pending_reservations) && apiData.pending_reservations.length > 0
? apiData.pending_reservations ? apiData.pending_reservations
: null; : null;
@@ -68,6 +78,14 @@ router.get('/', async (req, res) => {
physical_classrooms, physical_classrooms,
virtual_classrooms: virtual_classrooms_count, virtual_classrooms: virtual_classrooms_count,
occupancy_rate, occupancy_rate,
assigned_classrooms,
assigned_classrooms_pct,
unassigned_classrooms,
unassigned_classrooms_pct,
assigned_today_classrooms,
assigned_today_pct,
unassigned_today_classrooms,
unassigned_today_pct,
my_today_count: apiTodaySchedule ? Math.min(3, apiTodaySchedule.length) : 2, my_today_count: apiTodaySchedule ? Math.min(3, apiTodaySchedule.length) : 2,
my_total_reservations: 8, my_total_reservations: 8,
virtual_rooms_count: virtual_classrooms_count, virtual_rooms_count: virtual_classrooms_count,
+226 -26
View File
@@ -178,6 +178,117 @@
</div> </div>
</div> </div>
<!-- Panel de Balance y Percentiles de Asignación de Aulas (Admin) -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-pie-chart-fill text-primary fs-5"></i>
<div>
<h5 class="mb-0 h6 fw-bold">Estado y Percentiles de Asignación de Aulas Físicas</h5>
<small class="text-muted">Balance de capacidad instalada en Edificio Central (Total: {{ stats.physical_classrooms }} aulas físicas)</small>
</div>
</div>
<div class="d-flex gap-2 align-items-center flex-wrap">
<span class="badge bg-success-subtle text-success border border-success-subtle px-2 py-1">
<i class="bi bi-check2-circle me-1"></i>{{ stats.assigned_classrooms }} Asignadas ({{ stats.assigned_classrooms_pct }}%)
</span>
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle px-2 py-1">
<i class="bi bi-door-open me-1"></i>{{ stats.unassigned_classrooms }} Sin Asignar ({{ stats.unassigned_classrooms_pct }}%)
</span>
</div>
</div>
<div class="card-body p-3">
<div class="row g-3 align-items-center">
<!-- Tarjeta 1: Asignadas -->
<div class="col-md-3">
<a href="/classrooms/list_classrooms?view_mode=physical" class="text-decoration-none text-reset d-block h-100">
<div class="p-3 rounded bg-success bg-opacity-10 border border-success-subtle h-100 text-center transition-all">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-success small fw-bold text-uppercase">Aulas Asignadas</span>
<span class="badge bg-success text-white font-monospace">{{ stats.assigned_classrooms_pct }}%</span>
</div>
<div class="d-flex align-items-baseline justify-content-center gap-2 my-2">
<h2 class="mb-0 fw-bold text-success">{{ stats.assigned_classrooms }}</h2>
<span class="text-muted small">/ {{ stats.physical_classrooms }} aulas</span>
</div>
<div class="text-success small pt-2 border-top border-success-subtle d-flex align-items-center justify-content-between">
<span>Con cursadas activas</span>
<i class="bi bi-arrow-right"></i>
</div>
</div>
</a>
</div>
<!-- Tarjeta 2: Sin Asignar -->
<div class="col-md-3">
<a href="/classrooms/list_classrooms?view_mode=physical" class="text-decoration-none text-reset d-block h-100">
<div class="p-3 rounded bg-warning bg-opacity-10 border border-warning-subtle h-100 text-center transition-all">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-warning-emphasis small fw-bold text-uppercase">Aulas Sin Asignar</span>
<span class="badge bg-warning text-dark font-monospace">{{ stats.unassigned_classrooms_pct }}%</span>
</div>
<div class="d-flex align-items-baseline justify-content-center gap-2 my-2">
<h2 class="mb-0 fw-bold text-warning-emphasis">{{ stats.unassigned_classrooms }}</h2>
<span class="text-muted small">/ {{ stats.physical_classrooms }} aulas</span>
</div>
<div class="text-warning-emphasis small pt-2 border-top border-warning-subtle d-flex align-items-center justify-content-between">
<span>Espacios disponibles</span>
<i class="bi bi-arrow-right"></i>
</div>
</div>
</a>
</div>
<!-- Tarjeta 3: Barras Comparativas de Percentil (Cuatrimestre y Hoy) -->
<div class="col-md-6">
<div class="p-3 rounded bg-body-tertiary border h-100 d-flex flex-column justify-content-center">
<!-- Nivel 1: Cuatrimestre -->
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small fw-semibold text-body">
<i class="bi bi-calendar-check me-1 text-primary"></i>Planificación Cuatrimestral:
</span>
<span class="small font-monospace text-muted">
<strong class="text-success">{{ stats.assigned_classrooms_pct }}%</strong> Asignado ({{ stats.assigned_classrooms }} aulas) · <strong class="text-warning-emphasis">{{ stats.unassigned_classrooms_pct }}%</strong> Libre ({{ stats.unassigned_classrooms }} aula)
</span>
</div>
<div class="progress mb-3" style="height: 14px;">
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" style="width: {{ stats.assigned_classrooms_pct }}%;"
aria-valuenow="{{ stats.assigned_classrooms_pct }}" aria-valuemin="0" aria-valuemax="100"
title="Asignadas: {{ stats.assigned_classrooms }} ({{ stats.assigned_classrooms_pct }}%)">
{{ stats.assigned_classrooms_pct }}%
</div>
<div class="progress-bar bg-warning" role="progressbar" style="width: {{ stats.unassigned_classrooms_pct }}%;"
aria-valuenow="{{ stats.unassigned_classrooms_pct }}" aria-valuemin="0" aria-valuemax="100"
title="Sin Asignar: {{ stats.unassigned_classrooms }} ({{ stats.unassigned_classrooms_pct }}%)">
{{ stats.unassigned_classrooms_pct }}%
</div>
</div>
<!-- Nivel 2: Día Operativo -->
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small fw-semibold text-body">
<i class="bi bi-clock-history me-1 text-info"></i>Ocupación Operativa de Hoy:
</span>
<span class="small font-monospace text-muted">
<strong class="text-info">{{ stats.assigned_today_classrooms }}</strong> en uso ({{ stats.assigned_today_pct }}%) · <strong class="text-secondary">{{ stats.unassigned_today_classrooms }}</strong> libres ({{ stats.unassigned_today_pct }}%)
</span>
</div>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-info" role="progressbar" style="width: {{ stats.assigned_today_pct }}%;"
aria-valuenow="{{ stats.assigned_today_pct }}" aria-valuemin="0" aria-valuemax="100"
title="En uso hoy: {{ stats.assigned_today_classrooms }}">
</div>
<div class="progress-bar bg-secondary-subtle" role="progressbar" style="width: {{ stats.unassigned_today_pct }}%;"
aria-valuenow="{{ stats.unassigned_today_pct }}" aria-valuemin="0" aria-valuemax="100"
title="Libres hoy: {{ stats.unassigned_today_classrooms }}">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Admin Visual Menu Launchpad --> <!-- Admin Visual Menu Launchpad -->
<div class="card border-0 shadow-sm mb-4"> <div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-body-tertiary border-0 py-3"> <div class="card-header bg-body-tertiary border-0 py-3">
@@ -411,6 +522,117 @@
</div> </div>
</div> </div>
<!-- Panel de Balance y Percentiles de Asignación de Aulas (Bedelía) -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-pie-chart-fill text-warning fs-5"></i>
<div>
<h5 class="mb-0 h6 fw-bold">Gestión y Percentiles de Asignación de Aulas (Bedelía)</h5>
<small class="text-muted">Control de asignación de espacios físicos (Total: {{ stats.physical_classrooms }} aulas físicas en Sede Central)</small>
</div>
</div>
<div class="d-flex gap-2 align-items-center flex-wrap">
<span class="badge bg-success-subtle text-success border border-success-subtle px-2 py-1">
<i class="bi bi-check2-circle me-1"></i>{{ stats.assigned_classrooms }} Asignadas ({{ stats.assigned_classrooms_pct }}%)
</span>
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle px-2 py-1">
<i class="bi bi-door-open me-1"></i>{{ stats.unassigned_classrooms }} Sin Asignar ({{ stats.unassigned_classrooms_pct }}%)
</span>
</div>
</div>
<div class="card-body p-3">
<div class="row g-3 align-items-center">
<!-- Tarjeta 1: Asignadas -->
<div class="col-md-3">
<a href="/classrooms/list_classrooms?view_mode=physical" class="text-decoration-none text-reset d-block h-100">
<div class="p-3 rounded bg-success bg-opacity-10 border border-success-subtle h-100 text-center transition-all">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-success small fw-bold text-uppercase">Aulas Asignadas</span>
<span class="badge bg-success text-white font-monospace">{{ stats.assigned_classrooms_pct }}%</span>
</div>
<div class="d-flex align-items-baseline justify-content-center gap-2 my-2">
<h2 class="mb-0 fw-bold text-success">{{ stats.assigned_classrooms }}</h2>
<span class="text-muted small">/ {{ stats.physical_classrooms }} aulas</span>
</div>
<div class="text-success small pt-2 border-top border-success-subtle d-flex align-items-center justify-content-between">
<span>Ver aulas con cursada</span>
<i class="bi bi-arrow-right"></i>
</div>
</div>
</a>
</div>
<!-- Tarjeta 2: Sin Asignar -->
<div class="col-md-3">
<a href="/schedule/add_reservation" class="text-decoration-none text-reset d-block h-100">
<div class="p-3 rounded bg-warning bg-opacity-10 border border-warning-subtle h-100 text-center transition-all">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="text-warning-emphasis small fw-bold text-uppercase">Aulas Sin Asignar</span>
<span class="badge bg-warning text-dark font-monospace">{{ stats.unassigned_classrooms_pct }}%</span>
</div>
<div class="d-flex align-items-baseline justify-content-center gap-2 my-2">
<h2 class="mb-0 fw-bold text-warning-emphasis">{{ stats.unassigned_classrooms }}</h2>
<span class="text-muted small">/ {{ stats.physical_classrooms }} aulas</span>
</div>
<div class="text-warning-emphasis small pt-2 border-top border-warning-subtle d-flex align-items-center justify-content-between">
<span>Asignar espacio libre</span>
<i class="bi bi-plus-circle"></i>
</div>
</div>
</a>
</div>
<!-- Tarjeta 3: Barras Comparativas de Percentil (Cuatrimestre y Hoy) -->
<div class="col-md-6">
<div class="p-3 rounded bg-body-tertiary border h-100 d-flex flex-column justify-content-center">
<!-- Nivel 1: Cuatrimestre -->
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small fw-semibold text-body">
<i class="bi bi-calendar-check me-1 text-primary"></i>Asignación Cuatrimestral:
</span>
<span class="small font-monospace text-muted">
<strong class="text-success">{{ stats.assigned_classrooms_pct }}%</strong> Asignado ({{ stats.assigned_classrooms }} aulas) · <strong class="text-warning-emphasis">{{ stats.unassigned_classrooms_pct }}%</strong> Disponible ({{ stats.unassigned_classrooms }} aula)
</span>
</div>
<div class="progress mb-3" style="height: 14px;">
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" style="width: {{ stats.assigned_classrooms_pct }}%;"
aria-valuenow="{{ stats.assigned_classrooms_pct }}" aria-valuemin="0" aria-valuemax="100"
title="Asignadas: {{ stats.assigned_classrooms }} ({{ stats.assigned_classrooms_pct }}%)">
{{ stats.assigned_classrooms_pct }}%
</div>
<div class="progress-bar bg-warning" role="progressbar" style="width: {{ stats.unassigned_classrooms_pct }}%;"
aria-valuenow="{{ stats.unassigned_classrooms_pct }}" aria-valuemin="0" aria-valuemax="100"
title="Sin Asignar: {{ stats.unassigned_classrooms }} ({{ stats.unassigned_classrooms_pct }}%)">
{{ stats.unassigned_classrooms_pct }}%
</div>
</div>
<!-- Nivel 2: Día Operativo -->
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small fw-semibold text-body">
<i class="bi bi-clock-history me-1 text-info"></i>Ocupación Operativa de Hoy:
</span>
<span class="small font-monospace text-muted">
<strong class="text-info">{{ stats.assigned_today_classrooms }}</strong> en uso ({{ stats.assigned_today_pct }}%) · <strong class="text-secondary">{{ stats.unassigned_today_classrooms }}</strong> libres ({{ stats.unassigned_today_pct }}%)
</span>
</div>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-info" role="progressbar" style="width: {{ stats.assigned_today_pct }}%;"
aria-valuenow="{{ stats.assigned_today_pct }}" aria-valuemin="0" aria-valuemax="100"
title="En uso hoy: {{ stats.assigned_today_classrooms }}">
</div>
<div class="progress-bar bg-secondary-subtle" role="progressbar" style="width: {{ stats.unassigned_today_pct }}%;"
aria-valuenow="{{ stats.unassigned_today_pct }}" aria-valuemin="0" aria-valuemax="100"
title="Libres hoy: {{ stats.unassigned_today_classrooms }}">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bedelía Operational Launchpad --> <!-- Bedelía Operational Launchpad -->
<div class="card border-0 shadow-sm mb-4"> <div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-body-tertiary border-0 py-3"> <div class="card-header bg-body-tertiary border-0 py-3">
@@ -537,7 +759,7 @@
<!-- Docente KPI Cards --> <!-- Docente KPI Cards -->
<div class="row mb-4 g-3"> <div class="row mb-4 g-3">
<div class="col-md-6 col-xl-3"> <div class="col-md-4 col-xl-4">
<div class="card border-0 shadow-sm h-100 dashboard-stat-card transition-all glass-card"> <div class="card border-0 shadow-sm h-100 dashboard-stat-card transition-all glass-card">
<div class="card-body p-3"> <div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
@@ -557,7 +779,7 @@
</div> </div>
</div> </div>
<div class="col-md-6 col-xl-3"> <div class="col-md-4 col-xl-4">
<a href="/schedule/list_reservations" class="text-decoration-none text-reset d-block h-100"> <a href="/schedule/list_reservations" class="text-decoration-none text-reset d-block h-100">
<div class="card border-0 shadow-sm h-100 dashboard-stat-card transition-all glass-card"> <div class="card border-0 shadow-sm h-100 dashboard-stat-card transition-all glass-card">
<div class="card-body p-3"> <div class="card-body p-3">
@@ -579,8 +801,8 @@
</a> </a>
</div> </div>
<div class="col-md-6 col-xl-3"> <div class="col-md-4 col-xl-4">
<a href="/classrooms/list_classrooms" class="text-decoration-none text-reset d-block h-100"> <a href="/classrooms/list_classrooms?view_mode=virtual" class="text-decoration-none text-reset d-block h-100">
<div class="card border-0 shadow-sm h-100 dashboard-stat-card transition-all glass-card"> <div class="card border-0 shadow-sm h-100 dashboard-stat-card transition-all glass-card">
<div class="card-body p-3"> <div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
@@ -600,28 +822,6 @@
</div> </div>
</a> </a>
</div> </div>
<div class="col-md-6 col-xl-3">
<a href="/schedule/add_reservation" class="text-decoration-none text-reset d-block h-100">
<div class="card border-0 shadow-sm h-100 dashboard-stat-card transition-all bg-primary bg-opacity-10 border border-primary-subtle">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="text-primary small fw-bold text-uppercase">Nueva Solicitud</span>
<h4 class="mb-0 fw-bold mt-1 text-primary">Reservar Aula</h4>
</div>
<div class="bg-primary text-white rounded-circle p-3">
<i class="bi bi-plus-lg fs-3"></i>
</div>
</div>
<div class="d-flex align-items-center justify-content-between pt-3 mt-2 border-top text-primary small fw-bold">
<span>Solicitar espacio para clase</span>
<i class="bi bi-arrow-right"></i>
</div>
</div>
</div>
</a>
</div>
</div> </div>
<!-- Docente Launchpad --> <!-- Docente Launchpad -->