feat: implement Bedelía dashboard panels and Phase 1 Automated Security Audit

This commit is contained in:
2026-09-19 15:17:17 -03:00
parent ada39248d7
commit 71fb20a271
8 changed files with 392 additions and 11 deletions
+8 -1
View File
@@ -68,6 +68,9 @@ router.get('/', async (req, res) => {
? apiData.milestones
: null;
// Bedelía-specific data
const bedeliaData = apiData.bedelia || {};
// Consolidated and role-specific stats
const stats = {
total_classrooms,
@@ -135,7 +138,11 @@ router.get('/', async (req, res) => {
{ start_time: '08:00', end_time: '12:00', classroom: { code: 'Aula 102' }, subject_name: 'Algoritmos y Estructuras de Datos', user: { name: 'Prof. Ana Vega' } },
{ start_time: '08:30', end_time: '12:30', classroom: { code: 'Aula 204' }, subject_name: 'Matemática Discreta', user: { name: 'Prof. Carlos López' } },
{ start_time: '14:00', end_time: '18:00', classroom: { code: 'Lab Redes' }, subject_name: 'Redes y Telecomunicaciones', user: { name: 'Ing. Marcos Ruiz' } }
]
],
unassigned_commissions_week: bedeliaData.unassigned_commissions_week || [],
unassigned_commissions_month: bedeliaData.unassigned_commissions_month || [],
available_classrooms_week: bedeliaData.available_classrooms_week || [],
available_classrooms_month: bedeliaData.available_classrooms_month || []
};
} else if (role === 'docente') {
const myClasses = apiTodaySchedule ? apiTodaySchedule.slice(0, 3) : null;
+265
View File
@@ -633,6 +633,254 @@
</div>
</div>
<!-- ========================= Panel: Comisiones Sin Reserva (Semana/Mes) ========================= -->
<div class="card border-0 shadow-sm mb-4" id="bedelia-unassigned-commissions">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-calendar-x-fill text-danger fs-5"></i>
<div>
<h5 class="mb-0 h6 fw-bold">Comisiones Sin Reserva de Aula</h5>
<small class="text-muted">Comisiones activas sin clase reservada en el período seleccionado</small>
</div>
</div>
<div class="d-flex gap-2 align-items-center flex-wrap">
<div class="btn-group btn-group-sm" role="group" id="commPeriodSwitcher">
<button type="button" class="btn btn-outline-danger active" data-period="week" onclick="switchCommPeriod('week')">
<i class="bi bi-calendar-week me-1"></i>Esta Semana
<span class="badge bg-danger ms-1" id="commWeekBadge">{{ role_data.unassigned_commissions_week | length }}</span>
</button>
<button type="button" class="btn btn-outline-danger" data-period="month" onclick="switchCommPeriod('month')">
<i class="bi bi-calendar-month me-1"></i>Este Mes
<span class="badge bg-danger ms-1" id="commMonthBadge">{{ role_data.unassigned_commissions_month | length }}</span>
</button>
</div>
<a href="/admin/commissions_list" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-list-ul me-1"></i>Ver Todas
</a>
</div>
</div>
<div class="card-body p-0">
<!-- Tabla Semana -->
<div id="commTableWeek">
{% if role_data.unassigned_commissions_week and role_data.unassigned_commissions_week | length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 small">
<thead class="table-light">
<tr>
<th class="ps-3">Código</th>
<th>Materia</th>
<th>Docente</th>
<th>Horario Programado</th>
<th>Turno</th>
<th>Cupo</th>
<th class="text-end pe-3">Acción</th>
</tr>
</thead>
<tbody>
{% for comm in role_data.unassigned_commissions_week %}
<tr>
<td class="ps-3">
<span class="badge bg-danger-subtle text-danger border font-monospace">{{ comm.full_code }}</span>
</td>
<td>
<div class="fw-semibold text-body">{{ comm.subject_name }}</div>
<small class="text-muted font-monospace">{{ comm.subject_code }}</small>
</td>
<td class="small">{{ comm.teacher_name }}</td>
<td>
<span class="badge bg-body-tertiary text-body border"><i class="bi bi-clock me-1"></i>{{ comm.schedule }}</span>
</td>
<td><span class="badge bg-info-subtle text-info-emphasis border">{{ comm.shift }}</span></td>
<td class="font-monospace">{{ comm.current_students }}/{{ comm.max_students }}</td>
<td class="text-end pe-3">
<a href="/schedule/add_reservation?commission_id={{ comm.id }}" class="btn btn-sm btn-outline-success py-0 px-2" title="Asignar aula a esta comisión">
<i class="bi bi-plus-circle me-1"></i>Asignar
</a>
<a href="/admin/commission_detail?id={{ comm.id }}" class="btn btn-sm btn-outline-primary py-0 px-2 ms-1" title="Ver detalle">
<i class="bi bi-eye me-1"></i>Detalle
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-check-circle-fill text-success fs-1 d-block mb-2"></i>
<h6 class="text-success fw-bold">¡Todas las comisiones tienen reserva esta semana!</h6>
<p class="text-muted small mb-0">No hay comisiones activas sin aula reservada en la semana actual.</p>
</div>
{% endif %}
</div>
<!-- Tabla Mes -->
<div id="commTableMonth" style="display: none;">
{% if role_data.unassigned_commissions_month and role_data.unassigned_commissions_month | length > 0 %}
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 small">
<thead class="table-light">
<tr>
<th class="ps-3">Código</th>
<th>Materia</th>
<th>Docente</th>
<th>Horario Programado</th>
<th>Turno</th>
<th>Cupo</th>
<th class="text-end pe-3">Acción</th>
</tr>
</thead>
<tbody>
{% for comm in role_data.unassigned_commissions_month %}
<tr>
<td class="ps-3">
<span class="badge bg-danger-subtle text-danger border font-monospace">{{ comm.full_code }}</span>
</td>
<td>
<div class="fw-semibold text-body">{{ comm.subject_name }}</div>
<small class="text-muted font-monospace">{{ comm.subject_code }}</small>
</td>
<td class="small">{{ comm.teacher_name }}</td>
<td>
<span class="badge bg-body-tertiary text-body border"><i class="bi bi-clock me-1"></i>{{ comm.schedule }}</span>
</td>
<td><span class="badge bg-info-subtle text-info-emphasis border">{{ comm.shift }}</span></td>
<td class="font-monospace">{{ comm.current_students }}/{{ comm.max_students }}</td>
<td class="text-end pe-3">
<a href="/schedule/add_reservation?commission_id={{ comm.id }}" class="btn btn-sm btn-outline-success py-0 px-2" title="Asignar aula a esta comisión">
<i class="bi bi-plus-circle me-1"></i>Asignar
</a>
<a href="/admin/commission_detail?id={{ comm.id }}" class="btn btn-sm btn-outline-primary py-0 px-2 ms-1" title="Ver detalle">
<i class="bi bi-eye me-1"></i>Detalle
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-check-circle-fill text-success fs-1 d-block mb-2"></i>
<h6 class="text-success fw-bold">¡Todas las comisiones tienen reserva este mes!</h6>
<p class="text-muted small mb-0">No hay comisiones activas sin aula reservada en el mes actual.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- ========================= Panel: Aulas Disponibles (Semana/Mes) ========================= -->
<div class="card border-0 shadow-sm mb-4" id="bedelia-available-classrooms">
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-door-open-fill text-success fs-5"></i>
<div>
<h5 class="mb-0 h6 fw-bold">Aulas Disponibles (Sin Reservas)</h5>
<small class="text-muted">Espacios sin ninguna reserva cargada en el período seleccionado</small>
</div>
</div>
<div class="d-flex gap-2 align-items-center flex-wrap">
<div class="btn-group btn-group-sm" role="group" id="classroomPeriodSwitcher">
<button type="button" class="btn btn-outline-success active" data-period="week" onclick="switchClassroomPeriod('week')">
<i class="bi bi-calendar-week me-1"></i>Esta Semana
<span class="badge bg-success ms-1" id="classWeekBadge">{{ role_data.available_classrooms_week | length }}</span>
</button>
<button type="button" class="btn btn-outline-success" data-period="month" onclick="switchClassroomPeriod('month')">
<i class="bi bi-calendar-month me-1"></i>Este Mes
<span class="badge bg-success ms-1" id="classMonthBadge">{{ role_data.available_classrooms_month | length }}</span>
</button>
</div>
<a href="/classrooms/list_classrooms" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-list-ul me-1"></i>Catálogo Completo
</a>
</div>
</div>
<div class="card-body p-3">
<!-- Vista Semana -->
<div id="classroomGridWeek">
{% if role_data.available_classrooms_week and role_data.available_classrooms_week | length > 0 %}
<div class="row g-2">
{% for aula in role_data.available_classrooms_week %}
<div class="col-sm-6 col-md-4 col-lg-3">
<a href="/schedule/add_reservation?classroom_id={{ aula.id }}" class="text-decoration-none text-reset d-block h-100">
<div class="p-3 rounded border h-100 transition-all {% if aula.is_virtual %}bg-info bg-opacity-10 border-info-subtle{% else %}bg-success bg-opacity-10 border-success-subtle{% endif %}">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge {% if aula.is_virtual %}bg-info-subtle text-info{% else %}bg-success-subtle text-success{% endif %} border font-monospace fw-bold">
{{ aula.code }}
</span>
{% if aula.is_virtual %}
<i class="bi bi-camera-video text-info"></i>
{% else %}
<i class="bi bi-door-open text-success"></i>
{% endif %}
</div>
<div class="small text-muted mb-1">
<i class="bi bi-building me-1"></i>{{ aula.building }}{% if aula.floor %} · {{ aula.floor }}{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="small fw-semibold text-body">
<i class="bi bi-people-fill me-1"></i>Cap: {{ aula.capacity }}
</span>
<span class="badge bg-success text-white small"><i class="bi bi-plus-circle me-1"></i>Asignar</span>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-calendar-check-fill text-primary fs-1 d-block mb-2"></i>
<h6 class="text-primary fw-bold">¡Todas las aulas están siendo utilizadas esta semana!</h6>
<p class="text-muted small mb-0">No hay aulas sin reservas activas en la semana actual.</p>
</div>
{% endif %}
</div>
<!-- Vista Mes -->
<div id="classroomGridMonth" style="display: none;">
{% if role_data.available_classrooms_month and role_data.available_classrooms_month | length > 0 %}
<div class="row g-2">
{% for aula in role_data.available_classrooms_month %}
<div class="col-sm-6 col-md-4 col-lg-3">
<a href="/schedule/add_reservation?classroom_id={{ aula.id }}" class="text-decoration-none text-reset d-block h-100">
<div class="p-3 rounded border h-100 transition-all {% if aula.is_virtual %}bg-info bg-opacity-10 border-info-subtle{% else %}bg-success bg-opacity-10 border-success-subtle{% endif %}">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge {% if aula.is_virtual %}bg-info-subtle text-info{% else %}bg-success-subtle text-success{% endif %} border font-monospace fw-bold">
{{ aula.code }}
</span>
{% if aula.is_virtual %}
<i class="bi bi-camera-video text-info"></i>
{% else %}
<i class="bi bi-door-open text-success"></i>
{% endif %}
</div>
<div class="small text-muted mb-1">
<i class="bi bi-building me-1"></i>{{ aula.building }}{% if aula.floor %} · {{ aula.floor }}{% endif %}
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="small fw-semibold text-body">
<i class="bi bi-people-fill me-1"></i>Cap: {{ aula.capacity }}
</span>
<span class="badge bg-success text-white small"><i class="bi bi-plus-circle me-1"></i>Asignar</span>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-calendar-check-fill text-primary fs-1 d-block mb-2"></i>
<h6 class="text-primary fw-bold">¡Todas las aulas están siendo utilizadas este mes!</h6>
<p class="text-muted small mb-0">No hay aulas sin reservas activas en el mes actual.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Bedelía Operational Launchpad -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-body-tertiary border-0 py-3">
@@ -1314,5 +1562,22 @@ document.querySelectorAll('[data-color]').forEach(function(el) {
const color = el.getAttribute('data-color');
if (color) el.style.setProperty('background-color', color, 'important');
});
// Bedelía: Switchers de período para paneles de Comisiones y Aulas
function switchCommPeriod(period) {
document.getElementById('commTableWeek').style.display = period === 'week' ? 'block' : 'none';
document.getElementById('commTableMonth').style.display = period === 'month' ? 'block' : 'none';
document.querySelectorAll('#commPeriodSwitcher button').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-period') === period);
});
}
function switchClassroomPeriod(period) {
document.getElementById('classroomGridWeek').style.display = period === 'week' ? 'block' : 'none';
document.getElementById('classroomGridMonth').style.display = period === 'month' ? 'block' : 'none';
document.querySelectorAll('#classroomPeriodSwitcher button').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-period') === period);
});
}
</script>
{% endblock %}