diff --git a/CHANGELOG.md b/CHANGELOG.md index 7604a76..541e993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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:** diff --git a/README.md b/README.md index 1f698f6..53b8c2c 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/ROADMAP_MVP.md b/ROADMAP_MVP.md index d6598cb..7215b37 100644 --- a/ROADMAP_MVP.md +++ b/ROADMAP_MVP.md @@ -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:** diff --git a/backend/scripts/reset_academic_data.py b/backend/scripts/reset_academic_data.py index e3a20b4..3f9403c 100644 --- a/backend/scripts/reset_academic_data.py +++ b/backend/scripts/reset_academic_data.py @@ -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 { diff --git a/frontend/src/routes/classrooms.js b/frontend/src/routes/classrooms.js index 5de1ec9..d499dc2 100644 --- a/frontend/src/routes/classrooms.js +++ b/frontend/src/routes/classrooms.js @@ -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 }; diff --git a/frontend/src/routes/schedule.js b/frontend/src/routes/schedule.js index 9fc849a..77e4a76 100644 --- a/frontend/src/routes/schedule.js +++ b/frontend/src/routes/schedule.js @@ -307,13 +307,33 @@ const handleAddReservation = async (req, res) => { }; }); - const 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 } - ]; + 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 } + ]; + } 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 = [ - { 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 } - ]; + 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 } + ]; + } 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.)` })); diff --git a/frontend/views/admin/commissions/list.html b/frontend/views/admin/commissions/list.html index 127ddcd..23218c4 100644 --- a/frontend/views/admin/commissions/list.html +++ b/frontend/views/admin/commissions/list.html @@ -231,6 +231,10 @@ class="btn btn-outline-primary py-0 px-2" title="Gestionar e Inscribir Alumnos"> + + + + + + + + + {% endblock %} diff --git a/frontend/views/schedule/add.html b/frontend/views/schedule/add.html index 97f523c..8b7b7be 100644 --- a/frontend/views/schedule/add.html +++ b/frontend/views/schedule/add.html @@ -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) {