344 lines
16 KiB
HTML
344 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Gestión de Ocupación y Métricas - Edu-Space Admin{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.kpi-card {
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0, 0, 0, 0.07);
|
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
}
|
|
.kpi-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.08) !important;
|
|
}
|
|
.kpi-icon-circle {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.4rem;
|
|
}
|
|
.metric-bar-container {
|
|
height: 10px;
|
|
border-radius: 6px;
|
|
background-color: var(--bs-tertiary-bg);
|
|
overflow: hidden;
|
|
}
|
|
.metric-bar-fill {
|
|
width: 0;
|
|
height: 100%;
|
|
border-radius: 6px;
|
|
transition: width 0.6s ease;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid pt-5 mt-4">
|
|
<!-- Header -->
|
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
|
<div>
|
|
<h1 class="h3 fw-bold mb-1">
|
|
<i class="bi bi-graph-up-arrow text-primary me-2"></i>Gestión de Ocupación y Métricas de Espacios
|
|
</h1>
|
|
<p class="text-muted mb-0 small">
|
|
Analítica institucional de aprovechamiento de aulas físicas, demanda en Campus Virtual y distribución horaria.
|
|
</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a href="/classrooms/list_classrooms" class="btn btn-outline-primary">
|
|
<i class="bi bi-buildings me-1"></i>Aulas Físicas
|
|
</a>
|
|
<a href="/classrooms/list_classrooms" class="btn btn-outline-info">
|
|
<i class="bi bi-globe2 me-1"></i>Campus Virtual (∞)
|
|
</a>
|
|
<a href="/schedule/calendar_view" class="btn btn-outline-secondary">
|
|
<i class="bi bi-calendar-week me-1"></i>Ver Calendario
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 1. Fila de KPIs Globales de Ocupación -->
|
|
<div class="row g-3 mb-4">
|
|
<!-- Ocupación Hoy -->
|
|
<div class="col-xl-3 col-sm-6">
|
|
<div class="card kpi-card border-0 shadow-sm p-3 h-100">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.72rem;">Tasa de Ocupación Hoy</span>
|
|
<h2 class="h3 fw-bold mb-0 text-primary mt-1">{{ avg_day_pct }}%</h2>
|
|
</div>
|
|
<div class="kpi-icon-circle bg-primary-subtle text-primary">
|
|
<i class="bi bi-clock-history"></i>
|
|
</div>
|
|
</div>
|
|
<div class="metric-bar-container mt-3">
|
|
<div class="metric-bar-fill bg-primary" data-width="{{ avg_day_pct }}%"></div>
|
|
</div>
|
|
<small class="text-muted mt-2 d-block" style="font-size: 0.72rem;">
|
|
Promedio sobre 15 hs operativas ({{ physical_rooms_count }} aulas presenciales)
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ocupación Semana -->
|
|
<div class="col-xl-3 col-sm-6">
|
|
<div class="card kpi-card border-0 shadow-sm p-3 h-100">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.72rem;">Ocupación Semanal</span>
|
|
<h2 class="h3 fw-bold mb-0 text-info mt-1">{{ avg_week_pct }}%</h2>
|
|
</div>
|
|
<div class="kpi-icon-circle bg-info-subtle text-info">
|
|
<i class="bi bi-calendar-week"></i>
|
|
</div>
|
|
</div>
|
|
<div class="metric-bar-container mt-3">
|
|
<div class="metric-bar-fill bg-info" data-width="{{ avg_week_pct }}%"></div>
|
|
</div>
|
|
<small class="text-muted mt-2 d-block" style="font-size: 0.72rem;">
|
|
Semana en curso (Lunes a Sábado)
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ocupación Mensual -->
|
|
<div class="col-xl-3 col-sm-6">
|
|
<div class="card kpi-card border-0 shadow-sm p-3 h-100">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.72rem;">Ocupación del Mes</span>
|
|
<h2 class="h3 fw-bold mb-0 text-success mt-1">{{ avg_month_pct }}%</h2>
|
|
</div>
|
|
<div class="kpi-icon-circle bg-success-subtle text-success">
|
|
<i class="bi bi-calendar3"></i>
|
|
</div>
|
|
</div>
|
|
<div class="metric-bar-container mt-3">
|
|
<div class="metric-bar-fill bg-success" data-width="{{ avg_month_pct }}%"></div>
|
|
</div>
|
|
<small class="text-muted mt-2 d-block" style="font-size: 0.72rem;">
|
|
Total mes calendario activo
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clases Totales Dictadas -->
|
|
<div class="col-xl-3 col-sm-6">
|
|
<div class="card kpi-card border-0 shadow-sm p-3 h-100">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.72rem;">Total Clases en el Mes</span>
|
|
<h2 class="h3 fw-bold mb-0 text-warning mt-1">{{ total_month_classes }}</h2>
|
|
</div>
|
|
<div class="kpi-icon-circle bg-warning-subtle text-warning">
|
|
<i class="bi bi-mortarboard"></i>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex gap-2 mt-3 small font-monospace">
|
|
<span class="badge bg-primary-subtle text-primary border">Físicas: {{ total_month_classes - v_month }}</span>
|
|
<span class="badge bg-info-subtle text-info border">Virtuales: {{ v_month }}</span>
|
|
</div>
|
|
<small class="text-muted mt-2 d-block" style="font-size: 0.72rem;">
|
|
Reservas y sesiones registradas en el período
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. Detalle y Distribución Espacial -->
|
|
<div class="row g-4 mb-4">
|
|
<!-- Ocupación por Piso / Nivel -->
|
|
<div class="col-lg-6">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center">
|
|
<h5 class="card-title h6 fw-bold mb-0">
|
|
<i class="bi bi-layers text-primary me-2"></i>Tasa de Ocupación por Piso / Nivel
|
|
</h5>
|
|
<span class="badge bg-secondary-subtle text-secondary border">Presencial</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-flex flex-column gap-3">
|
|
{% for fs in floor_summary %}
|
|
<div>
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<span class="fw-semibold text-body small">
|
|
{{ fs.floor }} ({{ fs.count }} aulas)
|
|
</span>
|
|
<span class="fw-bold font-monospace small {% if fs.avg_pct > 50 %}text-danger{% elif fs.avg_pct > 25 %}text-warning{% else %}text-success{% endif %}">
|
|
{{ fs.avg_pct }}%
|
|
</span>
|
|
</div>
|
|
<div class="metric-bar-container">
|
|
<div class="metric-bar-fill {% if fs.avg_pct > 50 %}bg-danger{% elif fs.avg_pct > 25 %}bg-warning{% else %}bg-success{% endif %}"
|
|
data-width="{{ fs.avg_pct }}%"></div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Distribución por Turno -->
|
|
<div class="col-lg-6">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center">
|
|
<h5 class="card-title h6 fw-bold mb-0">
|
|
<i class="bi bi-brightness-high text-warning me-2"></i>Distribución de Clases por Turno
|
|
</h5>
|
|
<span class="badge bg-secondary-subtle text-secondary border">Mes Completo</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-2 text-center mb-3">
|
|
<div class="col-3">
|
|
<div class="p-2 rounded bg-body-tertiary border">
|
|
<div class="small text-muted text-uppercase fw-semibold" style="font-size: 0.68rem;">Mañana</div>
|
|
<div class="h5 fw-bold text-primary mb-0">{{ shift_counts.Mañana }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="p-2 rounded bg-body-tertiary border">
|
|
<div class="small text-muted text-uppercase fw-semibold" style="font-size: 0.68rem;">Tarde</div>
|
|
<div class="h5 fw-bold text-warning mb-0">{{ shift_counts.Tarde }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="p-2 rounded bg-body-tertiary border">
|
|
<div class="small text-muted text-uppercase fw-semibold" style="font-size: 0.68rem;">Vespertino</div>
|
|
<div class="h5 fw-bold text-info mb-0">{{ shift_counts.Vespertino }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="p-2 rounded bg-body-tertiary border">
|
|
<div class="small text-muted text-uppercase fw-semibold" style="font-size: 0.68rem;">Noche</div>
|
|
<div class="h5 fw-bold text-danger mb-0">{{ shift_counts.Noche }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Campus Virtual Box -->
|
|
<div class="p-3 rounded bg-primary-subtle border border-primary-subtle">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="fw-bold text-primary mb-1"><i class="bi bi-globe2 me-2"></i>Campus Virtual (A Demanda · ∞)</h6>
|
|
<p class="small text-muted mb-0">Salas simultáneas sin límite físico de cupo ni colisión.</p>
|
|
</div>
|
|
<div class="text-end">
|
|
<div class="badge bg-primary text-white fs-6">{{ v_month }} clases mes</div>
|
|
<small class="d-block text-muted mt-1">{{ v_today }} hoy · {{ v_week }} semana</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3. Top Aulas de Mayor Demanda vs Menor Demanda -->
|
|
<div class="row g-4">
|
|
<!-- Mayor Demanda -->
|
|
<div class="col-lg-6">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-body-tertiary border-0 py-3">
|
|
<h5 class="card-title h6 fw-bold mb-0 text-success">
|
|
<i class="bi bi-arrow-up-circle-fill me-2"></i>Top Aulas con Mayor Demanda y Ocupación
|
|
</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0 small">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Aula</th>
|
|
<th>Piso</th>
|
|
<th>Capacidad</th>
|
|
<th>Ocup. Mes</th>
|
|
<th class="text-end">Acción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for rm in top_rooms %}
|
|
<tr>
|
|
<td class="fw-bold text-primary">{{ rm.classroom.code }}</td>
|
|
<td>{{ rm.classroom.floor_display }}</td>
|
|
<td>{{ rm.classroom.capacity }} asientos</td>
|
|
<td>
|
|
<span class="badge bg-success-subtle text-success border border-success-subtle font-monospace">
|
|
{{ rm.month_pct }}% ({{ rm.month_count }} cl.)
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<a href="/schedule/list_reservations"
|
|
class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 0.72rem;">
|
|
Ver Reservas
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Menor Demanda / Oportunidades -->
|
|
<div class="col-lg-6">
|
|
<div class="card border-0 shadow-sm h-100">
|
|
<div class="card-header bg-body-tertiary border-0 py-3">
|
|
<h5 class="card-title h6 fw-bold mb-0 text-secondary">
|
|
<i class="bi bi-arrow-down-circle-fill me-2"></i>Aulas con Mayor Disponibilidad (Menor Uso)
|
|
</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0 small">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Aula</th>
|
|
<th>Piso</th>
|
|
<th>Capacidad</th>
|
|
<th>Ocup. Mes</th>
|
|
<th class="text-end">Acción</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for rm in low_rooms %}
|
|
<tr>
|
|
<td class="fw-bold text-body">{{ rm.classroom.code }}</td>
|
|
<td>{{ rm.classroom.floor_display }}</td>
|
|
<td>{{ rm.classroom.capacity }} asientos</td>
|
|
<td>
|
|
<span class="badge bg-secondary-subtle text-secondary border font-monospace">
|
|
{{ rm.month_pct }}% ({{ rm.month_count }} cl.)
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<a href="/schedule/add_reservation?classroom_id={{ rm.classroom.id }}"
|
|
class="btn btn-sm btn-outline-success py-0 px-2" style="font-size: 0.72rem;">
|
|
Reservar
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.querySelectorAll('.metric-bar-fill[data-width]').forEach(function(bar) {
|
|
bar.style.width = bar.getAttribute('data-width');
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|