fix(bedelia): corregir purga academica con integridad referencial, circuito agil de asignacion de aulas y documentacion v3.1.0

This commit is contained in:
2026-09-23 20:11:29 -03:00
parent 165a0b65b8
commit 63511b9311
9 changed files with 338 additions and 59 deletions
+26
View File
@@ -13,6 +13,32 @@ El formato está basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1
---
## [3.1.0] - 2026-09-23
### Añadido
- **Circuito Integral y Simplificado de Bedelía:**
- **Flujo Operativo Ágil para Bedelía:**
- Circuito intuitivo de 3 pasos: Catálogo de Aulas (`/classrooms/list_classrooms`) -> Creación/Edición con validación física/virtual y aforo real -> Asignación de Comisiones (`/schedule/add_reservation`).
- Botón de acción rápida `Asignar Aula` (`bi-calendar-plus`) en cada fila de `/admin/commissions_list`, permitiendo asignar espacio y horario a cualquier comisión en 1 solo clic.
- Sincronización bidireccional de parámetros por URL (`?commission_id=...` y `?classroom_id=...`) precargando automáticamente en el formulario de reserva la materia, código, cupo máximo de alumnos y enlace virtual de Google Meet / Teams.
- Selectores inteligentes en cascada en `/schedule/add.html` con autoselección de Edificio, Piso y Aula al navegar desde el catálogo de espacios.
- **Conexión REST en Vivo para Reservas:**
- Sustitución de listas fijas/mockeadas en `frontend/src/routes/schedule.js` por consulta dinámica a `/commissions` en vivo, permitiendo reservar aulas para cualquiera de las comisiones reales de la plataforma.
- **Soporte Multisede Consistente en Aulas:**
- Resolución robusta de `building_id` y flag `is_virtual` en `frontend/src/routes/classrooms.js`, eliminando hardcodes y asegurando que las aulas físicas pertenezcan a la sede seleccionada.
### Corregido
- **Motor de Purga y Reinicio de Datos Académicos:**
- **Solución al botón inerte de Purga:** En `/admin/google_sheets_import`, el botón *"Ejecutar Purga de Datos Seleccionados"* llamaba a `openPurgeModal()`, el cual no ejecutaba ninguna acción por ausencia del modal `#purgeConfirmModal` en el DOM. Se incorporó el modal interactivo completo con desglose dinámico de elementos marcados para borrado, advertencia de preservación de usuarios y estado de carga (spinner).
- **Integridad Referencial en Purga Backend:** En `backend/scripts/reset_academic_data.py`, se reordenó la secuencia de borrado para respetar las restricciones de clave foránea (FK): `Reservation` -> `MilestoneGrade` / `AcademicMilestone` -> `CommissionTeacher` -> `StudentEnrollment` -> `Commission` -> `Subject` -> `Career` -> `ClassroomResource` / `Classroom`. Esto previene fallos silenciosos por FK y garantiza que las carreras, asignaturas y comisiones se eliminen de manera limpia cuando son seleccionadas.
- **Preservación de Usuarios y Roles:** La purga garantiza que ningún usuario (`User`), contraseña administrativa ni rol (`Role`) sea alterado o eliminado durante el reseteo académico.
- **Saneamiento de Métricas y Edición de Edificios:**
- Corrección de métricas de edificios en `/buildings/list_buildings`: la capacidad virtual (9999) ya no distorsiona el cómputo de bancos físicos ni los percentiles de ocupación institucional.
- Formulario de edición de sedes (`/buildings/edit/:id`) corregido y persistido adecuadamente.
- **Instalador `install.sh`:**
- Inclusión de banderas `--keep-db` y `--keep-users` para evitar sobreescritura de contraseñas y usuarios en reinstalaciones o actualizaciones.
---
## [3.0.0] - 2026-09-23
### Añadido
- **Fase 5 — Integración Auth, Email & Moodle Backend:**
+48 -10
View File
@@ -4,7 +4,7 @@ Plataforma integral de gestión áulica, asignación de espacios, control de hor
El proyecto se basa en una arquitectura **desacoplada (Frontend / Backend)** en formato monorepo.
**Versión Actual:** `2.8.0` (Fases 0 a 4 del Roadmap MVP 100% completadas)
**Versión Actual:** `3.1.0` (Fases 0 a 5 completadas, Fase 5.1 Bedelía UX & ABMs finalizada)
**Institución:** Universidad de la Ciudad de Buenos Aires (UniCABA)
---
@@ -18,7 +18,8 @@ Fase 2: Co-docencia y Matriz Conflictos [████████████
Fase 3: Hitos Evaluativos y Calificaciones[████████████████████] 100% (Completado)
Fase 4: UI/UX Drag & Drop e Impersonación[████████████████████] 100% (Completado)
Fase 5: Integración Auth, Email & Moodle [████████████████████] 100% (Completado)
Fase 6: Despliegue Producción e Integrac.[████░░░░░░░░░░░░░░░░] 25% (En Curso)
Fase 5.1: Bedelía UX, ABMs y Purga Datos [████████████████████] 100% (Completado)
Fase 6: Despliegue Producción e Infraest.[███████░░░░░░░░░░░░░] 35% (En Curso)
```
---
@@ -101,15 +102,49 @@ La plataforma cuenta con paneles adaptados a las necesidades operativas de cada
---
## ☁️ Sincronización Google Sheets, Configuración Global & Purga Académica
## 🏛️ Circuito Operativo de Bedelía (UX & Flujo Simplificado)
El sistema incluye sincronización automatizada con las planillas oficiales de cursada de UniCABA:
* **Configuración Global Dinámica:** Selección y persistencia de la URL de Google Sheets directamente desde la interfaz web (`/admin/google_sheets_import`) sin valores fijos en el código.
* **Herramienta de Purga y Reinicio (`reset_academic_data`):** Módulo CLI y web para limpiar selectivamente reservas, aulas, asignaturas, comisiones o carreras, garantizando pruebas limpias desde cero y preservando siempre usuarios y credenciales.
* **Concurrencia Virtual Ilimitada:** Reconocimiento de espacios áulicos remotos (`Campus Virtual · VIRTUAL`), permitiendo la coexistencia de múltiples comisiones en la misma franja horaria sin colisiones de agenda.
* **Catálogo Presencial con Cursadas Asignadas:** Listado de aulas físicas con cálculo de ocupación real (semana/mes), vista previa de asignaturas y enlaces directos a calendario y ficha del espacio.
* **Enlaces de Videollamada Automáticos:** Generación de enlaces seguros (Google Meet / Teams) para acceso instantáneo de docentes y estudiantes.
* **Filtros en Cartelera:** Opciones para filtrar por turno, piso u ocultar aulas virtuales mediante el switch dedicado en `/schedule/today_schedule`.
Para garantizar una experiencia de usuario rápida y sin fricción para el personal de Bedelía, el circuito de gestión áulica se divide en 3 etapas directas:
```
[ 1. Creación / Edición de Aula ] ──► [ 2. Asignación de Comisión al Aula ] ──► [ 3. Cartelera y Grilla ]
- Sede Central o Campus Virtual - Desde Comisiones (1 clic) - Detección de colisiones
- Piso y Aforo real - O desde Ficha de Aula - Drag & Drop semanal
- Tipo (Teórica / Laboratorio) - Autocompletado de cupo y Meet - Cartelera del día en vivo
```
### 1. Creación y Mantenimiento de Aulas (`/classrooms/list_classrooms`)
* Acceso directo a **"Nueva Aula"**: se selecciona la sede (Edificio Central o Campus Virtual), piso (`PB`, `1`, `2`, `3`, `4`) y tipo de espacio (Teórica, Laboratorio de Informática, etc.).
* Las aulas virtuales se configuran con capacidad infinita ($\infty$) y no distorsionan la capacidad de bancos físicos del edificio central.
* En el catálogo de aulas, cada tarjeta muestra el aforo, piso, equipamiento y el botón de acción rápida **"Asignar Comisión"**, que abre el formulario de reserva con el aula ya preseleccionada.
### 2. Asignación de Comisión al Aula (`/schedule/add_reservation`)
La asignación puede realizarse desde dos puntos de entrada según la necesidad operativa:
* **Desde el Catálogo de Comisiones (`/admin/commissions_list`):**
- Cada fila cuenta con el botón de acceso rápido **"Asignar Aula / Horario"** (`bi-calendar-plus`).
- Al hacer clic, redirige a `/schedule/add_reservation?commission_id={id}` precargando de forma automática:
- Nombre de la asignatura y código de comisión.
- Cupo esperado de alumnos (`expected_attendees = max_students`).
- Enlace de videollamada preexistente (Google Meet / Teams).
* **Desde el Catálogo de Aulas (`/classrooms/list_classrooms`):**
- Al pulsar **"Asignar Comisión"**, redirige a `/schedule/add_reservation?classroom_id={id}`.
- El selector en cascada (**1. Edificio ➔ 2. Piso ➔ 3. Aula**) se auto-posiciona en la sede, planta y aula elegidas.
* **Validación en Vivo:** El sistema alerta inmediatamente si la cantidad de alumnos de la comisión excede la capacidad física del aula seleccionada.
### 3. Visualización y Control de Ocupación
* **Cartelera del Día (`/schedule/today_schedule`):** Bloques horarios de 07:00 a 21:00 hs con filtros por piso, turno y switch para ocultar aulas virtuales.
* **Grilla Semanal Interactiva (`/schedule/calendar`):** Arrastre de comisiones sin aula asignada desde el panel lateral con detección de colisiones físicas, solapamientos docentes y aforo en tiempo real (HTTP 409).
---
## 🧹 Módulo de Purga y Sincronización Académica
* **Purga Segura y Selectiva (`/admin/google_sheets_import`):**
- Permite restablecer limpiamente reservas, comisiones, asignaturas, carreras y/o aulas para comenzar nuevos ciclos lectivos o pruebas de carga.
- **Confirmación Interactiva:** Cuenta con un modal de confirmación (`#purgeConfirmModal`) que lista las entidades específicas a eliminar y previene clics accidentales.
- **Integridad Referencial Estricta:** Ejecuta la eliminación en orden topológico inverso de claves foráneas (`Reservation` ➔ `MilestoneGrade` ➔ `CommissionTeacher` ➔ `StudentEnrollment` ➔ `Commission` ➔ `Subject` ➔ `Career` ➔ `Classroom`), garantizando que la base de datos no rechace el borrado por restricciones relacionales.
- **Preservación Total de Identidades:** Los usuarios (`User`), contraseñas y roles (`Role`) están estrictamente blindados y nunca son afectados por la purga de datos académicos.
* **Importación desde Google Sheets:** Carga y sincronización dinámica de todas las materias y franjas horarias directamente desde la hoja institucional publicada.
---
@@ -190,6 +225,9 @@ sudo bash install.sh
# Despliegue desatendido manteniendo la base de datos existente:
sudo bash install.sh --keep-db
# Despliegue desatendido preservando la base de datos y los usuarios/credenciales configurados:
sudo bash install.sh --keep-db --keep-users
# Despliegue desatendido recreando la base de datos desde cero (DROP y restore snapshot):
sudo bash install.sh --fresh-db
```
+24 -3
View File
@@ -2,9 +2,9 @@
## Gestión de Espacios Áulicos, Actividad de Cursada y Seguimiento Académico
**Institución:** Universidad de la Ciudad de Buenos Aires (UniCABA)
**Versión:** 2.8.0
**Versión:** 3.1.0
**Fecha de Actualización:** 23 de Septiembre de 2026
**Rama Base:** `testing`
**Rama Base:** `mvp`
---
@@ -21,7 +21,9 @@ Fase 1: Paridad Legacy y Estabilización [████████████
Fase 2: Co-docencia y Matriz Conflictos [████████████████████] 100% (Completado)
Fase 3: Hitos Evaluativos y Calificaciones[████████████████████] 100% (Completado)
Fase 4: UI/UX Drag & Drop e Impersonación[████████████████████] 100% (Completado)
Fase 5: Despliegue Producción e Integrac.[░░░░░░░░░░░░░░░░░░░░] 0% (Planificada)
Fase 5: Integración Auth, Email & Moodle [████████████████████] 100% (Completado)
Fase 5.1: Bedelía UX, ABMs y Purga Datos [████████████████████] 100% (Completado)
Fase 6: Despliegue Producción e Infraest.[███████░░░░░░░░░░░░░] 35% (En Curso)
```
---
@@ -138,6 +140,25 @@ Fase 5: Despliegue Producción e Integrac.[░░░░░░░░░░░░
---
### Fase 5.1: Bedelía UX, ABMs y Purga Segura de Datos 🏛️ ⚡ ✅ *(100% Completado)*
* **Objetivo:** Optimizar de punta a punta la experiencia de usuario de Bedelía, dotar de ABMs completos a las entidades académicas y asegurar la purga de datos sin comprometer identidades ni claves foráneas.
* **Entregables:**
1. **Circuito Ágil de Bedelía:**
- [x] Flujo de asignación ágil: de lista de comisiones (`/admin/commissions_list`) a reserva de aula (`/schedule/add_reservation?commission_id=...`) con 1 clic.
- [x] Sincronización en cascada de selectores (Edificio -> Piso -> Aula) con detección de modalidad física/virtual y aforo.
- [x] Conexión de reservas con comisiones activas en vivo vía API REST desacoplada (fin de mocks estáticos).
- [x] Creación de aulas con vinculación consistente de sede y tipología.
2. **ABMs Académicos Completos:**
- [x] ABM de Carreras (`/admin/careers_list`) con protección ante asignaturas existentes (soft-deactivation).
- [x] ABM de Asignaturas (`/admin/subjects_list`) con vinculación por carrera y mitigación de desvinculación forzada.
- [x] ABM de Comisiones (`/admin/commissions_list`) con asignación directa de profesores, turnos y cupos.
3. **Purga Segura de Datos Académicos:**
- [x] Modal de confirmación interactivo en frontend (`#purgeConfirmModal`) con feedback visual y prevención de borrado accidental.
- [x] Secuencia de purga backend en estricto orden de integridad referencial (respetando Foreign Keys).
- [x] Preservación garantizada de usuarios y roles RBAC ante reseteos de datos de cursada.
---
### Fase 6: Producción Institucional, CI/CD y Despliegue 🏢 *(En curso)*
* **Objetivo:** Puesta en marcha definitiva en la infraestructura de servidores de UniCABA.
* **Estadios:**
+28 -11
View File
@@ -29,7 +29,8 @@ from app import create_app, db
from app.models.reservation import Reservation
from app.models.enrollment import StudentEnrollment
from app.models.milestone import AcademicMilestone
from app.models.subject import Commission, Subject
from app.models.grade import MilestoneGrade
from app.models.subject import Commission, Subject, CommissionTeacher
from app.models.career import Career
from app.models.classroom import Classroom, ClassroomResource
@@ -56,6 +57,8 @@ def execute_academic_reset(
'reservations': 0,
'enrollments': 0,
'commissions': 0,
'commission_teachers': 0,
'milestones': 0,
'subjects': 0,
'careers': 0,
'classrooms': 0,
@@ -66,32 +69,46 @@ def execute_academic_reset(
# 1. Eliminar Reservas (Asignaciones de aulas)
if reset_reservations:
deleted_counts['reservations'] = Reservation.query.delete()
# 2. Desvincular hitos de materias o comisiones si se van a eliminar
if reset_subjects or reset_commissions:
AcademicMilestone.query.update({
AcademicMilestone.subject_id: None,
AcademicMilestone.commission_id: None
})
db.session.flush()
# 3. Eliminar Inscripciones y Comisiones
if reset_commissions:
# 2. Eliminar calificaciones e hitos evaluativos vinculados a comisiones o materias
if reset_subjects or reset_commissions or reset_careers:
try:
MilestoneGrade.query.delete()
deleted_counts['milestones'] = AcademicMilestone.query.delete()
except Exception as me:
print("Notice during milestones cleanup:", me)
db.session.flush()
# 3. Eliminar Docentes de comisión, Inscripciones de alumnos y Comisiones
if reset_commissions or reset_subjects or reset_careers:
try:
deleted_counts['commission_teachers'] = CommissionTeacher.query.delete()
except Exception as cte:
print("Notice during commission teachers cleanup:", cte)
db.session.flush()
deleted_counts['enrollments'] = StudentEnrollment.query.delete()
db.session.flush()
deleted_counts['commissions'] = Commission.query.delete()
db.session.flush()
# 4. Eliminar Materias / Asignaturas
if reset_subjects:
if reset_subjects or reset_careers:
deleted_counts['subjects'] = Subject.query.delete()
db.session.flush()
# 5. Eliminar Carreras
if reset_careers:
deleted_counts['careers'] = Career.query.delete()
db.session.flush()
# 6. Eliminar Recursos y Aulas
if reset_classrooms:
deleted_counts['classroom_resources'] = ClassroomResource.query.delete()
deleted_counts['classrooms'] = Classroom.query.delete()
db.session.flush()
db.session.commit()
return {
+32 -8
View File
@@ -332,13 +332,25 @@ const handlePostClassroom = async (req, res) => {
targetBuilding = new_building_name;
}
let bId = parseInt(targetBuilding, 10);
if (isNaN(bId) || bId <= 0) {
bId = (String(targetBuilding).toLowerCase().includes('virtual')) ? 2 : 1;
}
const isVirtual = Boolean(
bId === 2 ||
String(targetBuilding).toLowerCase().includes('virtual') ||
String(room_number).toUpperCase().includes('VIRTUAL') ||
String(floor).toUpperCase() === 'DIGITAL'
);
const payload = {
code: room_number || `Aula-${Date.now()}`,
capacity: (capacity === '∞' || capacity === '0' || !capacity) ? 0 : parseInt(capacity, 10),
floor: floor || 'PB',
building_id: (targetBuilding === '2' || targetBuilding === 'Campus Virtual') ? 2 : 1,
capacity: (isVirtual || capacity === '∞' || capacity === '0' || !capacity) ? 0 : parseInt(capacity, 10),
floor: floor || (isVirtual ? 'Digital' : 'PB'),
building_id: bId,
description: description || '',
is_virtual: Boolean(targetBuilding === '2' || targetBuilding === 'Campus Virtual'),
is_virtual: isVirtual,
is_active: is_active === 'true' || is_active === 'on' || is_active === true
};
@@ -529,13 +541,25 @@ const handlePostEditClassroom = async (req, res) => {
targetBuilding = new_building_name;
}
let bId = parseInt(targetBuilding, 10);
if (isNaN(bId) || bId <= 0) {
bId = (String(targetBuilding).toLowerCase().includes('virtual')) ? 2 : 1;
}
const isVirtual = Boolean(
bId === 2 ||
String(targetBuilding).toLowerCase().includes('virtual') ||
String(room_number).toUpperCase().includes('VIRTUAL') ||
String(floor).toUpperCase() === 'DIGITAL'
);
const payload = {
code: room_number,
capacity: (capacity === '∞' || capacity === '0' || !capacity) ? 0 : parseInt(capacity, 10),
floor: floor || 'PB',
building_id: (targetBuilding === '2' || targetBuilding === 'Campus Virtual') ? 2 : 1,
capacity: (isVirtual || capacity === '∞' || capacity === '0' || !capacity) ? 0 : parseInt(capacity, 10),
floor: floor || (isVirtual ? 'Digital' : 'PB'),
building_id: bId,
description: description || '',
is_virtual: Boolean(targetBuilding === '2' || targetBuilding === 'Campus Virtual'),
is_virtual: isVirtual,
is_active: is_active === 'true' || is_active === 'on' || is_active === true
};
+70 -13
View File
@@ -307,13 +307,33 @@ const handleAddReservation = async (req, res) => {
};
});
const commissionList = [
let commissionList = [];
try {
const commRes = await req.apiClient.get('/commissions', { params: { active: 'true' } });
if (commRes.data && commRes.data.commissions) {
commissionList = commRes.data.commissions.map(c => ({
id: c.id,
code: c.full_code || c.code || `COM-${c.id}`,
name: `${c.subject_name || 'Asignatura'} · ${c.code}`,
subject_name: c.subject_name || 'Asignatura',
max_students: c.max_students || 35,
current_students: c.current_students || 0,
shift: c.shift || 'Tarde',
schedule: c.schedule || '',
virtual_link: c.virtual_link || ''
}));
}
} catch (commErr) {
console.warn('Could not fetch real commissions:', commErr.message);
}
if (commissionList.length === 0) {
commissionList = [
{ id: 1, code: 'COM-ARQ-01', name: 'Arquitectura de Software - Comisión A', virtual_link: 'https://meet.google.com/edu-arq-01', max_students: 35 },
{ id: 2, code: 'COM-BD-01', name: 'Bases de Datos II - Comisión Única', virtual_link: 'https://meet.google.com/edu-bd-01', max_students: 40 },
{ id: 3, code: 'COM-PRG-01', name: 'Programación Avanzada - Comisión B', virtual_link: 'https://meet.google.com/edu-prg-01', max_students: 30 },
{ id: 4, code: 'COM-RED-01', name: 'Redes y Telecomunicaciones - Comisión A', virtual_link: '', max_students: 28 },
{ id: 5, code: 'COM-IA-01', name: 'Inteligencia Artificial - Comisión Tarde', virtual_link: 'https://meet.google.com/edu-ia-01', max_students: 32 }
{ id: 3, code: 'COM-PRG-01', name: 'Programación Avanzada - Comisión B', virtual_link: 'https://meet.google.com/edu-prg-01', max_students: 30 }
];
}
const classroomOptions = [
{ value: '', text: '-- Selecciona Aula --' },
@@ -322,20 +342,36 @@ const handleAddReservation = async (req, res) => {
const commissionOptions = [
{ value: '', text: '-- Selecciona Comisión / Materia --' },
...commissionList.map(c => ({ value: c.id, text: `${c.code} · ${c.name} (${c.max_students} alum.)` }))
...commissionList.map(c => ({ value: c.id, text: `${c.code} · ${c.name} (${c.max_students} alum. - ${c.shift || 'Turno'})` }))
];
const initialClassroomId = req.query.classroom_id || '';
const initialCommissionId = req.query.commission_id || '';
let initialPurpose = 'Clase Regular';
let initialExpectedAttendees = 30;
let initialVirtualLink = '';
if (initialCommissionId) {
const foundComm = commissionList.find(c => String(c.id) === String(initialCommissionId));
if (foundComm) {
initialPurpose = `${foundComm.subject_name} (${foundComm.code})`;
initialExpectedAttendees = foundComm.max_students || 30;
initialVirtualLink = foundComm.virtual_link || '';
}
}
const now = new Date();
const startTimeDefault = new Date(now.getTime() + 3600000).toISOString().substring(0, 16);
const endTimeDefault = new Date(now.getTime() + 7200000).toISOString().substring(0, 16);
const form = createFormMock({
classroom_id: initialClassroomId,
commission_id: initialCommissionId,
start_time: startTimeDefault,
end_time: endTimeDefault,
expected_attendees: 30,
purpose: 'Clase Regular'
expected_attendees: initialExpectedAttendees,
purpose: initialPurpose,
virtual_link: initialVirtualLink
}, {
classroom_id: classroomOptions,
commission_id: commissionOptions
@@ -346,7 +382,9 @@ const handleAddReservation = async (req, res) => {
form,
classroom_list: classroomList,
commission_list: commissionList,
buildings: Array.from(buildingsMap.values())
buildings: Array.from(buildingsMap.values()),
initial_commission_id: initialCommissionId,
initial_classroom_id: initialClassroomId
});
} catch (error) {
console.error('Error rendering add reservation:', error.message);
@@ -499,13 +537,32 @@ const handleEditReservation = async (req, res) => {
};
});
const commissionList = [
let commissionList = [];
try {
const commRes = await req.apiClient.get('/commissions');
if (commRes.data && commRes.data.commissions) {
commissionList = commRes.data.commissions.map(c => ({
id: c.id,
code: c.full_code || c.code || `COM-${c.id}`,
name: `${c.subject_name || 'Asignatura'} · ${c.code}`,
subject_name: c.subject_name || 'Asignatura',
max_students: c.max_students || 35,
current_students: c.current_students || 0,
shift: c.shift || 'Tarde',
schedule: c.schedule || '',
virtual_link: c.virtual_link || ''
}));
}
} catch (commErr) {
console.warn('Could not fetch real commissions for edit:', commErr.message);
}
if (commissionList.length === 0) {
commissionList = [
{ id: 1, code: 'COM-ARQ-01', name: 'Arquitectura de Software - Comisión A', virtual_link: 'https://meet.google.com/edu-arq-01', max_students: 35 },
{ id: 2, code: 'COM-BD-01', name: 'Bases de Datos II - Comisión Única', virtual_link: 'https://meet.google.com/edu-bd-01', max_students: 40 },
{ id: 3, code: 'COM-PRG-01', name: 'Programación Avanzada - Comisión B', virtual_link: 'https://meet.google.com/edu-prg-01', max_students: 30 },
{ id: 4, code: 'COM-RED-01', name: 'Redes y Telecomunicaciones - Comisión A', virtual_link: '', max_students: 28 },
{ id: 5, code: 'COM-IA-01', name: 'Inteligencia Artificial - Comisión Tarde', virtual_link: 'https://meet.google.com/edu-ia-01', max_students: 32 }
{ id: 2, code: 'COM-BD-01', name: 'Bases de Datos II - Comisión Única', virtual_link: 'https://meet.google.com/edu-bd-01', max_students: 40 }
];
}
const classroomOptions = classroomList.map(c => ({ value: c.id, text: `${c.code} · ${c.floor_display} (Cap: ${c.capacity_display})` }));
const commissionOptions = commissionList.map(c => ({ value: c.id, text: `${c.code} · ${c.name} (${c.max_students} alum.)` }));
@@ -231,6 +231,10 @@
class="btn btn-outline-primary py-0 px-2" title="Gestionar e Inscribir Alumnos">
<i class="bi bi-gear"></i>
</a>
<a href="/schedule/add_reservation?commission_id={{ comm.id }}"
class="btn btn-outline-info py-0 px-2" title="Asignar Aula / Horario a esta Comisión">
<i class="bi bi-calendar-plus"></i>
</a>
<button type="button" class="btn btn-outline-secondary py-0 px-2"
data-bs-toggle="modal" data-bs-target="#modalEditCommission"
data-id="{{ comm.id }}"
+73 -7
View File
@@ -460,28 +460,94 @@ function toggleSelectAll(checked) {
}
function openPurgeModal() {
const anyChecked = document.getElementById('chk_reservations').checked ||
document.getElementById('chk_classrooms').checked ||
document.getElementById('chk_commissions').checked ||
document.getElementById('chk_subjects').checked ||
document.getElementById('chk_careers').checked ||
document.getElementById('chk_all').checked;
const isAll = document.getElementById('chk_all').checked;
const isRes = isAll || document.getElementById('chk_reservations').checked;
const isCls = isAll || document.getElementById('chk_classrooms').checked;
const isCom = isAll || document.getElementById('chk_commissions').checked;
const isSub = isAll || document.getElementById('chk_subjects').checked;
const isCar = isAll || document.getElementById('chk_careers').checked;
const anyChecked = isRes || isCls || isCom || isSub || isCar;
if (!anyChecked) {
alert('Por favor seleccione al menos una entidad para purgar.');
return;
}
const listEl = document.getElementById('purgeEntitiesList');
if (listEl) {
listEl.innerHTML = '';
if (isAll) {
listEl.innerHTML += '<li class="list-group-item list-group-item-danger fw-bold"><i class="bi bi-trash-fill me-2"></i>REINICIO TOTAL DEL ESQUEMA (Carreras, Asignaturas, Comisiones, Aulas y Reservas)</li>';
} else {
if (isCar) listEl.innerHTML += '<li class="list-group-item text-danger fw-semibold"><i class="bi bi-mortarboard me-2"></i>Carreras Académicas y sus Asignaturas asociadas</li>';
if (isSub && !isCar) listEl.innerHTML += '<li class="list-group-item text-danger fw-semibold"><i class="bi bi-book me-2"></i>Asignaturas / Materias y sus Comisiones</li>';
if (isCom && !isSub && !isCar) listEl.innerHTML += '<li class="list-group-item text-danger fw-semibold"><i class="bi bi-diagram-3 me-2"></i>Comisiones de Cursada e Inscripciones</li>';
if (isRes) listEl.innerHTML += '<li class="list-group-item text-danger"><i class="bi bi-calendar-event me-2"></i>Asignaciones de Aulas (Reservas del Calendario)</li>';
if (isCls) listEl.innerHTML += '<li class="list-group-item text-danger"><i class="bi bi-door-open me-2"></i>Aulas Físicas y Virtuales</li>';
}
}
const modalEl = document.getElementById('purgeConfirmModal');
if (modalEl) {
if (modalEl && typeof bootstrap !== 'undefined' && bootstrap.Modal) {
try {
const modal = new bootstrap.Modal(modalEl);
modal.show();
return;
} catch (e) {
console.warn('Bootstrap modal error, falling back to window.confirm:', e);
}
}
// Fallback nativo si Bootstrap modal no estuviera disponible
if (confirm("ATENCIÓN: ¿Confirma la purga y eliminación permanente de los datos académicos seleccionados?\nEsta acción es irreversible.")) {
confirmPurge();
}
}
function confirmPurge() {
const btn = document.getElementById('btnDoPurge');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Purgando base de datos...';
}
const form = document.getElementById('purgeForm');
if (form) {
form.submit();
}
}
</script>
<!-- Modal: Confirmación de Purga de Datos Académicos -->
<div class="modal fade" id="purgeConfirmModal" tabindex="-1" aria-labelledby="purgeConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header bg-danger text-white py-3">
<h5 class="modal-title h6 fw-bold" id="purgeConfirmModalLabel">
<i class="bi bi-exclamation-triangle-fill me-2"></i>Confirmar Purga de Datos Académicos
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<p class="mb-3 text-body">
Se procederá a <strong>eliminar de forma permanente</strong> los siguientes registros de la base de datos:
</p>
<ul class="list-group list-group-flush border rounded mb-3 small" id="purgeEntitiesList">
<!-- Poblado dinámicamente -->
</ul>
<div class="alert alert-warning small mb-0 d-flex gap-2 align-items-start">
<i class="bi bi-shield-lock-fill fs-5 text-warning flex-shrink-0"></i>
<div>
<strong>Seguridad del Sistema:</strong> Los usuarios del sistema (administradores, bedeles, docentes, alumnos), sus contraseñas, roles RBAC y registros de auditoría <u>permanecerán intactos</u>.
</div>
</div>
</div>
<div class="modal-footer bg-body-tertiary border-top py-2">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancelar</button>
<button type="button" class="btn btn-danger btn-sm px-3" id="btnDoPurge" onclick="confirmPurge()">
<i class="bi bi-trash-fill me-1"></i>Sí, Confirmar Purga
</button>
</div>
</div>
</div>
</div>
{% endblock %}
+26
View File
@@ -605,7 +605,33 @@ $(document).ready(function() {
updateSummary();
});
// Sincronización inicial si classroom_id viene precargado (desde aulas o query)
const preselectedClassroomId = $classroom.val();
if (preselectedClassroomId && classroomData[preselectedClassroomId]) {
const c = classroomData[preselectedClassroomId];
if (c.building_id) {
$building.val(c.building_id).trigger('change');
if (c.floor) {
$floor.val(c.floor).trigger('change');
}
$classroom.val(preselectedClassroomId);
}
}
// Sincronización inicial si commission_id viene precargado (desde comisiones o query)
const preselectedCommId = $('#commission-select').val();
if (preselectedCommId && commissionData[preselectedCommId]) {
const comm = commissionData[preselectedCommId];
if (comm.virtual_link && !$('#virtual_link').val().trim()) {
$('#virtual_link').val(comm.virtual_link);
}
if (comm.max_students && (!($('#expected_attendees').val()) || $('#expected_attendees').val() == '30')) {
$('#expected_attendees').val(comm.max_students);
}
}
updateSummary();
checkCapacity();
// Form validation
$('#reservation-form').on('submit', function(e) {