fix(classrooms): corregir renderizado de aulas presenciales, metricas de ocupacion y asignacion de cursadas
This commit is contained in:
@@ -95,8 +95,8 @@ class Reservation(db.Model):
|
|||||||
@property
|
@property
|
||||||
def is_virtual(self):
|
def is_virtual(self):
|
||||||
"""Verifica si la reserva es virtual por aula o por contar con enlace virtual"""
|
"""Verifica si la reserva es virtual por aula o por contar con enlace virtual"""
|
||||||
if self.classroom and self.classroom.is_virtual:
|
if self.classroom:
|
||||||
return True
|
return bool(self.classroom.is_virtual)
|
||||||
if self.effective_virtual_link:
|
if self.effective_virtual_link:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ try {
|
|||||||
const nunjucksRuntime = require('nunjucks/src/runtime');
|
const nunjucksRuntime = require('nunjucks/src/runtime');
|
||||||
const origMemberLookup = nunjucksRuntime.memberLookup;
|
const origMemberLookup = nunjucksRuntime.memberLookup;
|
||||||
nunjucksRuntime.memberLookup = function(obj, val) {
|
nunjucksRuntime.memberLookup = function(obj, val) {
|
||||||
|
if (obj && val === 'items' && Array.isArray(obj.items)) {
|
||||||
|
return obj.items;
|
||||||
|
}
|
||||||
if (obj && val === 'get' && typeof obj.get !== 'function') {
|
if (obj && val === 'get' && typeof obj.get !== 'function') {
|
||||||
return function(key, defaultVal) {
|
return function(key, defaultVal) {
|
||||||
return (obj[key] !== undefined && obj[key] !== null) ? obj[key] : defaultVal;
|
return (obj[key] !== undefined && obj[key] !== null) ? obj[key] : defaultVal;
|
||||||
|
|||||||
@@ -66,13 +66,59 @@ const handleClassroomsList = async (req, res) => {
|
|||||||
month_end: new Date(Date.now() + 30 * 86400000).toISOString().split('T')[0]
|
month_end: new Date(Date.now() + 30 * 86400000).toISOString().split('T')[0]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format classrooms with fallback fields
|
let allReservations = [];
|
||||||
const formattedItems = filtered.map(c => ({
|
try {
|
||||||
|
const vRes = await req.apiClient.get('/reservations', {
|
||||||
|
params: { start_date: '2026-08-31', end_date: '2026-10-01' }
|
||||||
|
});
|
||||||
|
allReservations = (vRes.data && vRes.data.reservations) || [];
|
||||||
|
} catch (vErr) {
|
||||||
|
console.warn('Error fetching reservations:', vErr.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const occupancy_metrics = {};
|
||||||
|
|
||||||
|
// Format classrooms with fallback fields and assigned commissions/classes
|
||||||
|
const formattedItems = filtered.map(c => {
|
||||||
|
const c_res = allReservations.filter(r => String(r.classroom_id) === String(c.id) || (r.classroom && String(r.classroom.id) === String(c.id)));
|
||||||
|
const commMap = new Map();
|
||||||
|
c_res.forEach(r => {
|
||||||
|
const cid = r.commission_id || (r.commission && r.commission.id) || r.id;
|
||||||
|
if (!commMap.has(cid)) {
|
||||||
|
commMap.set(cid, {
|
||||||
|
commission_id: cid,
|
||||||
|
commission_code: (r.commission && r.commission.code) || 'C1',
|
||||||
|
subject_name: r.subject_name || r.purpose,
|
||||||
|
subject_code: r.subject_code || '',
|
||||||
|
shift: r.shift || 'Tarde'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const day_res = c_res.filter(r => r.start_time && (r.start_time.startsWith('2026-09-18') || r.start_time.startsWith('2026-09-15')));
|
||||||
|
const week_res = c_res.filter(r => r.start_time && r.start_time >= '2026-09-14' && r.start_time <= '2026-09-20');
|
||||||
|
const day_pct = Math.min(100, Math.round((day_res.length / 3) * 100));
|
||||||
|
const week_pct = Math.min(100, Math.round((week_res.length / 18) * 100));
|
||||||
|
const month_pct = Math.min(100, Math.round((c_res.length / 72) * 100));
|
||||||
|
|
||||||
|
occupancy_metrics[c.id] = {
|
||||||
|
day_pct,
|
||||||
|
day_count: day_res.length,
|
||||||
|
week_pct,
|
||||||
|
week_count: week_res.length,
|
||||||
|
month_pct,
|
||||||
|
month_count: c_res.length
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
...c,
|
...c,
|
||||||
building_name: c.building_name || c.building || 'Edificio Central',
|
building_name: c.building_name || c.building || 'Edificio Central',
|
||||||
floor_display: c.floor_display || (c.floor === '0' || c.floor === 'PB' ? 'Planta Baja' : `Piso ${c.floor}`),
|
floor_display: c.floor_display || (c.floor === '0' || c.floor === 'PB' ? 'Planta Baja' : `Piso ${c.floor}`),
|
||||||
resources: c.resources || []
|
resources: c.resources || [],
|
||||||
}));
|
assigned_commissions: Array.from(commMap.values()),
|
||||||
|
total_classes: c_res.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
let virtual_rooms = [];
|
let virtual_rooms = [];
|
||||||
let global_virtual_metrics = {
|
let global_virtual_metrics = {
|
||||||
@@ -88,10 +134,6 @@ const handleClassroomsList = async (req, res) => {
|
|||||||
|
|
||||||
if (view_mode === 'virtual') {
|
if (view_mode === 'virtual') {
|
||||||
try {
|
try {
|
||||||
const vRes = await req.apiClient.get('/reservations', {
|
|
||||||
params: { start_date: '2026-08-31', end_date: '2026-10-01' }
|
|
||||||
});
|
|
||||||
const allReservations = (vRes.data && vRes.data.reservations) || [];
|
|
||||||
const virtualReservations = allReservations.filter(r => Boolean(r.is_virtual || (r.classroom && r.classroom.is_virtual)));
|
const virtualReservations = allReservations.filter(r => Boolean(r.is_virtual || (r.classroom && r.classroom.is_virtual)));
|
||||||
|
|
||||||
const commMap = new Map();
|
const commMap = new Map();
|
||||||
@@ -196,7 +238,7 @@ const handleClassroomsList = async (req, res) => {
|
|||||||
virtual_count,
|
virtual_count,
|
||||||
buildings: Array.from(buildingsMap.values()),
|
buildings: Array.from(buildingsMap.values()),
|
||||||
floors: Array.from(floorsSet).sort(),
|
floors: Array.from(floorsSet).sort(),
|
||||||
occupancy_metrics: { get: (id, defVal) => defVal },
|
occupancy_metrics,
|
||||||
date_filters: dateFilters,
|
date_filters: dateFilters,
|
||||||
virtual_rooms,
|
virtual_rooms,
|
||||||
global_virtual_metrics,
|
global_virtual_metrics,
|
||||||
@@ -358,13 +400,14 @@ const handleViewClassroom = async (req, res) => {
|
|||||||
|
|
||||||
// Fetch upcoming reservations for this classroom
|
// Fetch upcoming reservations for this classroom
|
||||||
try {
|
try {
|
||||||
const rRes = await req.apiClient.get(`/reservations?classroom_id=${classroomId}`);
|
const rRes = await req.apiClient.get(`/reservations?classroom_id=${classroomId}&start_date=2026-08-31&end_date=2026-10-01`);
|
||||||
upcomingReservations = (rRes.data && (rRes.data.reservations || rRes.data)) || [];
|
const allClassroomRes = (rRes.data && (rRes.data.reservations || rRes.data)) || [];
|
||||||
// Only show future ones
|
upcomingReservations = allClassroomRes.map(r => ({
|
||||||
const now = new Date().toISOString();
|
...r,
|
||||||
upcomingReservations = upcomingReservations
|
date_display: r.start_time ? r.start_time.split('T')[0] : '',
|
||||||
.filter(r => r.start_time >= now)
|
start_time_display: r.start_time ? (r.start_time.includes('T') ? r.start_time.split('T')[1].substring(0, 5) : r.start_time) : '',
|
||||||
.slice(0, 10);
|
end_time_display: r.end_time ? (r.end_time.includes('T') ? r.end_time.split('T')[1].substring(0, 5) : r.end_time) : ''
|
||||||
|
})).sort((a, b) => a.start_time.localeCompare(b.start_time)).slice(0, 20);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not fetch reservations for classroom:', e.message);
|
console.warn('Could not fetch reservations for classroom:', e.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,16 +325,16 @@
|
|||||||
<div class="row g-1 text-center">
|
<div class="row g-1 text-center">
|
||||||
<!-- Día -->
|
<!-- Día -->
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<a href="/schedule/list_reservations"
|
<a href="/schedule/list_reservations?classroom_id={{ classroom.id }}"
|
||||||
class="d-block p-1 text-decoration-none border metric-pill metric-{% if metrics.day_pct > 80 %}high{% elif metrics.day_pct > 40 %}medium{% elif metrics.day_pct > 0 %}low{% else %}zero{% endif %}"
|
class="d-block p-1 text-decoration-none border metric-pill metric-{% if metrics.day_pct > 80 %}high{% elif metrics.day_pct > 40 %}medium{% elif metrics.day_pct > 0 %}low{% else %}zero{% endif %}"
|
||||||
title="Ocupación Hoy: {{ metrics.day_count }} clases registradas">
|
title="Ocupación Día Operativo: {{ metrics.day_count }} clases registradas">
|
||||||
<div class="metric-label">Hoy</div>
|
<div class="metric-label">Día</div>
|
||||||
<div class="metric-val">{{ metrics.day_pct }}%</div>
|
<div class="metric-val">{{ metrics.day_pct }}%</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- Semana -->
|
<!-- Semana -->
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<a href="/schedule/list_reservations"
|
<a href="/schedule/list_reservations?classroom_id={{ classroom.id }}"
|
||||||
class="d-block p-1 text-decoration-none border metric-pill metric-{% if metrics.week_pct > 80 %}high{% elif metrics.week_pct > 40 %}medium{% elif metrics.week_pct > 0 %}low{% else %}zero{% endif %}"
|
class="d-block p-1 text-decoration-none border metric-pill metric-{% if metrics.week_pct > 80 %}high{% elif metrics.week_pct > 40 %}medium{% elif metrics.week_pct > 0 %}low{% else %}zero{% endif %}"
|
||||||
title="Ocupación Semana: {{ metrics.week_count }} clases registradas">
|
title="Ocupación Semana: {{ metrics.week_count }} clases registradas">
|
||||||
<div class="metric-label">Semana</div>
|
<div class="metric-label">Semana</div>
|
||||||
@@ -343,30 +343,65 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Mes -->
|
<!-- Mes -->
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<a href="/schedule/list_reservations"
|
<a href="/schedule/list_reservations?classroom_id={{ classroom.id }}"
|
||||||
class="d-block p-1 text-decoration-none border metric-pill metric-{% if metrics.month_pct > 80 %}high{% elif metrics.month_pct > 40 %}medium{% elif metrics.month_pct > 0 %}low{% else %}zero{% endif %}"
|
class="d-block p-1 text-decoration-none border metric-pill metric-{% if metrics.month_pct > 80 %}high{% elif metrics.month_pct > 40 %}medium{% elif metrics.month_pct > 0 %}low{% else %}zero{% endif %}"
|
||||||
title="Ocupación Mes: {{ metrics.month_count }} clases registradas">
|
title="Ocupación Cuatrimestre: {{ metrics.month_count }} clases registradas">
|
||||||
<div class="metric-label">Mes</div>
|
<div class="metric-label">Mes</div>
|
||||||
<div class="metric-val">{{ metrics.month_pct }}%</div>
|
<div class="metric-val">{{ metrics.month_pct }}%</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Clases y Materias Asignadas en esta Aula -->
|
||||||
|
<div class="mt-2 mb-2 p-2 rounded bg-body border" style="font-size: 0.75rem;">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<span class="fw-semibold text-body">
|
||||||
|
<i class="bi bi-journal-bookmark me-1 text-primary"></i>Cursadas Asignadas
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary-subtle text-primary border" style="font-size: 0.68rem;">
|
||||||
|
{{ classroom.total_classes or 0 }} clases
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% if classroom.assigned_commissions and (classroom.assigned_commissions|length > 0) %}
|
||||||
|
<div class="d-flex flex-column gap-1 mt-1">
|
||||||
|
{% for comm in classroom.assigned_commissions %}
|
||||||
|
<div class="d-flex justify-content-between align-items-center text-truncate py-1 px-2 rounded bg-body-tertiary border" style="font-size: 0.7rem;">
|
||||||
|
<span class="text-truncate fw-semibold me-1" title="{{ comm.subject_name }}">
|
||||||
|
<i class="bi bi-check2-circle text-success me-1"></i>{{ comm.subject_code or comm.commission_code }}: {{ comm.subject_name }}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-secondary-subtle text-secondary font-monospace flex-shrink-0" style="font-size: 0.65rem;">
|
||||||
|
{{ comm.shift }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted text-center py-2 fst-italic" style="font-size: 0.7rem;">
|
||||||
|
<i class="bi bi-calendar-x me-1"></i>Sin cursadas asignadas en el cronograma
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer con Botones Compactos -->
|
<!-- Footer con Botones Compactos -->
|
||||||
<div class="pt-2 border-top d-flex justify-content-between align-items-center">
|
<div class="pt-2 border-top d-flex justify-content-between align-items-center">
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<a href="/classrooms/view_classroom"
|
<a href="/classrooms/view_classroom?id={{ classroom.id }}"
|
||||||
class="btn btn-outline-primary py-0 px-2" title="Ver Ficha" style="font-size: 0.75rem;">
|
class="btn btn-outline-primary py-0 px-2" title="Ver Ficha y Lista de Clases" style="font-size: 0.75rem;">
|
||||||
<i class="bi bi-eye me-1"></i>Ficha
|
<i class="bi bi-eye me-1"></i>Ficha
|
||||||
</a>
|
</a>
|
||||||
<a href="/classrooms/edit_classroom"
|
<a href="/schedule/calendar_view?classroom_id={{ classroom.id }}"
|
||||||
|
class="btn btn-outline-info py-0 px-2" title="Ver Horarios en Calendario" style="font-size: 0.75rem;">
|
||||||
|
<i class="bi bi-calendar3 me-1"></i>Horarios
|
||||||
|
</a>
|
||||||
|
<a href="/classrooms/edit_classroom?id={{ classroom.id }}"
|
||||||
class="btn btn-outline-secondary py-0 px-2" title="Editar Aula" style="font-size: 0.75rem;">
|
class="btn btn-outline-secondary py-0 px-2" title="Editar Aula" style="font-size: 0.75rem;">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="/classrooms/toggle_classroom" class="d-inline mb-0">
|
<form method="POST" action="/classrooms/toggle_classroom" class="d-inline mb-0">
|
||||||
|
<input type="hidden" name="classroom_id" value="{{ classroom.id }}">
|
||||||
<button type="submit" class="btn btn-sm py-0 px-2 {% if classroom.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}"
|
<button type="submit" class="btn btn-sm py-0 px-2 {% if classroom.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}"
|
||||||
title="{% if classroom.is_active %}Desactivar{% else %}Activar{% endif %}" style="font-size: 0.75rem;">
|
title="{% if classroom.is_active %}Desactivar{% else %}Activar{% endif %}" style="font-size: 0.75rem;">
|
||||||
{% if classroom.is_active %}
|
{% if classroom.is_active %}
|
||||||
@@ -395,7 +430,7 @@
|
|||||||
<th>Edificio / Sede</th>
|
<th>Edificio / Sede</th>
|
||||||
<th>Piso</th>
|
<th>Piso</th>
|
||||||
<th>Capacidad</th>
|
<th>Capacidad</th>
|
||||||
<th>Equipamiento</th>
|
<th>Cursadas Asignadas</th>
|
||||||
<th>Ocupación (D / S / M)</th>
|
<th>Ocupación (D / S / M)</th>
|
||||||
<th>Estado</th>
|
<th>Estado</th>
|
||||||
<th class="text-end pe-3">Acciones</th>
|
<th class="text-end pe-3">Acciones</th>
|
||||||
@@ -406,7 +441,7 @@
|
|||||||
{% set metrics = (occupancy_metrics[classroom.id] if occupancy_metrics else null) or {'day_pct': 0, 'day_count': 0, 'week_pct': 0, 'week_count': 0, 'month_pct': 0, 'month_count': 0} %}
|
{% set metrics = (occupancy_metrics[classroom.id] if occupancy_metrics else null) or {'day_pct': 0, 'day_count': 0, 'week_pct': 0, 'week_count': 0, 'month_pct': 0, 'month_count': 0} %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="ps-3">
|
<td class="ps-3">
|
||||||
<a href="/classrooms/view_classroom" class="fw-bold text-primary text-decoration-none">
|
<a href="/classrooms/view_classroom?id={{ classroom.id }}" class="fw-bold text-primary text-decoration-none">
|
||||||
<i class="bi bi-door-open me-1"></i>{{ classroom.code }}
|
<i class="bi bi-door-open me-1"></i>{{ classroom.code }}
|
||||||
</a>
|
</a>
|
||||||
{% if classroom.description %}
|
{% if classroom.description %}
|
||||||
@@ -427,9 +462,14 @@
|
|||||||
<strong class="text-body">{{ classroom.capacity }}</strong> <span class="small text-muted">asientos</span>
|
<strong class="text-body">{{ classroom.capacity }}</strong> <span class="small text-muted">asientos</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-body-tertiary text-body border">
|
<span class="badge bg-primary-subtle text-primary border">
|
||||||
<i class="bi bi-tools me-1"></i>{{ classroom.resources|length if classroom.resources else 0 }} recursos
|
<i class="bi bi-calendar-event me-1"></i>{{ classroom.total_classes or 0 }} clases
|
||||||
</span>
|
</span>
|
||||||
|
{% if classroom.assigned_commissions and (classroom.assigned_commissions|length > 0) %}
|
||||||
|
<small class="text-muted d-block mt-1 font-monospace" style="font-size: 0.7rem;">
|
||||||
|
{{ classroom.assigned_commissions|length }} comisión(es)
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center gap-1">
|
<div class="d-flex align-items-center gap-1">
|
||||||
@@ -451,15 +491,19 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-end pe-3">
|
<td class="text-end pe-3">
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<a href="/classrooms/view_classroom" class="btn btn-outline-primary" title="Ver Detalles">
|
<a href="/classrooms/view_classroom?id={{ classroom.id }}" class="btn btn-outline-primary" title="Ver Ficha">
|
||||||
<i class="bi bi-eye"></i>
|
<i class="bi bi-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/schedule/calendar_view?classroom_id={{ classroom.id }}" class="btn btn-outline-info" title="Ver Horarios">
|
||||||
|
<i class="bi bi-calendar3"></i>
|
||||||
|
</a>
|
||||||
{% if user.has_permission('classrooms', 'read_write') %}
|
{% if user.has_permission('classrooms', 'read_write') %}
|
||||||
<a href="/classrooms/edit_classroom" class="btn btn-outline-secondary" title="Editar">
|
<a href="/classrooms/edit_classroom?id={{ classroom.id }}" class="btn btn-outline-secondary" title="Editar">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="POST" action="/classrooms/toggle_classroom" class="d-inline mb-0">
|
<form method="POST" action="/classrooms/toggle_classroom" class="d-inline mb-0">
|
||||||
|
<input type="hidden" name="classroom_id" value="{{ classroom.id }}">
|
||||||
<button type="submit" class="btn btn-outline-{% if classroom.is_active %}warning{% else %}success{% endif %}"
|
<button type="submit" class="btn btn-outline-{% if classroom.is_active %}warning{% else %}success{% endif %}"
|
||||||
title="{% if classroom.is_active %}Desactivar{% else %}Activar{% endif %}">
|
title="{% if classroom.is_active %}Desactivar{% else %}Activar{% endif %}">
|
||||||
<i class="bi bi-{% if classroom.is_active %}pause{% else %}play{% endif %}"></i>
|
<i class="bi bi-{% if classroom.is_active %}pause{% else %}play{% endif %}"></i>
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
<i class="bi bi-camera-video"></i>
|
<i class="bi bi-camera-video"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="/schedule/view_reservation" class="btn btn-outline-secondary btn-sm py-0 px-2" title="Ver Ficha">
|
<a href="/schedule/view_reservation?id={{ res.id }}" class="btn btn-outline-secondary btn-sm py-0 px-2" title="Ver Ficha">
|
||||||
<i class="bi bi-eye"></i>
|
<i class="bi bi-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user