344 lines
18 KiB
HTML
344 lines
18 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{% trans %}Gestión de Ocupación y Métricas{% endtrans %} - 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>{% trans %}Gestión de Ocupación y Métricas de Espacios{% endtrans %}
|
|
</h1>
|
|
<p class="text-muted mb-0 small">
|
|
{% trans %}Analítica institucional de aprovechamiento de aulas físicas, demanda en Campus Virtual y distribución horaria.{% endtrans %}
|
|
</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a href="{{ url_for('classrooms.list_classrooms', view_mode='physical') }}" class="btn btn-outline-primary">
|
|
<i class="bi bi-buildings me-1"></i>{% trans %}Aulas Físicas{% endtrans %}
|
|
</a>
|
|
<a href="{{ url_for('classrooms.list_classrooms', view_mode='virtual') }}" class="btn btn-outline-info">
|
|
<i class="bi bi-globe2 me-1"></i>{% trans %}Campus Virtual (∞){% endtrans %}
|
|
</a>
|
|
<a href="{{ url_for('schedule.calendar_view') }}" class="btn btn-outline-secondary">
|
|
<i class="bi bi-calendar-week me-1"></i>{% trans %}Ver Calendario{% endtrans %}
|
|
</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;">{% trans %}Tasa de Ocupación Hoy{% endtrans %}</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;">
|
|
{% trans %}Promedio sobre 15 hs operativas ({{ physical_rooms_count }} aulas presenciales){% endtrans %}
|
|
</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;">{% trans %}Ocupación Semanal{% endtrans %}</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;">
|
|
{% trans %}Semana en curso (Lunes a Sábado){% endtrans %}
|
|
</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;">{% trans %}Ocupación del Mes{% endtrans %}</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;">
|
|
{% trans %}Total mes calendario activo{% endtrans %}
|
|
</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;">{% trans %}Total Clases en el Mes{% endtrans %}</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;">
|
|
{% trans %}Reservas y sesiones registradas en el período{% endtrans %}
|
|
</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>{% trans %}Tasa de Ocupación por Piso / Nivel{% endtrans %}
|
|
</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 }} {% trans %}aulas{% endtrans %})
|
|
</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>{% trans %}Distribución de Clases por Turno{% endtrans %}
|
|
</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;">{% trans %}Mañana{% endtrans %}</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;">{% trans %}Tarde{% endtrans %}</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;">{% trans %}Vespertino{% endtrans %}</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;">{% trans %}Noche{% endtrans %}</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>{% trans %}Campus Virtual (A Demanda · ∞){% endtrans %}</h6>
|
|
<p class="small text-muted mb-0">{% trans %}Salas simultáneas sin límite físico de cupo ni colisión.{% endtrans %}</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>{% trans %}Top Aulas con Mayor Demanda y Ocupación{% endtrans %}
|
|
</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0 small">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>{% trans %}Aula{% endtrans %}</th>
|
|
<th>{% trans %}Piso{% endtrans %}</th>
|
|
<th>{% trans %}Capacidad{% endtrans %}</th>
|
|
<th>{% trans %}Ocup. Mes{% endtrans %}</th>
|
|
<th class="text-end">{% trans %}Acción{% endtrans %}</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="{{ url_for('schedule.list_reservations', classroom_id=rm.classroom.id, date_from=date_filters.month_start, date_to=date_filters.month_end) }}"
|
|
class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 0.72rem;">
|
|
{% trans %}Ver Reservas{% endtrans %}
|
|
</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>{% trans %}Aulas con Mayor Disponibilidad (Menor Uso){% endtrans %}
|
|
</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0 small">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>{% trans %}Aula{% endtrans %}</th>
|
|
<th>{% trans %}Piso{% endtrans %}</th>
|
|
<th>{% trans %}Capacidad{% endtrans %}</th>
|
|
<th>{% trans %}Ocup. Mes{% endtrans %}</th>
|
|
<th class="text-end">{% trans %}Acción{% endtrans %}</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="{{ url_for('schedule.add_reservation') }}?classroom_id={{ rm.classroom.id }}"
|
|
class="btn btn-sm btn-outline-success py-0 px-2" style="font-size: 0.72rem;">
|
|
{% trans %}Reservar{% endtrans %}
|
|
</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 %}
|