feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user