feat: completar fases 3 y 4 del roadmap mvp (gradebook, drag-and-drop, impersonacion) y actualizar README

This commit is contained in:
2026-09-23 12:10:31 -03:00
parent 4ebc5df4fd
commit 7ad4e3f099
38 changed files with 3804 additions and 390 deletions
@@ -101,6 +101,18 @@
</li>
{% endif %}
</ul>
<div class="mt-3 pt-2 border-top">
<a href="/schedule/gradebook/{{ commission.id }}" class="btn btn-primary btn-sm w-100 shadow-sm d-flex align-items-center justify-content-center gap-2">
<i class="bi bi-journal-check fs-6"></i>
<span class="fw-semibold">Libro de Calificaciones (Actas)</span>
</a>
{% if commission.grades_closed %}
<div class="badge bg-danger-subtle text-danger border border-danger-subtle w-100 mt-2 py-2 text-center">
<i class="bi bi-lock-fill me-1"></i>Acta Cerrada: {{ commission.acta_number }}
</div>
{% endif %}
</div>
</div>
</div>
+3
View File
@@ -183,6 +183,9 @@
<i class="bi bi-pencil"></i>
</a>
{% if u.id != user.id %}
<a href="/auth/impersonate/{{ u.id }}" class="btn btn-outline-info" title="Impersonar usuario (Iniciar sesión como {{ u.name }})">
<i class="bi bi-incognito"></i>
</a>
<form action="/admin/user_toggle" method="POST" class="d-inline">
<input type="hidden" name="id" value="{{ u.id }}">
<button type="submit" class="btn {% if u.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}"
+519 -225
View File
@@ -188,258 +188,488 @@
-webkit-text-fill-color: transparent;
font-weight: 700;
}
/* =======================================================
SIDEBAR & TOGGLE BUTTON DESIGN (720p, 1360x768 & 1080p)
======================================================= */
.app-wrapper {
min-height: 100vh;
display: flex;
}
.sidebar {
width: 250px;
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
z-index: 1040;
display: flex;
flex-direction: column;
border-right: 1px solid rgba(0, 0, 0, 0.08);
background: var(--bs-body-bg);
position: sticky;
top: 0;
height: 100vh;
}
[data-bs-theme="dark"] .sidebar {
border-right: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(24, 32, 47, 0.98);
}
/* Header del Sidebar */
.sidebar-header {
height: 54px;
padding: 0.5rem 0.85rem;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
transition: all 0.25s ease;
}
[data-bs-theme="dark"] .sidebar-header {
border-bottom-color: rgba(255, 255, 255, 0.07);
}
/* Botón de Toggle Limpio & Estilizado */
.sidebar-toggle-btn {
width: 34px;
height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.08);
background: rgba(0, 0, 0, 0.03);
color: #475569;
transition: all 0.2s ease-in-out;
cursor: pointer;
padding: 0;
outline: none !important;
box-shadow: none !important;
}
.sidebar-toggle-btn:hover {
background: rgba(180, 62, 142, 0.1);
border-color: rgba(180, 62, 142, 0.4);
color: #B43E8E !important;
transform: scale(1.05);
}
[data-bs-theme="dark"] .sidebar-toggle-btn {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.1);
color: #94A3B8;
}
[data-bs-theme="dark"] .sidebar-toggle-btn:hover {
background: rgba(255, 138, 182, 0.15);
border-color: rgba(255, 138, 182, 0.4);
color: #FF8AB6 !important;
transform: scale(1.05);
}
/* Sidebar Colapsado (Compacto, ultra limpio para pantallas chicas) */
.sidebar.collapsed {
width: 68px;
}
.sidebar.collapsed .brand-link {
display: none !important;
}
.sidebar.collapsed .sidebar-header {
justify-content: center !important;
padding: 0.5rem !important;
}
.sidebar.collapsed .brand-text,
.sidebar.collapsed .nav-text,
.sidebar.collapsed .nav-caret,
.sidebar.collapsed .sidebar-footer-text,
.sidebar.collapsed .dropdown-header {
display: none !important;
}
.sidebar.collapsed .nav-link {
justify-content: center !important;
padding: 0.55rem 0 !important;
}
.sidebar.collapsed .nav-link i {
margin-right: 0 !important;
font-size: 1.25rem !important;
}
.sidebar.collapsed .sidebar-footer {
padding: 0.5rem !important;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.sidebar.collapsed .sidebar-footer .theme-toggle-btn {
margin-bottom: 0.5rem !important;
}
.sidebar.collapsed .collapse {
display: none !important;
}
/* Navegación del Sidebar */
.sidebar-nav {
padding: 0;
list-style: none;
width: 100%;
}
.sidebar-nav .nav-item {
margin-bottom: 2px;
width: 100%;
}
.sidebar-nav .nav-link {
display: flex;
align-items: center;
padding: 0.5rem 0.85rem;
color: var(--bs-body-color);
border-radius: 8px;
text-decoration: none;
white-space: nowrap;
font-size: 0.88rem;
font-weight: 500;
transition: all 0.18s ease;
width: 100%;
}
.sidebar-nav .nav-link:hover, .sidebar-nav .nav-link[aria-expanded="true"] {
background-color: rgba(180, 62, 142, 0.08);
color: #B43E8E !important;
}
[data-bs-theme="dark"] .sidebar-nav .nav-link:hover, [data-bs-theme="dark"] .sidebar-nav .nav-link[aria-expanded="true"] {
background-color: rgba(255, 138, 182, 0.1);
color: #FF8AB6 !important;
}
.sidebar-nav .nav-link i {
font-size: 1.15rem;
min-width: 26px;
text-align: center;
margin-right: 10px;
}
.sidebar-submenu {
list-style: none;
padding-left: 2.2rem;
margin-top: 0.2rem;
}
.sidebar-submenu .nav-link {
padding: 0.35rem 0.75rem;
font-size: 0.83rem;
color: var(--bs-secondary-color);
}
/* Contenido Principal & Eliminación de Desperdicio Superior */
.main-content {
flex-grow: 1;
min-width: 0;
display: flex;
flex-direction: column;
background: transparent;
}
.app-main-container {
padding-top: 0.85rem !important;
padding-bottom: 1.5rem !important;
}
/* Anular margen excesivo heredado de templates antiguos */
.app-main-container > .container-fluid.pt-5.mt-4,
.app-main-container > .container.pt-5.mt-4,
.app-main-container .pt-5.mt-4,
.main-content .container-fluid.pt-5.mt-4,
.main-content .container.pt-5.mt-4 {
padding-top: 0 !important;
margin-top: 0 !important;
}
/* Mobile Header */
.mobile-header {
display: none;
}
@media (max-width: 991.98px) {
.sidebar {
position: fixed;
left: -260px;
}
.sidebar.mobile-open {
left: 0;
}
.mobile-header {
display: flex;
background: var(--bs-body-bg);
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
padding: 0.5rem 1rem;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 1030;
height: 52px;
}
[data-bs-theme="dark"] .mobile-header {
border-bottom-color: rgba(255, 255, 255, 0.08);
}
}
/* =======================================================
OPTIMIZACIONES PARA 720p / 1360x768 (Mercado Argentino / Bedelía)
======================================================= */
@media (max-height: 820px), (max-width: 1400px) {
.sidebar-header {
height: 48px;
padding: 0.4rem 0.65rem;
}
.sidebar-header .brand-text {
font-size: 1.05rem !important;
}
.sidebar-header .navbar-logo {
height: 24px;
}
.sidebar-nav .nav-link {
padding: 0.4rem 0.75rem;
font-size: 0.83rem;
}
.sidebar-nav .nav-link i {
font-size: 1.05rem;
min-width: 22px;
margin-right: 8px;
}
.sidebar-submenu .nav-link {
padding: 0.28rem 0.65rem;
font-size: 0.8rem;
}
.sidebar-footer {
padding: 0.4rem 0.65rem !important;
}
.app-main-container {
padding-top: 0.5rem !important;
padding-left: 0.85rem !important;
padding-right: 0.85rem !important;
}
/* Títulos y márgenes más compactos para ganar viewport útil */
h1.h3, .h3, h1.h4, .h4 {
font-size: 1.25rem !important;
margin-bottom: 0.2rem !important;
}
.dashboard-role-banner {
padding: 0.75rem 1rem !important;
margin-bottom: 0.85rem !important;
}
.card {
margin-bottom: 0.85rem !important;
}
.card-body {
padding: 0.85rem !important;
}
}
/* =======================================================
OPTIMIZACIONES PARA 1080p (1920x1080 / Monitores Grandes)
======================================================= */
@media (min-width: 1600px) and (min-height: 900px) {
.sidebar {
width: 270px;
}
.sidebar-header {
height: 60px;
padding: 0.75rem 1rem;
}
.sidebar-nav .nav-link {
padding: 0.58rem 1rem;
font-size: 0.92rem;
}
.app-main-container {
padding-top: 1.25rem !important;
padding-left: 2rem !important;
padding-right: 2rem !important;
}
}
.brand-link span {
background: linear-gradient(90deg, #B43E8E, #682CAD);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 700;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body data-user-authenticated="{{ 'true' if (user and user.is_authenticated) else 'false' }}">
{% if user.is_authenticated %}
<!-- Navigation -->
<nav class="navbar navbar-expand-lg glass-navbar fixed-top">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center" href="/dashboard">
<img src="/img/logo_unicaba_mini.png" alt="Logo UniCABA" class="navbar-logo me-2" height="30">
<span>Edu-Space Admin</span>
</a>
<div class="app-wrapper">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header border-bottom border-secondary-subtle">
<a href="/dashboard" class="text-decoration-none d-flex align-items-center brand-link">
<img src="/img/logo_unicaba_mini.png" alt="Logo UniCABA" class="navbar-logo me-2" height="26">
<span class="brand-text fs-5">Edu-Space</span>
</a>
<button class="sidebar-toggle-btn d-none d-lg-inline-flex" id="sidebarToggle" type="button" aria-label="Alternar menú lateral" title="Alternar menú lateral">
<i class="bi bi-layout-sidebar-inset fs-5"></i>
</button>
<button class="sidebar-toggle-btn d-lg-none" id="sidebarCloseMobile" type="button" aria-label="Cerrar menú">
<i class="bi bi-x-lg fs-5"></i>
</button>
</div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
{% set user_role = 'admin' %}
<ul class="navbar-nav me-auto">
<div class="sidebar-body flex-grow-1 overflow-y-auto p-2">
<ul class="sidebar-nav" id="sidebarNav">
{% set user_role = role if role else 'admin' %}
{% if user_role == 'admin' %}
<!-- ================= ADMIN NAVIGATION ================= -->
<!-- ADMIN -->
<li class="nav-item">
<a class="nav-link" href="/dashboard">
<i class="bi bi-speedometer2 me-1"></i>Panel General
</a>
<a class="nav-link" href="/dashboard"><i class="bi bi-speedometer2 text-primary"></i> <span class="nav-text">Panel General</span></a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-door-open me-1"></i>Espacios & Sedes
<li class="nav-item">
<a class="nav-link collapsed" href="#menuEspacios" data-bs-toggle="collapse" role="button" aria-expanded="false">
<i class="bi bi-door-open text-info"></i> <span class="nav-text flex-grow-1">Espacios & Sedes</span> <i class="bi bi-chevron-down nav-caret ms-auto fs-6"></i>
</a>
<ul class="dropdown-menu shadow-sm">
<li><a class="dropdown-item" href="/classrooms/list_classrooms?view_mode=physical"><i class="bi bi-door-open me-2 text-primary"></i>Aulas Presenciales</a></li>
<li><a class="dropdown-item" href="/classrooms/list_classrooms?view_mode=virtual"><i class="bi bi-camera-video me-2 text-info"></i>Aulas Virtuales (Reunión)</a></li>
<li><a class="dropdown-item" href="/classrooms/list_classrooms"><i class="bi bi-grid me-2 text-secondary"></i>Todas las Aulas</a></li>
<li><a class="dropdown-item" href="/classrooms/add_classroom"><i class="bi bi-plus-circle me-2 text-success"></i>Nueva Aula</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/buildings/list_buildings"><i class="bi bi-buildings me-2 text-secondary"></i>Edificios y Sedes</a></li>
<li><a class="dropdown-item" href="/admin/occupancy_metrics"><i class="bi bi-graph-up-arrow me-2 text-success"></i>Métricas de Ocupación</a></li>
</ul>
<div class="collapse" id="menuEspacios" data-bs-parent="#sidebarNav">
<ul class="sidebar-submenu">
<li><a class="nav-link" href="/classrooms/list_classrooms?view_mode=physical">Presenciales</a></li>
<li><a class="nav-link" href="/classrooms/list_classrooms?view_mode=virtual">Virtuales</a></li>
<li><a class="nav-link" href="/classrooms/list_classrooms">Todas las Aulas</a></li>
<li><a class="nav-link text-success" href="/classrooms/add_classroom"><i class="bi bi-plus-circle me-1" style="min-width:auto; font-size:1rem;"></i>Nueva Aula</a></li>
<li><a class="nav-link" href="/buildings/list_buildings">Edificios</a></li>
<li><a class="nav-link" href="/admin/occupancy_metrics">Métricas</a></li>
</ul>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-calendar3 me-1"></i>Cronograma & Reservas
<li class="nav-item">
<a class="nav-link collapsed" href="#menuReservas" data-bs-toggle="collapse" role="button" aria-expanded="false">
<i class="bi bi-calendar3 text-warning"></i> <span class="nav-text flex-grow-1">Reservas</span> <i class="bi bi-chevron-down nav-caret ms-auto fs-6"></i>
</a>
<ul class="dropdown-menu shadow-sm">
<li><a class="dropdown-item" href="/schedule/calendar_view"><i class="bi bi-calendar-week me-2 text-primary"></i>Calendario General</a></li>
<li><a class="dropdown-item" href="/schedule/today_schedule"><i class="bi bi-calendar-day me-2 text-info"></i>Cartelera del Día</a></li>
<li><a class="dropdown-item" href="/schedule/list_reservations"><i class="bi bi-list-check me-2 text-warning"></i>Gestión de Reservas</a></li>
<li><a class="dropdown-item" href="/schedule/add_reservation"><i class="bi bi-plus-circle me-2 text-success"></i>Nueva Reserva</a></li>
<li><a class="dropdown-item" href="/schedule/commissions_view"><i class="bi bi-diagram-3 me-2 text-primary"></i>Comisiones y Cursadas</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/genetic_algorithm_web/genetic_optimizer"><i class="bi bi-cpu me-2 text-info"></i>Optimizador de Aulas (IA)</a></li>
</ul>
<div class="collapse" id="menuReservas" data-bs-parent="#sidebarNav">
<ul class="sidebar-submenu">
<li><a class="nav-link" href="/schedule/calendar_view">Calendario</a></li>
<li><a class="nav-link" href="/schedule/today_schedule">Cartelera</a></li>
<li><a class="nav-link" href="/schedule/list_reservations">Gestión</a></li>
<li><a class="nav-link text-success" href="/schedule/add_reservation"><i class="bi bi-plus-circle me-1" style="min-width:auto; font-size:1rem;"></i>Nueva</a></li>
<li><a class="nav-link" href="/schedule/commissions_view">Comisiones</a></li>
<li><a class="nav-link text-info fw-semibold" href="/genetic_algorithm_web/genetic_optimizer"><i class="bi bi-cpu me-1" style="min-width:auto; font-size:1rem;"></i>IA Optimizer</a></li>
</ul>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-mortarboard me-1"></i>Gestión Académica
<li class="nav-item">
<a class="nav-link collapsed" href="#menuAcademica" data-bs-toggle="collapse" role="button" aria-expanded="false">
<i class="bi bi-mortarboard text-success"></i> <span class="nav-text flex-grow-1">Académica</span> <i class="bi bi-chevron-down nav-caret ms-auto fs-6"></i>
</a>
<ul class="dropdown-menu shadow-sm">
<li><a class="dropdown-item" href="/admin/careers_list"><i class="bi bi-mortarboard me-2 text-primary"></i>Carreras</a></li>
<li><a class="dropdown-item" href="/admin/subjects_list"><i class="bi bi-book me-2 text-info"></i>Asignaturas</a></li>
<li><a class="dropdown-item" href="/admin/commissions_list"><i class="bi bi-diagram-3 me-2 text-success"></i>Comisiones</a></li>
<li><a class="dropdown-item" href="/admin/academic_terms_list"><i class="bi bi-calendar-range me-2 text-warning"></i>Ciclo Lectivo</a></li>
</ul>
<div class="collapse" id="menuAcademica" data-bs-parent="#sidebarNav">
<ul class="sidebar-submenu">
<li><a class="nav-link" href="/admin/careers_list">Carreras</a></li>
<li><a class="nav-link" href="/admin/subjects_list">Asignaturas</a></li>
<li><a class="nav-link" href="/admin/commissions_list">Comisiones</a></li>
<li><a class="nav-link" href="/admin/academic_terms_list">Ciclo Lectivo</a></li>
</ul>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-shield-lock me-1"></i>Administración
<li class="nav-item">
<a class="nav-link collapsed" href="#menuAdmin" data-bs-toggle="collapse" role="button" aria-expanded="false">
<i class="bi bi-shield-lock text-secondary"></i> <span class="nav-text flex-grow-1">Administración</span> <i class="bi bi-chevron-down nav-caret ms-auto fs-6"></i>
</a>
<ul class="dropdown-menu shadow-sm">
<li><a class="dropdown-item" href="/admin/users_list"><i class="bi bi-people me-2 text-primary"></i>Usuarios</a></li>
<li><a class="dropdown-item" href="/admin/roles_list"><i class="bi bi-person-badge me-2 text-info"></i>Roles y Permisos RBAC</a></li>
<li><a class="dropdown-item" href="/admin/milestone_types_list"><i class="bi bi-tags me-2 text-secondary"></i>Tipificaciones de Hitos</a></li>
<li><a class="dropdown-item" href="/admin/google_sheets_import"><i class="bi bi-file-earmark-spreadsheet me-2 text-success"></i>Sincronización Sheets</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/admin/audit_logs"><i class="bi bi-shield-check me-2 text-warning"></i>Registro de Auditoría</a></li>
</ul>
<div class="collapse" id="menuAdmin" data-bs-parent="#sidebarNav">
<ul class="sidebar-submenu">
<li><a class="nav-link" href="/admin/users_list">Usuarios</a></li>
<li><a class="nav-link" href="/admin/roles_list">Roles RBAC</a></li>
<li><a class="nav-link" href="/admin/milestone_types_list">Tipos Hitos</a></li>
<li><a class="nav-link" href="/admin/google_sheets_import">Sincro Sheets</a></li>
<li><a class="nav-link" href="/admin/audit_logs">Auditoría</a></li>
</ul>
</div>
</li>
{% elif user_role == 'bedelia' %}
<!-- ================= BEDELÍA NAVIGATION ================= -->
<li class="nav-item">
<a class="nav-link" href="/dashboard">
<i class="bi bi-speedometer2 me-1"></i>Panel Bedelía
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-door-open me-1"></i>Espacios & Aulas
</a>
<ul class="dropdown-menu shadow-sm">
<li><a class="dropdown-item" href="/classrooms/list_classrooms?view_mode=physical"><i class="bi bi-door-open me-2 text-primary"></i>Aulas Presenciales</a></li>
<li><a class="dropdown-item" href="/classrooms/list_classrooms?view_mode=virtual"><i class="bi bi-camera-video me-2 text-info"></i>Aulas Virtuales (Reunión)</a></li>
<li><a class="dropdown-item" href="/classrooms/list_classrooms"><i class="bi bi-grid me-2 text-secondary"></i>Todas las Aulas</a></li>
<li><a class="dropdown-item" href="/classrooms/add_classroom"><i class="bi bi-plus-circle me-2 text-success"></i>Nueva Aula</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/buildings/list_buildings"><i class="bi bi-buildings me-2 text-secondary"></i>Edificios y Sedes</a></li>
<li><a class="dropdown-item" href="/admin/occupancy_metrics"><i class="bi bi-graph-up-arrow me-2 text-success"></i>Métricas de Ocupación</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-calendar3 me-1"></i>Operaciones & Reservas
</a>
<ul class="dropdown-menu shadow-sm">
<li><a class="dropdown-item" href="/schedule/today_schedule"><i class="bi bi-calendar-day me-2 text-info"></i>Cartelera del Día</a></li>
<li><a class="dropdown-item" href="/schedule/calendar_view"><i class="bi bi-calendar-week me-2 text-primary"></i>Calendario General</a></li>
<li><a class="dropdown-item" href="/schedule/list_reservations"><i class="bi bi-list-check me-2 text-warning"></i>Gestión de Reservas</a></li>
<li><a class="dropdown-item" href="/schedule/add_reservation"><i class="bi bi-plus-circle me-2 text-success"></i>Registrar Reserva</a></li>
<li><a class="dropdown-item" href="/schedule/commissions_view"><i class="bi bi-diagram-3 me-2 text-primary"></i>Comisiones y Cursadas</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-mortarboard me-1"></i>Académica & Cursadas
</a>
<ul class="dropdown-menu shadow-sm">
<li><a class="dropdown-item" href="/admin/careers_list"><i class="bi bi-mortarboard me-2 text-primary"></i>Carreras</a></li>
<li><a class="dropdown-item" href="/admin/subjects_list"><i class="bi bi-book me-2 text-info"></i>Asignaturas</a></li>
<li><a class="dropdown-item" href="/admin/commissions_list"><i class="bi bi-diagram-3 me-2 text-success"></i>Gestión de Comisiones</a></li>
<li><a class="dropdown-item" href="/admin/academic_terms_list"><i class="bi bi-calendar-range me-2 text-warning"></i>Ciclo Lectivo</a></li>
<li><a class="dropdown-item" href="/admin/milestone_types_list"><i class="bi bi-tags me-2 text-secondary"></i>Tipificaciones de Hitos</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/admin/google_sheets_import"><i class="bi bi-file-earmark-spreadsheet me-2 text-success"></i>Sincronización Sheets</a></li>
</ul>
</li>
<!-- BEDELIA -->
<li class="nav-item"><a class="nav-link" href="/dashboard"><i class="bi bi-speedometer2 text-warning"></i> <span class="nav-text">Panel Bedelía</span></a></li>
<li class="nav-item"><a class="nav-link" href="/classrooms/list_classrooms"><i class="bi bi-door-open text-primary"></i> <span class="nav-text">Aulas y Espacios</span></a></li>
<li class="nav-item"><a class="nav-link" href="/schedule/today_schedule"><i class="bi bi-calendar-day text-info"></i> <span class="nav-text">Cartelera Diaria</span></a></li>
<li class="nav-item"><a class="nav-link" href="/schedule/list_reservations"><i class="bi bi-list-check text-success"></i> <span class="nav-text">Gestión Reservas</span></a></li>
<li class="nav-item"><a class="nav-link" href="/schedule/add_reservation"><i class="bi bi-plus-circle text-success"></i> <span class="nav-text">Nueva Reserva</span></a></li>
<li class="nav-item"><a class="nav-link" href="/admin/commissions_list"><i class="bi bi-diagram-3 text-primary"></i> <span class="nav-text">Comisiones</span></a></li>
{% elif user_role == 'docente' %}
<!-- ================= DOCENTE / PROFESOR NAVIGATION ================= -->
<li class="nav-item">
<a class="nav-link" href="/dashboard">
<i class="bi bi-speedometer2 me-1"></i>Panel Docente
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-calendar-check me-1"></i>Mis Clases & Reservas
</a>
<ul class="dropdown-menu shadow-sm">
<li><a class="dropdown-item" href="/schedule/list_reservations"><i class="bi bi-calendar2-check me-2 text-primary"></i>Mis Reservas de Aulas</a></li>
<li><a class="dropdown-item" href="/schedule/add_reservation"><i class="bi bi-plus-circle me-2 text-success"></i>Solicitar Nueva Reserva</a></li>
<li><a class="dropdown-item" href="/schedule/commissions_view"><i class="bi bi-diagram-3 me-2 text-info"></i>Mis Comisiones & Cursadas</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link fw-semibold" href="/my_subjects/my_subjects_list">
<i class="bi bi-mortarboard me-1 text-info"></i>Mis Materias
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-building me-1"></i>Campus & Horarios
</a>
<ul class="dropdown-menu shadow-sm">
<li><a class="dropdown-item" href="/schedule/today_schedule"><i class="bi bi-calendar-day me-2 text-info"></i>Cartelera del Día (¿Dónde curso hoy?)</a></li>
<li><a class="dropdown-item" href="/schedule/calendar_view"><i class="bi bi-calendar-week me-2 text-primary"></i>Calendario General de Aulas</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/classrooms/list_classrooms"><i class="bi bi-camera-video me-2 text-info"></i>Aulas Virtuales (Reuniones)</a></li>
<li><a class="dropdown-item" href="/classrooms/list_classrooms"><i class="bi bi-door-open me-2 text-secondary"></i>Consulta de Aulas Presenciales</a></li>
</ul>
</li>
<!-- DOCENTE -->
<li class="nav-item"><a class="nav-link" href="/dashboard"><i class="bi bi-speedometer2 text-info"></i> <span class="nav-text">Panel Docente</span></a></li>
<li class="nav-item"><a class="nav-link" href="/schedule/list_reservations"><i class="bi bi-calendar2-check text-primary"></i> <span class="nav-text">Mis Reservas</span></a></li>
<li class="nav-item"><a class="nav-link" href="/schedule/add_reservation"><i class="bi bi-plus-circle text-success"></i> <span class="nav-text">Solicitar Reserva</span></a></li>
<li class="nav-item"><a class="nav-link" href="/schedule/commissions_view"><i class="bi bi-diagram-3 text-info"></i> <span class="nav-text">Mis Comisiones</span></a></li>
<li class="nav-item"><a class="nav-link" href="/my_subjects/my_subjects_list"><i class="bi bi-mortarboard text-warning"></i> <span class="nav-text">Mis Materias</span></a></li>
<li class="nav-item"><a class="nav-link" href="/schedule/today_schedule"><i class="bi bi-geo-alt text-danger"></i> <span class="nav-text">Cartelera Hoy</span></a></li>
{% else %}
<!-- ================= ALUMNO / ESTUDIANTE NAVIGATION ================= -->
<li class="nav-item">
<a class="nav-link" href="/dashboard">
<i class="bi bi-house-door me-1"></i>Mi Portal
</a>
</li>
<li class="nav-item">
<a class="nav-link fw-semibold" href="/my_subjects/my_subjects_list">
<i class="bi bi-mortarboard me-1 text-success"></i>Mis Materias
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/schedule/today_schedule">
<i class="bi bi-geo-alt me-1"></i>Cartelera de Hoy
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/schedule/calendar_view">
<i class="bi bi-calendar-week me-1"></i>Cronograma de Clases
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/classrooms/list_classrooms">
<i class="bi bi-camera-video me-1"></i>Aulas Virtuales
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/classrooms/list_classrooms">
<i class="bi bi-door-open me-1"></i>Espacios del Campus
</a>
</li>
<!-- ALUMNO -->
<li class="nav-item"><a class="nav-link" href="/dashboard"><i class="bi bi-house-door text-success"></i> <span class="nav-text">Mi Portal</span></a></li>
<li class="nav-item"><a class="nav-link" href="/my_subjects/my_subjects_list"><i class="bi bi-mortarboard text-primary"></i> <span class="nav-text">Mis Materias</span></a></li>
<li class="nav-item"><a class="nav-link" href="/schedule/today_schedule"><i class="bi bi-geo-alt text-danger"></i> <span class="nav-text">Cartelera Hoy</span></a></li>
<li class="nav-item"><a class="nav-link" href="/schedule/calendar_view"><i class="bi bi-calendar-week text-info"></i> <span class="nav-text">Cronograma</span></a></li>
<li class="nav-item"><a class="nav-link" href="/classrooms/list_classrooms"><i class="bi bi-camera-video text-secondary"></i> <span class="nav-text">Aulas Virtuales</span></a></li>
{% endif %}
</ul>
<ul class="navbar-nav align-items-center">
<!-- Theme Mode Toggle Button -->
<li class="nav-item me-2">
<button class="theme-toggle-btn" type="button" aria-label="Cambiar tema" title="Modo Claro/Oscuro">
<i class="bi bi-moon-stars-fill"></i>
</button>
</li>
<!-- Language Switcher -->
<li class="nav-item me-2">
{% include 'partials/language_switcher.html' %}
</li>
<!-- User Dropdown with Avatar and Institutional Role -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center py-1" href="#" role="button" data-bs-toggle="dropdown">
<span class="user-avatar-small">{{ user.name[0]|upper if user.name else 'U' }}</span>
<div class="d-none d-lg-block text-start ms-2">
<div class="lh-1 small fw-semibold">{{ user.first_name }}</div>
<span class="badge {{ user.role_badge_display.class }} role-pill mt-1" style="font-size: 0.65rem; padding: 0.15rem 0.45rem;">
<i class="bi {{ user.role_badge_display.icon }}"></i> {{ user.role_badge_display.label }}
</span>
</div>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
<li>
<div class="px-3 py-2 border-bottom">
<small class="text-muted d-block">Sesión iniciada como</small>
<span class="fw-bold small d-block">{{ user.email }}</span>
<span class="badge {{ user.role_badge_display.class }} role-pill mt-1">
<i class="bi {{ user.role_badge_display.icon }}"></i> {{ user.role_badge_display.label }}
</span>
</div>
</li>
<li><a class="dropdown-item" href="/auth/profile">
<i class="bi bi-person me-2"></i> Mi Perfil
</a></li>
<li><a class="dropdown-item" href="/auth/change_password">
<i class="bi bi-key me-2"></i> Cambiar Contraseña
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="/auth/logout">
<i class="bi bi-box-arrow-right me-2"></i> Cerrar Sesión
</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
{% endif %}
<!-- Flash Messages -->
<div class="sidebar-footer p-3 border-top border-secondary-subtle">
<div class="d-flex align-items-center justify-content-between mb-3 sidebar-footer-text">
{% include 'partials/language_switcher.html' %}
<button class="theme-toggle-btn" type="button" aria-label="Cambiar tema" title="Modo Claro/Oscuro">
<i class="bi bi-moon-stars-fill"></i>
</button>
</div>
<div class="dropdown dropup">
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle text-body w-100" data-bs-toggle="dropdown" aria-expanded="false">
<div class="user-avatar-small bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width:32px; height:32px; font-weight:bold; flex-shrink:0;">
{{ user.name[0]|upper if user.name else 'U' }}
</div>
<div class="ms-2 sidebar-footer-text text-truncate flex-grow-1" style="min-width:0;">
<small class="d-block fw-semibold text-truncate">{{ user.first_name }}</small>
<small class="text-muted d-block text-truncate" style="font-size:0.7rem;">{{ user.role_badge_display.label }}</small>
</div>
</a>
<ul class="dropdown-menu shadow-sm border-0 mb-2">
<li><h6 class="dropdown-header">{{ user.email }}</h6></li>
<li><a class="dropdown-item" href="/auth/profile"><i class="bi bi-person me-2"></i>Mi Perfil</a></li>
<li><a class="dropdown-item" href="/auth/change_password"><i class="bi bi-key me-2"></i>Cambiar Contraseña</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="/auth/logout"><i class="bi bi-box-arrow-right me-2"></i>Cerrar Sesión</a></li>
</ul>
</div>
</div>
</aside>
<!-- Main Content Wrapper -->
<div class="main-content">
<!-- Mobile Header -->
<div class="mobile-header shadow-sm">
<a href="/dashboard" class="text-decoration-none brand-link d-flex align-items-center">
<img src="/img/logo_unicaba_mini.png" alt="Logo UniCABA" class="navbar-logo me-2" height="24">
<span class="brand-text fw-bold fs-6">Edu-Space</span>
</a>
<button class="sidebar-toggle-btn" id="mobileMenuBtn" type="button" aria-label="Abrir menú">
<i class="bi bi-list fs-4"></i>
</button>
</div>
{% if is_impersonating %}
<!-- Banner Impersonación Activa -->
<div class="impersonation-banner px-3 py-2 d-flex align-items-center justify-content-between text-white shadow-sm" style="background: linear-gradient(90deg, #d97706 0%, #b45309 100%); z-index: 1045; border-bottom: 2px solid #f59e0b;">
<div class="d-flex align-items-center gap-2">
<div class="spinner-grow spinner-grow-sm text-warning" role="status" style="width: 14px; height: 14px;">
<span class="visually-hidden">Impersonating...</span>
</div>
<div>
<strong class="me-1"><i class="bi bi-incognito me-1"></i>Modo Impersonación Activo:</strong>
<span>Estás navegando como</span>
<strong class="text-warning-emphasis bg-white bg-opacity-25 px-2 py-0.5 rounded">{{ impersonated_user.name or impersonated_user.first_name or impersonated_user.username }}</strong>
<span class="badge bg-dark bg-opacity-50 text-white ms-1">{{ impersonated_user.role|capitalize }}</span>
<small class="ms-2 opacity-75 d-none d-md-inline">· Registro de auditoría activo (todas las acciones se trazan a tu usuario admin)</small>
</div>
</div>
<div>
<a href="/auth/stop-impersonating" class="btn btn-sm btn-light text-dark fw-bold shadow-sm d-inline-flex align-items-center">
<i class="bi bi-box-arrow-left me-1"></i> Salir de Impersonación
</a>
</div>
</div>
{% endif %}
<main class="container-fluid app-main-container px-3 px-xl-4">
<!-- Flash Messages -->
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1080;">
</div>
@@ -466,5 +696,69 @@
</script>
{% block extra_js %}{% endblock %}
</main>
</div><!-- main-content -->
</div><!-- app-wrapper -->
{% endif %}
<!-- Sidebar Toggle JS -->
<script>
document.addEventListener("DOMContentLoaded", function() {
const sidebar = document.getElementById("sidebar");
const toggleBtn = document.getElementById("sidebarToggle");
const mobileBtn = document.getElementById("mobileMenuBtn");
const closeMobileBtn = document.getElementById("sidebarCloseMobile");
if (!sidebar) return;
function updateToggleIcon() {
if (!toggleBtn) return;
const isCollapsed = sidebar.classList.contains("collapsed");
const icon = toggleBtn.querySelector("i");
if (icon) {
icon.className = isCollapsed ? "bi bi-layout-sidebar fs-5" : "bi bi-layout-sidebar-inset fs-5";
}
toggleBtn.setAttribute("title", isCollapsed ? "Expandir menú" : "Colapsar menú");
toggleBtn.setAttribute("aria-label", isCollapsed ? "Expandir menú" : "Colapsar menú");
}
// Restore state from localStorage (desktop only)
if (window.innerWidth >= 992) {
const isCollapsed = localStorage.getItem("sidebarCollapsed") === "true";
if (isCollapsed) {
sidebar.classList.add("collapsed");
}
}
updateToggleIcon();
if (toggleBtn) {
toggleBtn.addEventListener("click", function() {
sidebar.classList.toggle("collapsed");
localStorage.setItem("sidebarCollapsed", sidebar.classList.contains("collapsed"));
updateToggleIcon();
// Collapse any open submenus when collapsing sidebar
if (sidebar.classList.contains("collapsed")) {
document.querySelectorAll('.sidebar-submenu').forEach(function(submenu) {
const collapseInstance = bootstrap.Collapse.getInstance(submenu.parentElement);
if (collapseInstance) collapseInstance.hide();
});
}
});
}
if (mobileBtn) {
mobileBtn.addEventListener("click", function() {
sidebar.classList.add("mobile-open");
});
}
if (closeMobileBtn) {
closeMobileBtn.addEventListener("click", function() {
sidebar.classList.remove("mobile-open");
});
}
});
</script>
</body>
</html>
+7 -3
View File
@@ -3,10 +3,10 @@
{% block title %}Add Classroom - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="container-fluid px-0">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1 small">
@@ -171,10 +171,14 @@
{% endblock %}
{% block extra_js %}
<script type="application/json" id="buildingFloorsData">
{{ building_floors|tojson|safe }}
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Map of existing floors per building
var buildingFloors = {{ building_floors|tojson }};
var buildingFloorsDataEl = document.getElementById('buildingFloorsData');
var buildingFloors = buildingFloorsDataEl ? JSON.parse(buildingFloorsDataEl.textContent || '{}') : {};
var buildingSelect = document.getElementById('building_id');
var newBuildingContainer = document.getElementById('newBuildingContainer');
+7 -3
View File
@@ -3,9 +3,9 @@
{% block title %}Edit Classroom - {{ classroom.code_display }}{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="container-fluid px-0">
<!-- Header -->
<div class="row mb-4 align-items-center">
<div class="row mb-3 align-items-center">
<div class="col-md-7">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1 small">
@@ -165,9 +165,13 @@
{% endblock %}
{% block extra_js %}
<script type="application/json" id="buildingFloorsData">
{{ building_floors|tojson|safe }}
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var buildingFloors = {{ building_floors|tojson }};
var buildingFloorsDataEl = document.getElementById('buildingFloorsData');
var buildingFloors = buildingFloorsDataEl ? JSON.parse(buildingFloorsDataEl.textContent || '{}') : {};
var buildingSelect = document.getElementById('building_id');
var newBuildingContainer = document.getElementById('newBuildingContainer');
+1 -1
View File
@@ -129,7 +129,7 @@
{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="container-fluid px-0">
<!-- 1. Encabezado y Selector de Entorno (Presencial vs Virtual) -->
<div class="row mb-3">
+25 -16
View File
@@ -14,13 +14,13 @@
{% block content %}
{% if user.is_authenticated %}
<div class="container-fluid pt-5 mt-4">
<div class="container-fluid px-0">
{% endif %}
<!-- ========================================================================= -->
<!-- 1. HEADER & ROLE PERSPECTIVE SWITCHER -->
<!-- ========================================================================= -->
<div class="dashboard-role-banner mb-4 shadow-sm">
<div class="dashboard-role-banner mb-3 shadow-sm">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
<div>
<div class="d-flex align-items-center gap-2 mb-1 flex-wrap">
@@ -252,12 +252,12 @@
</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 }}%;"
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" {{ ('style="width: ' ~ (stats.assigned_classrooms_pct or 0) ~ '%;"') | safe }}
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 }}%;"
<div class="progress-bar bg-warning" role="progressbar" {{ ('style="width: ' ~ (stats.unassigned_classrooms_pct or 0) ~ '%;"') | safe }}
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 }}%
@@ -274,11 +274,11 @@
</span>
</div>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-info" role="progressbar" style="width: {{ stats.assigned_today_pct }}%;"
<div class="progress-bar bg-info" role="progressbar" {{ ('style="width: ' ~ (stats.assigned_today_pct or 0) ~ '%;"') | safe }}
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 }}%;"
<div class="progress-bar bg-secondary-subtle" role="progressbar" {{ ('style="width: ' ~ (stats.unassigned_today_pct or 0) ~ '%;"') | safe }}
aria-valuenow="{{ stats.unassigned_today_pct }}" aria-valuemin="0" aria-valuemax="100"
title="Libres hoy: {{ stats.unassigned_today_classrooms }}">
</div>
@@ -596,12 +596,12 @@
</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 }}%;"
<div class="progress-bar bg-success progress-bar-striped" role="progressbar" {{ ('style="width: ' ~ (stats.assigned_classrooms_pct or 0) ~ '%;"') | safe }}
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 }}%;"
<div class="progress-bar bg-warning" role="progressbar" {{ ('style="width: ' ~ (stats.unassigned_classrooms_pct or 0) ~ '%;"') | safe }}
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 }}%
@@ -618,11 +618,11 @@
</span>
</div>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-info" role="progressbar" style="width: {{ stats.assigned_today_pct }}%;"
<div class="progress-bar bg-info" role="progressbar" {{ ('style="width: ' ~ (stats.assigned_today_pct or 0) ~ '%;"') | safe }}
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 }}%;"
<div class="progress-bar bg-secondary-subtle" role="progressbar" {{ ('style="width: ' ~ (stats.unassigned_today_pct or 0) ~ '%;"') | safe }}
aria-valuenow="{{ stats.unassigned_today_pct }}" aria-valuemin="0" aria-valuemax="100"
title="Libres hoy: {{ stats.unassigned_today_classrooms }}">
</div>
@@ -1346,14 +1346,14 @@
</a>
</div>
<div class="col-md-6 col-lg-3">
<a href="/classrooms/list_classrooms" class="card h-100 p-3 action-menu-card bg-body">
<a href="/schedule/my_grades" class="card h-100 p-3 action-menu-card bg-body">
<div class="d-flex align-items-center">
<div class="quick-icon-circle bg-warning-subtle text-warning me-3">
<i class="bi bi-buildings fs-4"></i>
<i class="bi bi-journal-check fs-4"></i>
</div>
<div>
<h6 class="mb-1 fw-bold text-body">Espacios del Campus</h6>
<p class="text-muted small mb-0">Sedes, edificios y planos</p>
<h6 class="mb-1 fw-bold text-body">Mis Calificaciones</h6>
<p class="text-muted small mb-0">Notas, regularidad y actas</p>
</div>
</div>
</a>
@@ -1375,7 +1375,7 @@
<table class="table table-hover align-middle mb-0">
<thead class="table-light small">
<tr>
<th class="ps-3">Fecha</th>
<th class="ps-3">Fecha & Cuenta Regresiva</th>
<th>Tipo de Hito</th>
<th>Materia / Código</th>
<th>Aula / Enlace</th>
@@ -1386,7 +1386,16 @@
{% for m in role_data.upcoming_milestones %}
<tr>
<td class="ps-3 font-monospace fw-bold text-primary">
{{ m.date }}
<div>{{ m.date }}</div>
{% if m.days_remaining is not none and m.days_remaining is defined %}
{% if m.days_remaining == 0 %}
<span class="badge bg-danger text-white small">¡Hoy!</span>
{% elif m.days_remaining == 1 %}
<span class="badge bg-warning text-dark small">¡Mañana!</span>
{% elif m.days_remaining > 1 %}
<span class="badge bg-info-subtle text-info border small">Faltan {{ m.days_remaining }} días</span>
{% endif %}
{% endif %}
</td>
<td>
<span class="badge" data-color="{{ m.type_obj.color if m.type_obj else '#0d6efd' }}" style="background-color:var(--badge-bg,#0d6efd);color:#fff;">
+185 -28
View File
@@ -31,17 +31,35 @@
transition: background-color 0.25s ease, color 0.25s ease;
}
/* =============================================
NAVBAR COMPACTA — limpia y sin desperdicio
============================================= */
.glass-navbar {
padding: 0 !important; /* Bootstrap agrega padding, lo neutralizamos */
min-height: 48px;
}
.glass-navbar .container-fluid {
padding-top: 0.35rem;
padding-bottom: 0.35rem;
}
/* --- MODO CLARO ([data-bs-theme="light"]) --- */
[data-bs-theme="light"] body {
background: #F8FAFC !important;
color: #1E293B !important;
}
[data-bs-theme="light"] .glass-navbar {
background: rgba(255, 255, 255, 0.88) !important;
background: rgba(255, 255, 255, 0.92) !important;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04) !important;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05) !important;
}
[data-bs-theme="light"] .glass-navbar .navbar-brand span {
background: linear-gradient(90deg, #B43E8E, #682CAD);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
[data-bs-theme="light"] .glass-navbar .nav-link {
color: #334155 !important;
@@ -49,6 +67,8 @@
[data-bs-theme="light"] .glass-navbar .nav-link:hover,
[data-bs-theme="light"] .glass-navbar .nav-link.active {
color: #B43E8E !important;
background: rgba(180, 62, 142, 0.06);
border-radius: 7px;
}
[data-bs-theme="light"] .glass-card,
[data-bs-theme="light"] .card {
@@ -57,6 +77,16 @@
border: 1px solid rgba(0, 0, 0, 0.08) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04) !important;
}
/* Toggle button - modo claro */
[data-bs-theme="light"] .navbar-toggle-custom {
color: #334155;
border-color: rgba(51, 65, 85, 0.25);
}
[data-bs-theme="light"] .navbar-toggle-custom:hover {
background: rgba(180, 62, 142, 0.08);
border-color: #B43E8E;
color: #B43E8E;
}
/* --- MODO OSCURO ([data-bs-theme="dark"]) --- */
[data-bs-theme="dark"] body {
@@ -64,11 +94,17 @@
color: #F8FAFC !important;
}
[data-bs-theme="dark"] .glass-navbar {
background: rgba(15, 23, 42, 0.78) !important;
background: rgba(15, 23, 42, 0.82) !important;
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3) !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.07) !important;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.35) !important;
}
[data-bs-theme="dark"] .glass-navbar .navbar-brand span {
background: linear-gradient(90deg, #FF8AB6, #AE7FFF);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
[data-bs-theme="dark"] .glass-navbar .nav-link {
color: #CBD5E1 !important;
@@ -76,6 +112,8 @@
[data-bs-theme="dark"] .glass-navbar .nav-link:hover,
[data-bs-theme="dark"] .glass-navbar .nav-link.active {
color: #FF8AB6 !important;
background: rgba(255, 138, 182, 0.08);
border-radius: 7px;
}
[data-bs-theme="dark"] .glass-card,
[data-bs-theme="dark"] .card {
@@ -84,18 +122,123 @@
border: 1px solid rgba(255, 255, 255, 0.08) !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
}
.navbar-brand span {
font-weight: 700;
background: linear-gradient(90deg, #B43E8E, #682CAD);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
/* Toggle button - modo oscuro */
[data-bs-theme="dark"] .navbar-toggle-custom {
color: #CBD5E1;
border-color: rgba(203, 213, 225, 0.25);
}
[data-bs-theme="dark"] .navbar-toggle-custom:hover {
background: rgba(255, 138, 182, 0.1);
border-color: rgba(255, 138, 182, 0.5);
color: #FF8AB6;
}
/* =============================================
BRAND / LOGO
============================================= */
.navbar-brand {
font-weight: 700;
padding-top: 0;
padding-bottom: 0;
}
.navbar-brand span {
font-weight: 700;
font-size: 0.95rem;
letter-spacing: -0.01em;
}
.navbar-logo {
height: 26px;
width: auto;
}
/* =============================================
NAV LINKS — compactos
============================================= */
.glass-navbar .nav-link {
font-size: 0.84rem;
font-weight: 500;
padding: 0.3rem 0.65rem !important;
border-radius: 7px;
transition: color 0.15s ease, background 0.15s ease;
white-space: nowrap;
}
.glass-navbar .nav-link i {
font-size: 0.82rem;
}
/* =============================================
TOGGLE BUTTON — limpio, pill style
============================================= */
.navbar-toggle-custom {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 32px;
padding: 0;
border-radius: 8px;
border: 1px solid transparent;
background: transparent;
cursor: pointer;
transition: all 0.18s ease;
outline: none;
box-shadow: none !important;
}
.navbar-toggle-custom i {
font-size: 1.3rem;
line-height: 1;
}
/* =============================================
MAIN CONTAINER — espacio eficiente
============================================= */
.main-container {
flex: 1;
padding: 2rem 0;
padding: 1rem 0 1.5rem;
}
/* =============================================
RESPONSIVE — 720p (1360x768)
============================================= */
@media (max-height: 800px) {
.main-container {
padding: 0.65rem 0 1rem;
}
.glass-navbar .container-fluid {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
}
/* =============================================
RESPONSIVE — 1920x1080 (pantallas grandes)
============================================= */
@media (min-width: 1600px) {
.main-container {
padding: 1.25rem 0 2rem;
max-width: 1600px;
}
}
/* =============================================
MENU COLAPSADO (mobile / tablet)
============================================= */
@media (max-width: 991.98px) {
.glass-navbar .navbar-collapse {
padding: 0.5rem 0;
border-top: 1px solid rgba(128, 128, 128, 0.12);
margin-top: 0.3rem;
}
.glass-navbar .nav-link {
padding: 0.45rem 0.75rem !important;
border-radius: 8px;
margin: 1px 0;
}
.glass-navbar .navbar-nav.ms-auto {
padding-top: 0.5rem;
border-top: 1px solid rgba(128, 128, 128, 0.1);
margin-top: 0.25rem !important;
}
}
.fade-in-up {
@@ -117,42 +260,56 @@
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg glass-navbar sticky-top">
<div class="container-fluid px-4">
<a class="navbar-brand d-flex align-items-center" href="/dashboard">
<img src="/img/logo_unicaba_mini.png" alt="Logo UniCABA" class="navbar-logo me-2" height="30">
<div class="container-fluid px-3 px-lg-4">
<!-- Brand -->
<a class="navbar-brand d-flex align-items-center gap-2" href="/dashboard">
<img src="/img/logo_unicaba_mini.png" alt="Logo UniCABA" class="navbar-logo">
<span>Edu-Space Admin</span>
</a>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<i class="bi bi-list text-light fs-2"></i>
<!-- Toggle button — limpio, sin foco feo de Bootstrap -->
<button
class="navbar-toggle-custom d-lg-none"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Menú"
>
<i class="bi bi-list"></i>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto align-items-center ms-3">
<!-- Nav principal -->
<ul class="navbar-nav me-auto align-items-lg-center ms-lg-2 gap-lg-1">
<li class="nav-item">
<a class="nav-link px-3" href="/dashboard">
<a class="nav-link" href="/dashboard">
<i class="bi bi-speedometer2 me-1"></i>Panel General
</a>
</li>
<li class="nav-item">
<a class="nav-link px-3" href="/classrooms/list_classrooms">
<i class="bi bi-door-open me-1"></i>Espacios & Sedes
<a class="nav-link" href="/classrooms/list_classrooms">
<i class="bi bi-door-open me-1"></i>Espacios &amp; Sedes
</a>
</li>
<li class="nav-item">
<a class="nav-link px-3" href="/schedule/calendar_view">
<i class="bi bi-calendar3 me-1"></i>Cronograma & Reservas
<a class="nav-link" href="/schedule/calendar_view">
<i class="bi bi-calendar3 me-1"></i>Cronograma &amp; Reservas
</a>
</li>
<li class="nav-item">
<a class="nav-link px-3 active text-primary fw-semibold" href="/predictive">
<a class="nav-link" href="/predictive">
<i class="bi bi-cpu me-1"></i>Módulo Predictivo
</a>
</li>
</ul>
<ul class="navbar-nav ms-auto align-items-center">
<!-- Nav derecha -->
<ul class="navbar-nav align-items-lg-center ms-auto gap-2">
<li class="nav-item">
<a class="btn btn-outline-danger btn-sm rounded-pill px-3 fw-medium" href="/auth/logout">
<i class="bi bi-box-arrow-right me-1"></i> Cerrar Sesión
<i class="bi bi-box-arrow-right me-1"></i>Cerrar Sesión
</a>
</li>
</ul>
@@ -161,7 +318,7 @@
</nav>
<!-- Main Content -->
<main class="container main-container">
<main class="container-fluid container-xl main-container px-3 px-lg-4">
{% block content %}{% endblock %}
</main>
+240 -29
View File
@@ -3,29 +3,29 @@
{% block title %}Reservations Calendar - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="container-fluid px-0">
<!-- Header -->
<div class="row mb-4 align-items-center">
<div class="row mb-3 align-items-center">
<div class="col-md-7">
<h1 class="h3 mb-1">
<h1 class="h4 mb-1 fw-bold">
<i class="bi bi-calendar3 text-primary me-2"></i>Reservations Calendar
</h1>
<p class="text-muted small mb-0">View & Manage Reservations — Horarios de cursada y asignación de aulas UniCABA</p>
<p class="text-muted small mb-0">Gestión de Reservas — Horarios de cursada y asignación de aulas UniCABA</p>
</div>
<div class="col-md-5 text-md-end mt-3 mt-md-0">
<div class="col-md-5 text-md-end mt-2 mt-md-0">
{% if user.has_permission('reservations', 'read_write') %}
<a href="/schedule/add_reservation" class="btn btn-primary me-2">
<a href="/schedule/add_reservation" class="btn btn-primary btn-sm me-2">
<i class="bi bi-plus-circle me-1"></i>Add Reservation
</a>
{% endif %}
<a href="/schedule/list_reservations" class="btn btn-outline-secondary">
<a href="/schedule/list_reservations" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-list me-1"></i>List View
</a>
</div>
</div>
<!-- Filters Toolbar (Classroom & Subject Filters to prevent overlapping) -->
<div class="card border-0 shadow-sm mb-4">
<div class="card border-0 shadow-sm mb-3">
<div class="card-body p-3">
<div class="row g-2 align-items-center">
<!-- Filter by Classroom -->
@@ -141,32 +141,81 @@
</div>
</div>
<!-- Calendar Card -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary border-0 py-3">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-3">
<span class="fw-bold small text-secondary text-uppercase tracking-wide">
<i class="bi bi-info-circle me-1"></i>Referencia:
<!-- Grid Layout: Panel Lateral de Comisiones Pendientes + Calendario Principal -->
<div class="row g-3">
<!-- Sidebar de Comisiones Pendientes de Aula (Drag & Drop) -->
<div class="col-xl-3 col-lg-4" id="unassignedCommissionsSidebar">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-body-tertiary border-0 py-2 d-flex justify-content-between align-items-center">
<span class="fw-bold small text-body">
<i class="bi bi-grip-vertical text-primary me-1"></i>Comisiones por Asignar
</span>
<div class="d-flex align-items-center">
<span class="badge badge-brand me-1" style="width: 14px; height: 14px; border-radius: 4px;"></span>
<small class="text-muted">Clase Confirmada</small>
</div>
<div class="d-flex align-items-center">
<span class="badge bg-warning me-1" style="width: 14px; height: 14px; border-radius: 4px;"></span>
<small class="text-muted">Pendiente</small>
</div>
<span class="badge bg-primary rounded-pill" id="unassignedCount">{{ unassigned_commissions|length }}</span>
</div>
<div class="text-muted small">
<i class="bi bi-lightning-charge-fill text-warning me-1"></i>Haz clic en cualquier clase para ver los detalles completos
<div class="card-body p-2" style="max-height: 800px; overflow-y: auto;">
<div class="alert alert-info border-0 py-2 px-2 small mb-2 d-flex align-items-center">
<i class="bi bi-info-circle-fill me-2 fs-6"></i>
<span>Arrastra una comisión a la grilla para asignarla al aula filtrada.</span>
</div>
<div id="externalEventsContainer" class="d-flex flex-column gap-2">
{% for c in unassigned_commissions %}
<div class="fc-event-drag-item p-2 rounded-2 border shadow-sm"
data-commission-id="{{ c.id }}"
data-subject-name="{{ c.subject.name if c.subject else 'Materia' }}"
data-code="{{ c.get_full_code or c.code }}"
data-students="{{ c.current_students or 0 }}"
data-shift="{{ c.shift or 'Regular' }}"
style="cursor: grab; background-color: var(--bs-body-bg); border-left: 4px solid #B43E8E !important; user-select: none;">
<div class="d-flex justify-content-between align-items-center">
<strong class="text-primary small font-monospace">{{ c.get_full_code or c.code }}</strong>
<span class="badge bg-secondary-subtle text-secondary small"><i class="bi bi-people me-1"></i>{{ c.current_students or 0 }} al.</span>
</div>
<div class="small fw-semibold text-truncate text-body mt-1">{{ c.subject.name if c.subject else 'Materia' }}</div>
<div class="d-flex justify-content-between align-items-center mt-1">
<small class="text-muted" style="font-size: 0.72rem;">Turno: {{ c.shift or 'Regular' }}</small>
<span class="badge bg-primary-subtle text-primary border" style="font-size: 0.65rem;">Arrastrar <i class="bi bi-arrows-move"></i></span>
</div>
</div>
{% else %}
<div class="text-center py-4 text-muted small" id="noUnassignedPlaceholder">
<i class="bi bi-check2-circle fs-3 text-success d-block mb-1"></i>
Todas las comisiones tienen aula asignada.
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="card-body p-3">
<!-- FullCalendar Container -->
<div id="calendar"></div>
<!-- Columna del Calendario Principal -->
<div class="col-xl-9 col-lg-8" id="calendarMainCol">
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary border-0 py-3">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-3">
<span class="fw-bold small text-secondary text-uppercase tracking-wide">
<i class="bi bi-info-circle me-1"></i>Referencia:
</span>
<div class="d-flex align-items-center">
<span class="badge badge-brand me-1" style="width: 14px; height: 14px; border-radius: 4px;"></span>
<small class="text-muted">Clase Confirmada</small>
</div>
<div class="d-flex align-items-center">
<span class="badge bg-warning me-1" style="width: 14px; height: 14px; border-radius: 4px;"></span>
<small class="text-muted">Pendiente</small>
</div>
</div>
<div class="text-muted small">
<i class="bi bi-arrows-move text-primary me-1"></i><strong>Drag & Drop activo:</strong> Mueve clases en el calendario con validación de choques en tiempo real
</div>
</div>
</div>
<div class="card-body p-3">
<!-- FullCalendar Container -->
<div id="calendar"></div>
</div>
</div>
</div>
</div>
@@ -759,6 +808,10 @@ document.addEventListener('DOMContentLoaded', function() {
initialView: 'timeGridWeek',
initialDate: todayDate,
allDaySlot: false,
editable: true,
droppable: true,
eventDurationEditable: true,
eventResizableFromStart: false,
headerToolbar: {
left: 'prev,next today',
center: 'title',
@@ -784,6 +837,17 @@ document.addEventListener('DOMContentLoaded', function() {
slotEventOverlap: true,
expandRows: true,
stickyHeaderDates: true,
/* Drag and Drop Handlers con Validación de Colisión en Tiempo Real */
eventDrop: function(info) {
handleEventDragOrResize(info, false);
},
eventResize: function(info) {
handleEventDragOrResize(info, true);
},
eventReceive: function(info) {
handleExternalDrop(info);
},
/* Dynamic Server-Side Filtered Events */
events: function(info, successCallback, failureCallback) {
@@ -1018,6 +1082,153 @@ document.addEventListener('DOMContentLoaded', function() {
modal.show();
}
});
// Soporte Drag and Drop externo para Comisiones Pendientes
var containerEl = document.getElementById('externalEventsContainer');
if (containerEl && typeof FullCalendar.Draggable !== 'undefined') {
new FullCalendar.Draggable(containerEl, {
itemSelector: '.fc-event-drag-item',
eventData: function(eventEl) {
return {
title: (eventEl.getAttribute('data-code') || 'Comisión') + ' - ' + (eventEl.getAttribute('data-subject-name') || ''),
extendedProps: {
commission_id: eventEl.getAttribute('data-commission-id'),
subject_name: eventEl.getAttribute('data-subject-name'),
is_external_commission: true
},
duration: '02:00'
};
}
});
}
function showToast(title, message, details, type) {
var toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3';
toastContainer.style.zIndex = '1090';
document.body.appendChild(toastContainer);
}
var bgClass = type === 'danger' ? 'bg-danger text-white' : (type === 'warning' ? 'bg-warning text-dark' : 'bg-success text-white');
var iconClass = type === 'danger' ? 'bi-exclamation-octagon-fill' : (type === 'warning' ? 'bi-exclamation-triangle-fill' : 'bi-check-circle-fill');
var detailsHtml = '';
if (details && details.length > 0) {
detailsHtml = '<ul class="mb-0 mt-1 ps-3 small">' + details.map(function(d) { return '<li>' + d + '</li>'; }).join('') + '</ul>';
}
var toastEl = document.createElement('div');
toastEl.className = 'toast align-items-center ' + bgClass + ' border-0 shadow-lg mb-2';
toastEl.setAttribute('role', 'alert');
toastEl.setAttribute('aria-live', 'assertive');
toastEl.setAttribute('aria-atomic', 'true');
toastEl.innerHTML = '<div class="d-flex"><div class="toast-body"><div class="fw-bold"><i class="bi ' + iconClass + ' me-2"></i>' + title + '</div><div class="small">' + message + '</div>' + detailsHtml + '</div><button type="button" class="btn-close ' + (type === 'warning' ? '' : 'btn-close-white') + ' me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button></div>';
toastContainer.appendChild(toastEl);
var bsToast = new bootstrap.Toast(toastEl, { delay: 6000 });
bsToast.show();
toastEl.addEventListener('hidden.bs.toast', function() {
toastEl.remove();
});
}
function handleEventDragOrResize(info, isResize) {
var startIso = info.event.start ? info.event.start.toISOString() : null;
var endIso = info.event.end ? info.event.end.toISOString() : (info.event.start ? new Date(info.event.start.getTime() + 2 * 3600 * 1000).toISOString() : null);
var ext = info.event.extendedProps || {};
var classroomId = filterRoom.value ? parseInt(filterRoom.value, 10) : (ext.classroom_id || null);
fetch('/api/v1/reservations/drag-update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
reservation_id: parseInt(info.event.id, 10),
start_time: startIso,
end_time: endIso,
classroom_id: classroomId
})
})
.then(function(res) {
return res.json().then(function(data) {
return { status: res.status, data: data };
});
})
.then(function(result) {
if (result.status === 409 || (result.data && result.data.status === 'conflict')) {
info.revert();
var reasons = (result.data.conflicts || []).concat(result.data.block_reasons || []);
showToast('Conflicto de Horario Detectado', result.data.message || 'No se puede reubicar la clase en ese espacio/horario.', reasons, 'danger');
} else if (result.status >= 400) {
info.revert();
showToast('Error', result.data.message || 'Ocurrió un error al actualizar la reserva.', [], 'danger');
} else {
showToast('Reserva Actualizada', result.data.message || 'Horario y aula confirmados con éxito.', [], 'success');
}
})
.catch(function(err) {
info.revert();
console.error('Drag update error:', err);
showToast('Error de Conexión', 'No se pudo comunicar con el servidor.', [], 'danger');
});
}
function handleExternalDrop(info) {
var ext = info.event.extendedProps || {};
var commissionId = ext.commission_id;
var startIso = info.event.start ? info.event.start.toISOString() : null;
var endIso = info.event.end ? info.event.end.toISOString() : (info.event.start ? new Date(info.event.start.getTime() + 2 * 3600 * 1000).toISOString() : null);
var classroomId = filterRoom.value ? parseInt(filterRoom.value, 10) : null;
if (!classroomId) {
info.revert();
showToast('Aula Requerida', 'Por favor selecciona un Aula en el filtro superior antes de arrastrar comisiones a la grilla.', [], 'warning');
return;
}
fetch('/api/v1/reservations/drag-update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
commission_id: parseInt(commissionId, 10),
start_time: startIso,
end_time: endIso,
classroom_id: classroomId
})
})
.then(function(res) {
return res.json().then(function(data) {
return { status: res.status, data: data };
});
})
.then(function(result) {
if (result.status === 409 || (result.data && result.data.status === 'conflict')) {
info.revert();
var reasons = (result.data.conflicts || []).concat(result.data.block_reasons || []);
showToast('Conflicto al Asignar Comisión', result.data.message || 'El aula o docente no están disponibles en este horario.', reasons, 'danger');
} else if (result.status >= 400) {
info.revert();
showToast('Error', result.data.message || 'No se pudo asignar el aula a la comisión.', [], 'danger');
} else {
showToast('Comisión Asignada', result.data.message || 'Comisión asignada con éxito al aula.', [], 'success');
var draggedItem = document.querySelector('.fc-event-drag-item[data-commission-id="' + commissionId + '"]');
if (draggedItem) {
draggedItem.remove();
var countBadge = document.getElementById('unassignedCount');
if (countBadge) {
var curr = parseInt(countBadge.textContent, 10) || 1;
countBadge.textContent = Math.max(0, curr - 1);
}
}
calendar.refetchEvents();
}
})
.catch(function(err) {
info.revert();
console.error('External drop error:', err);
showToast('Error de Conexión', 'No se pudo registrar la asignación.', [], 'danger');
});
}
calendar.render();
+4 -4
View File
@@ -512,9 +512,9 @@
{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="container-fluid px-0">
<!-- Header -->
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-3">
<div>
<h1 class="h3 fw-bold mb-1">
<i class="bi bi-diagram-3 text-primary me-2"></i>Gestión de Comisiones y Cursadas
@@ -720,7 +720,7 @@
<div class="d-flex flex-wrap align-items-center gap-1">
{% for m in commission.academic_milestones %}
<span class="badge comm-milestone-chip text-white d-inline-flex align-items-center py-1 px-2"
style="background-color: {{ m.color }}; font-size: 0.72rem;"
{{ ('style="background-color: ' ~ (m.color or '#6c757d') ~ '; font-size: 0.72rem;"') | safe }}
title="{{ m.title }} - {{ m.date if m.date else '' }} ({{ m.classroom or 'Campus Virtual' }})">
<i class="bi bi-flag-fill me-1" style="font-size: 0.65rem;"></i>
{{ m.title or m.tipo }}
@@ -753,7 +753,7 @@
{% set fill_pct = (commission.current_students / commission.max_students * 100) if commission.max_students > 0 else 0 %}
<div class="progress mt-1 mx-auto" style="height: 4px; width: 50px;">
<div class="progress-bar {% if fill_pct > 90 %}bg-danger{% elif fill_pct > 70 %}bg-warning{% else %}bg-success{% endif %}"
style="width: {{ [fill_pct, 100]|min }}%;"></div>
{{ ('style="width: ' ~ ([fill_pct, 100]|min) ~ '%;"') | safe }}></div>
</div>
</td>
<td class="text-end pe-4">
+527
View File
@@ -0,0 +1,527 @@
{% extends 'base.html' %}
{% block title %}Libro de Calificaciones — {{ commission.full_code or commission.code }} — Edu-Space{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb small">
<li class="breadcrumb-item"><a href="/dashboard">Inicio</a></li>
<li class="breadcrumb-item"><a href="/admin/commissions_list">Comisiones</a></li>
<li class="breadcrumb-item"><a href="/admin/commission_detail?id={{ commission.id }}">{{ commission.full_code or commission.code }}</a></li>
<li class="breadcrumb-item active">Libro de Calificaciones</li>
</ol>
</nav>
<!-- Header Banner -->
<div class="card border-0 shadow-sm mb-4 bg-body">
<div class="card-body p-4">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
<div>
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-primary-subtle text-primary border border-primary-subtle font-monospace px-2 py-1">
{{ commission.full_code or commission.code }}
</span>
<span class="badge bg-secondary-subtle text-secondary border font-monospace">
{{ commission.semester }} {{ commission.year }}
</span>
{% if commission.grades_closed %}
<span class="badge bg-danger text-white px-2 py-1 shadow-sm">
<i class="bi bi-lock-fill me-1"></i>ACTA CERRADA ({{ commission.acta_number }})
</span>
{% else %}
<span class="badge bg-success-subtle text-success border border-success-subtle px-2 py-1">
<i class="bi bi-unlock-fill me-1"></i>CALIFICACIONES ABIERTAS
</span>
{% endif %}
</div>
<h3 class="fw-bold mb-1">{{ commission.subject.name if commission.subject else 'Materia' }}</h3>
<p class="text-muted small mb-0">
<i class="bi bi-clock me-1"></i>Turno: <strong>{{ commission.shift or 'A definir' }}</strong>
{% if commission.teachers and commission.teachers|length > 0 %}
<span class="mx-2">·</span>
<i class="bi bi-person-badge me-1"></i>Cátedra:
{% for t in commission.teachers %}
<span class="badge bg-light text-dark border me-1">{{ t.name }} ({{ t.role_in_commission or 'Docente' }})</span>
{% endfor %}
{% endif %}
</p>
</div>
<div class="d-flex flex-wrap align-items-center gap-2">
<!-- Live Auto-save indicator -->
<div id="saveStatusIndicator" class="small px-3 py-1 rounded-pill bg-body-tertiary border text-muted d-flex align-items-center gap-2">
<i class="bi bi-cloud-check text-success fs-6" id="saveStatusIcon"></i>
<span id="saveStatusText">Todos los cambios guardados</span>
</div>
<!-- Print / Export -->
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="window.print()">
<i class="bi bi-printer me-1"></i>Imprimir Planilla
</button>
<!-- Acta Action Buttons -->
{% if commission.grades_closed %}
{% if role in ['admin', 'bedelia'] %}
<button type="button" class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#reopenActaModal">
<i class="bi bi-unlock me-1"></i>Reabrir Acta
</button>
{% endif %}
{% else %}
<button type="button" class="btn btn-danger btn-sm shadow-sm" data-bs-toggle="modal" data-bs-target="#closeActaModal">
<i class="bi bi-file-earmark-lock me-1"></i>Cerrar Acta Formal
</button>
{% endif %}
</div>
</div>
</div>
</div>
<!-- KPI Summary Cards -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm p-3 h-100">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="text-muted small fw-semibold text-uppercase">Total Inscriptos</span>
<h3 class="fw-bold mb-0 mt-1" id="kpiTotalStudents">{{ students|length }}</h3>
</div>
<div class="bg-primary bg-opacity-10 text-primary rounded-circle p-3">
<i class="bi bi-people fs-4"></i>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm p-3 h-100">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="text-muted small fw-semibold text-uppercase">Promocionados (≥ 7)</span>
<h3 class="fw-bold mb-0 mt-1 text-success" id="kpiPromoted">0</h3>
</div>
<div class="bg-success bg-opacity-10 text-success rounded-circle p-3">
<i class="bi bi-award fs-4"></i>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm p-3 h-100">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="text-muted small fw-semibold text-uppercase">Regulares (4 a 6.9)</span>
<h3 class="fw-bold mb-0 mt-1 text-primary" id="kpiRegular">0</h3>
</div>
<div class="bg-info bg-opacity-10 text-info rounded-circle p-3">
<i class="bi bi-check2-circle fs-4"></i>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm p-3 h-100">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="text-muted small fw-semibold text-uppercase">Libres (< 4)</span>
<h3 class="fw-bold mb-0 mt-1 text-danger" id="kpiLibre">0</h3>
</div>
<div class="bg-danger bg-opacity-10 text-danger rounded-circle p-3">
<i class="bi bi-x-circle fs-4"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Gradebook Matrix Table Card -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h5 class="mb-0 h6 fw-bold">
<i class="bi bi-table me-2 text-primary"></i>Matriz de Evaluaciones y Calificaciones
</h5>
<small class="text-muted">Las notas se guardan automáticamente al escribirlas. Escala: 1 a 10 (Ausente: A o vacío).</small>
</div>
<div class="d-flex align-items-center gap-2">
<input type="text" id="filterStudentInput" class="form-control form-control-sm" placeholder="Buscar alumno o legajo..." style="max-width: 220px;">
</div>
</div>
<div class="card-body p-0">
{% if students and students|length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="gradebookTable">
<thead class="table-light small">
<tr>
<th class="ps-3" style="width: 60px;">#</th>
<th style="min-width: 220px;">Estudiante</th>
<th style="min-width: 110px;">Legajo</th>
{% for col in milestone_columns %}
<th class="text-center" style="min-width: 130px;">
<div>{{ col.title }}</div>
<small class="text-muted fw-normal font-monospace">({{ col.date }})</small>
</th>
{% endfor %}
<th class="text-center" style="min-width: 110px;">Promedio</th>
<th class="text-center pe-3" style="min-width: 140px;">Condición Final</th>
</tr>
</thead>
<tbody>
{% for s in students %}
<tr data-student-id="{{ s.id }}" data-student-name="{{ s.name|lower }}" data-student-dni="{{ s.dni or '' }}">
<td class="ps-3 font-monospace text-muted small">{{ loop.index }}</td>
<td>
<div class="fw-semibold text-body">{{ s.name }}</div>
<small class="text-muted font-monospace">{{ s.email }}</small>
</td>
<td>
<span class="badge bg-secondary-subtle text-secondary font-monospace">
{{ s.dni or ('LEG-' + s.id) }}
</span>
</td>
{% for col in milestone_columns %}
{% set grade_obj = s.grades[col.id] %}
<td class="text-center">
<div class="d-flex justify-content-center align-items-center">
<input
type="number"
step="0.5"
min="1"
max="10"
class="form-control form-control-sm text-center font-monospace fw-bold grade-input"
style="max-width: 85px; font-size: 0.95rem;"
data-milestone-id="{{ col.id }}"
data-student-id="{{ s.id }}"
value="{{ grade_obj.score if grade_obj and grade_obj.score is not none else '' }}"
placeholder="-"
{% if commission.grades_closed %}disabled{% endif %}
>
</div>
</td>
{% endfor %}
<!-- Promedio calculado en vivo -->
<td class="text-center">
<span class="font-monospace fw-bold fs-6 avg-cell">-</span>
</td>
<!-- Condición académica calculada en vivo -->
<td class="text-center pe-3">
<span class="badge condition-badge font-monospace py-2 px-2">Pendiente</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-people fs-1 text-muted d-block mb-3"></i>
<h5 class="fw-bold text-muted">No hay estudiantes inscriptos en esta comisión</h5>
<p class="text-muted small">Puede matricular alumnos desde el panel de detalle de la comisión.</p>
<a href="/admin/commission_detail?id={{ commission.id }}" class="btn btn-outline-primary btn-sm">
<i class="bi bi-person-plus me-1"></i>Ir a Matriculación
</a>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Modal: Cierre Formal de Acta -->
<div class="modal fade" id="closeActaModal" tabindex="-1" aria-labelledby="closeActaModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title h6 fw-bold" id="closeActaModalLabel">
<i class="bi bi-shield-lock me-1"></i>Cierre Formal de Acta de Regularidad
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning small mb-3">
<i class="bi bi-exclamation-triangle-fill me-1"></i>
<strong>Atención:</strong> Al cerrar el acta, todas las notas se congelan legalmente y se genera un número de acta único e inmutable. Sólo el personal de Bedelía o Administradores podrá reabrirla con justificación escrita.
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Observaciones o Notas de Cátedra (Opcional):</label>
<textarea id="closeActaNotes" class="form-control small" rows="3" placeholder="Ej: Cursada cerrada con 22 alumnos regulares y 5 promocionados."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-sm btn-danger" id="btnConfirmCloseActa">
<i class="bi bi-lock-fill me-1"></i>Confirmar y Sellar Acta
</button>
</div>
</div>
</div>
</div>
<!-- Modal: Reapertura de Acta (Admin / Bedelía) -->
<div class="modal fade" id="reopenActaModal" tabindex="-1" aria-labelledby="reopenActaModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title h6 fw-bold" id="reopenActaModalLabel">
<i class="bi bi-unlock-fill me-1"></i>Reapertura Excepcional de Acta
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="small text-muted mb-3">
Esta acción quedará asentada en la bitácora inmutable de auditoría institucional.
</p>
<div class="mb-3">
<label class="form-label small fw-semibold">Motivo formal de reapertura (Obligatorio):</label>
<textarea id="reopenActaReason" class="form-control small" rows="3" placeholder="Ej: Rectificación de nota de examen recuperatorio según resolución..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-sm btn-warning" id="btnConfirmReopenActa">
<i class="bi bi-unlock me-1"></i>Reabrir Acta
</button>
</div>
</div>
</div>
</div>
<div id="gradebookConfig" data-commission-id="{{ commission.id }}" data-is-closed="{{ 'true' if commission.grades_closed else 'false' }}" class="d-none"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const config = document.getElementById('gradebookConfig');
const commissionId = config ? config.dataset.commissionId : '';
const isClosed = config ? config.dataset.isClosed === 'true' : false;
const statusIndicator = document.getElementById('saveStatusIndicator');
const statusIcon = document.getElementById('saveStatusIcon');
const statusText = document.getElementById('saveStatusText');
function updateStatus(state, msg) {
if (state === 'saving') {
statusIcon.className = 'spinner-border spinner-border-sm text-primary';
statusText.textContent = msg || 'Guardando calificación...';
} else if (state === 'saved') {
statusIcon.className = 'bi bi-cloud-check text-success fs-6';
statusText.textContent = msg || 'Todos los cambios guardados';
} else if (state === 'error') {
statusIcon.className = 'bi bi-exclamation-circle text-danger fs-6';
statusText.textContent = msg || 'Error al guardar';
}
}
// Recalculate row averages and condition
function recalculateAllRows() {
let countPromoted = 0;
let countRegular = 0;
let countLibre = 0;
const rows = document.querySelectorAll('#gradebookTable tbody tr');
rows.forEach(row => {
const inputs = row.querySelectorAll('.grade-input');
const avgCell = row.querySelector('.avg-cell');
const badgeCell = row.querySelector('.condition-badge');
let sum = 0;
let count = 0;
let hasGrades = false;
inputs.forEach(inp => {
const val = parseFloat(inp.value);
if (!isNaN(val) && val >= 1 && val <= 10) {
sum += val;
count++;
hasGrades = true;
inp.classList.remove('is-invalid');
} else if (inp.value.trim() !== '') {
inp.classList.add('is-invalid');
} else {
inp.classList.remove('is-invalid');
}
});
if (count > 0) {
const avg = sum / count;
avgCell.textContent = avg.toFixed(1);
if (avg >= 7) {
badgeCell.className = 'badge bg-success-subtle text-success border border-success-subtle font-monospace py-2 px-2';
badgeCell.textContent = 'PROMOCIONADO';
countPromoted++;
} else if (avg >= 4) {
badgeCell.className = 'badge bg-primary-subtle text-primary border border-primary-subtle font-monospace py-2 px-2';
badgeCell.textContent = 'REGULAR';
countRegular++;
} else {
badgeCell.className = 'badge bg-danger-subtle text-danger border border-danger-subtle font-monospace py-2 px-2';
badgeCell.textContent = 'LIBRE';
countLibre++;
}
} else {
avgCell.textContent = '-';
badgeCell.className = 'badge bg-secondary-subtle text-secondary border font-monospace py-2 px-2';
badgeCell.textContent = 'PENDIENTE';
}
});
document.getElementById('kpiPromoted').textContent = countPromoted;
document.getElementById('kpiRegular').textContent = countRegular;
document.getElementById('kpiLibre').textContent = countLibre;
}
recalculateAllRows();
// Debounced auto-save on input
let saveTimeouts = {};
document.querySelectorAll('.grade-input').forEach(input => {
input.addEventListener('input', function() {
if (isClosed) return;
recalculateAllRows();
const milestoneId = this.dataset.milestoneId;
const studentId = this.dataset.studentId;
const key = `${studentId}-${milestoneId}`;
const rawVal = this.value.trim();
updateStatus('saving', 'Escribiendo...');
if (saveTimeouts[key]) clearTimeout(saveTimeouts[key]);
saveTimeouts[key] = setTimeout(async () => {
let score = null;
let isAbsent = false;
if (rawVal === '') {
score = null;
isAbsent = false;
} else {
const num = parseFloat(rawVal);
if (isNaN(num) || num < 1 || num > 10) {
updateStatus('error', 'Nota debe ser entre 1 y 10');
return;
}
score = num;
}
try {
updateStatus('saving', 'Guardando nota...');
const resp = await fetch(`/api/v1/commissions/${commissionId}/gradebook/grades`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
grades: [{
student_id: parseInt(studentId),
reservation_id: parseInt(milestoneId),
score: score,
is_absent: isAbsent
}]
})
});
const data = await resp.json();
if (resp.ok && data.success) {
updateStatus('saved', '✓ Guardado exitosamente');
} else {
updateStatus('error', data.message || 'Error al guardar');
}
} catch (err) {
console.error('Error auto-saving grade:', err);
updateStatus('error', 'Error de red al guardar');
}
}, 500);
});
});
// Client-side search filter
const filterInput = document.getElementById('filterStudentInput');
if (filterInput) {
filterInput.addEventListener('input', function() {
const term = this.value.toLowerCase().trim();
document.querySelectorAll('#gradebookTable tbody tr').forEach(row => {
const name = row.dataset.studentName || '';
const dni = row.dataset.studentDni || '';
if (name.includes(term) || dni.includes(term)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
}
// Close Acta
const btnClose = document.getElementById('btnConfirmCloseActa');
if (btnClose) {
btnClose.addEventListener('click', async function() {
const notes = document.getElementById('closeActaNotes').value;
this.disabled = true;
this.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Sellando...';
try {
const resp = await fetch(`/api/v1/commissions/${commissionId}/gradebook/close`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ notes })
});
const data = await resp.json();
if (resp.ok && data.success) {
window.location.reload();
} else {
alert('Error al cerrar acta: ' + (data.message || 'Error del servidor'));
btnClose.disabled = false;
btnClose.innerHTML = '<i class="bi bi-lock-fill me-1"></i>Confirmar y Sellar Acta';
}
} catch (err) {
alert('Error de conexión: ' + err.message);
btnClose.disabled = false;
btnClose.innerHTML = '<i class="bi bi-lock-fill me-1"></i>Confirmar y Sellar Acta';
}
});
}
// Reopen Acta
const btnReopen = document.getElementById('btnConfirmReopenActa');
if (btnReopen) {
btnReopen.addEventListener('click', async function() {
const reason = document.getElementById('reopenActaReason').value;
if (!reason || reason.trim().length < 5) {
alert('Debe especificar un motivo formal para la reapertura.');
return;
}
this.disabled = true;
this.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Reabriendo...';
try {
const resp = await fetch(`/api/v1/commissions/${commissionId}/gradebook/reopen`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason })
});
const data = await resp.json();
if (resp.ok && data.success) {
window.location.reload();
} else {
alert('Error al reabrir acta: ' + (data.message || 'Error del servidor'));
btnReopen.disabled = false;
btnReopen.innerHTML = '<i class="bi bi-unlock me-1"></i>Reabrir Acta';
}
} catch (err) {
alert('Error de conexión: ' + err.message);
btnReopen.disabled = false;
btnReopen.innerHTML = '<i class="bi bi-unlock me-1"></i>Reabrir Acta';
}
});
}
});
</script>
{% endblock %}
+2 -2
View File
@@ -3,9 +3,9 @@
{% block title %}Listado de Reservas - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="container-fluid px-0">
<!-- Header -->
<div class="row mb-4 align-items-center">
<div class="row mb-3 align-items-center">
<div class="col-md-7">
<h1 class="h3 mb-1">
<i class="bi bi-calendar-check text-primary me-2"></i>Listado de Reservas
+176
View File
@@ -0,0 +1,176 @@
{% extends 'base.html' %}
{% block title %}Mis Calificaciones y Estado Académico — Edu-Space{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb small">
<li class="breadcrumb-item"><a href="/dashboard">Inicio</a></li>
<li class="breadcrumb-item active">Mis Calificaciones</li>
</ol>
</nav>
<!-- Header Banner -->
<div class="card border-0 shadow-sm mb-4 bg-body">
<div class="card-body p-4">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
<div>
<span class="badge bg-success-subtle text-success border border-success-subtle font-monospace px-2 py-1 mb-2">
<i class="bi bi-mortarboard-fill me-1"></i>Portal del Estudiante
</span>
<h3 class="fw-bold mb-1">Mis Calificaciones y Regularidad</h3>
<p class="text-muted small mb-0">
Consulta oficial de notas por hito evaluativo, estado de cursada y actas selladas por Bedelía.
</p>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="window.print()">
<i class="bi bi-printer me-1"></i>Imprimir Certificado de Cursada
</button>
</div>
</div>
</div>
<!-- Subjects Grades Accordion / Cards -->
{% if subjects and subjects|length > 0 %}
<div class="row g-4">
{% for item in subjects %}
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-primary-subtle text-primary border font-monospace">
{{ item.commission.code }}
</span>
<span class="badge bg-secondary-subtle text-secondary border font-monospace">
{{ item.commission.semester }} {{ item.commission.year }}
</span>
{% if item.acta_number %}
<span class="badge bg-danger-subtle text-danger border border-danger-subtle font-monospace">
<i class="bi bi-lock-fill me-1"></i>Acta Cerrada: {{ item.acta_number }}
</span>
{% else %}
<span class="badge bg-info-subtle text-info border border-info-subtle">
<i class="bi bi-hourglass-split me-1"></i>Cursada en Curso
</span>
{% endif %}
</div>
<h5 class="fw-bold mb-0 text-body">{{ item.subject.name }}</h5>
<small class="text-muted">{{ item.subject.code }} · Carrera: {{ item.subject.career or 'Tecnicatura / Licenciatura' }}</small>
</div>
<div class="d-flex align-items-center gap-3">
<div class="text-end">
<span class="small text-muted d-block">Promedio General</span>
<span class="fw-bold fs-5 font-monospace text-primary">
{{ item.average if item.average is not none else '-' }}
</span>
</div>
<div>
{% if item.academic_condition == 'PROMOCIONADO' %}
<span class="badge bg-success text-white py-2 px-3 fs-6">
<i class="bi bi-award-fill me-1"></i>PROMOCIONADO
</span>
{% elif item.academic_condition == 'REGULAR' %}
<span class="badge bg-primary text-white py-2 px-3 fs-6">
<i class="bi bi-check-circle-fill me-1"></i>REGULAR
</span>
{% elif item.academic_condition == 'LIBRE' %}
<span class="badge bg-danger text-white py-2 px-3 fs-6">
<i class="bi bi-x-circle-fill me-1"></i>LIBRE
</span>
{% else %}
<span class="badge bg-secondary text-white py-2 px-3 fs-6">
PENDIENTE
</span>
{% endif %}
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light small">
<tr>
<th class="ps-3">Hito Evaluativo</th>
<th>Fecha</th>
<th>Tipo</th>
<th class="text-center">Calificación</th>
<th class="text-center">Condición</th>
<th class="pe-3">Observaciones de Cátedra</th>
</tr>
</thead>
<tbody>
{% for g in item.grades %}
<tr>
<td class="ps-3 fw-semibold text-body">
{{ g.milestone_title }}
</td>
<td class="font-monospace text-muted small">
{{ g.milestone_date }}
</td>
<td>
<span class="badge bg-light text-dark border">
{{ g.milestone_type }}
</span>
</td>
<td class="text-center">
{% if g.is_absent %}
<span class="badge bg-warning text-dark font-monospace">AUSENTE</span>
{% elif g.score is not none %}
<span class="font-monospace fw-bold fs-6 {% if g.score >= 7 %}text-success{% elif g.score >= 4 %}text-primary{% else %}text-danger{% endif %}">
{{ g.score }}
</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="text-center">
{% if g.is_absent %}
<span class="badge bg-warning-subtle text-warning border">Ausente</span>
{% elif g.score is not none %}
{% if g.score >= 7 %}
<span class="badge bg-success-subtle text-success border">Aprobado / Promoción</span>
{% elif g.score >= 4 %}
<span class="badge bg-primary-subtle text-primary border">Aprobado</span>
{% else %}
<span class="badge bg-danger-subtle text-danger border">Desaprobado</span>
{% endif %}
{% else %}
<span class="badge bg-secondary-subtle text-secondary border">Sin Calificar</span>
{% endif %}
</td>
<td class="pe-3 small text-muted">
{{ g.feedback or '-' }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="card border-0 shadow-sm">
<div class="card-body text-center py-5">
<i class="bi bi-journal-x fs-1 text-muted d-block mb-3"></i>
<h5 class="fw-bold text-muted">Aún no se registran calificaciones cargadas</h5>
<p class="text-muted small">
Tus notas aparecerán aquí a medida que los equipos docentes carguen las evaluaciones de tus materias cursadas.
</p>
<a href="/schedule/today_schedule" class="btn btn-outline-primary btn-sm">
<i class="bi bi-calendar-day me-1"></i>Ver Cartelera de Clases
</a>
</div>
</div>
{% endif %}
</div>
{% endblock %}
+2 -2
View File
@@ -3,9 +3,9 @@
{% block title %}Cartelera de Hoy - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="container-fluid px-0">
<!-- Header -->
<div class="row mb-4 align-items-center">
<div class="row mb-3 align-items-center">
<div class="col-md-7">
<h1 class="h3 mb-1">
<i class="bi bi-calendar-day text-info me-2"></i>Cartelera del Día
+1 -1
View File
@@ -204,7 +204,7 @@
{% set percentage = (reservation.expected_attendees / reservation.classroom.capacity * 100) %}
<div class="progress-bar bg-{% if percentage > 90 %}danger{% elif percentage > 75 %}warning{% else %}success{% endif %}"
role="progressbar"
style="{{ 'width: ' ~ ([percentage|round(1), 100]|min|string) ~ '%;' }}"></div>
{{ ('style="width: ' ~ ([percentage|round(1), 100]|min|string) ~ '%;"') | safe }}></div>
</div>
<small class="text-muted">
{{ reservation.expected_attendees }} / {{ reservation.classroom.capacity }} capacidad