fix(classrooms): blindar vistas presenciales y virtuales sin aulas + feat(admin): reinicializacion de sistema con clave de admin
This commit is contained in:
@@ -1529,5 +1529,7 @@ router.post('/settings/moodle/test', forwardSettingsAction);
|
||||
router.post('/settings/moodle/sync', forwardSettingsAction);
|
||||
router.get('/settings/moodle/stats', forwardSettingsAction);
|
||||
router.post('/settings/ui', forwardSettingsAction);
|
||||
router.post('/settings/reset_system', forwardSettingsAction);
|
||||
router.post('/system/reset', forwardSettingsAction);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -6,8 +6,13 @@ router.use(requireAuth);
|
||||
|
||||
const handleClassroomsList = async (req, res) => {
|
||||
try {
|
||||
const response = await req.apiClient.get('/classrooms');
|
||||
const allClassrooms = (response.data && response.data.classrooms) || [];
|
||||
let allClassrooms = [];
|
||||
try {
|
||||
const response = await req.apiClient.get('/classrooms');
|
||||
allClassrooms = (response.data && response.data.classrooms) || [];
|
||||
} catch (cErr) {
|
||||
console.warn('Notice fetching classrooms:', cErr.message);
|
||||
}
|
||||
|
||||
const view_mode = (req.query.view_mode || 'physical').toLowerCase();
|
||||
const search = (req.query.search || '').toLowerCase().trim();
|
||||
@@ -17,7 +22,7 @@ const handleClassroomsList = async (req, res) => {
|
||||
|
||||
// Filter based on view mode (physical vs virtual)
|
||||
let filtered = allClassrooms.filter(c => {
|
||||
const isVirt = Boolean(c.is_virtual || (c.code && c.code.includes('VIRTUAL')) || (c.building && c.building.toLowerCase().includes('virtual')));
|
||||
const isVirt = Boolean(c.is_virtual || ((c.code || '').includes('VIRTUAL')) || ((c.building || '').toLowerCase().includes('virtual')));
|
||||
if (view_mode === 'virtual') return isVirt;
|
||||
return !isVirt;
|
||||
});
|
||||
@@ -25,9 +30,9 @@ const handleClassroomsList = async (req, res) => {
|
||||
// Additional filters
|
||||
if (search) {
|
||||
filtered = filtered.filter(c =>
|
||||
(c.code && c.code.toLowerCase().includes(search)) ||
|
||||
(c.description && c.description.toLowerCase().includes(search)) ||
|
||||
(c.building && c.building.toLowerCase().includes(search))
|
||||
((c.code || '').toLowerCase().includes(search)) ||
|
||||
((c.description || '').toLowerCase().includes(search)) ||
|
||||
((c.building || '').toLowerCase().includes(search))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,9 +48,21 @@ const handleClassroomsList = async (req, res) => {
|
||||
filtered = filtered.filter(c => c.is_active);
|
||||
}
|
||||
|
||||
// Extract unique buildings and floors
|
||||
// Extract unique buildings and floors (consultando también /buildings si está disponible)
|
||||
const buildingsMap = new Map();
|
||||
const floorsSet = new Set();
|
||||
try {
|
||||
const bRes = await req.apiClient.get('/buildings');
|
||||
const apiBuildings = (bRes.data && bRes.data.buildings) || [];
|
||||
apiBuildings.forEach(b => {
|
||||
if (b.id && b.name) {
|
||||
buildingsMap.set(b.id, { id: b.id, name: b.name });
|
||||
}
|
||||
});
|
||||
} catch (bErr) {
|
||||
console.warn('Notice fetching buildings for classrooms filter:', bErr.message);
|
||||
}
|
||||
|
||||
allClassrooms.forEach(c => {
|
||||
if (c.building_id && c.building) {
|
||||
buildingsMap.set(c.building_id, { id: c.building_id, name: c.building });
|
||||
@@ -55,8 +72,8 @@ const handleClassroomsList = async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const physical_count = allClassrooms.filter(c => !c.is_virtual && !c.code.includes('VIRTUAL')).length;
|
||||
const virtual_count = allClassrooms.filter(c => c.is_virtual || c.code.includes('VIRTUAL')).length;
|
||||
const physical_count = allClassrooms.filter(c => !c.is_virtual && !(c.code || '').includes('VIRTUAL')).length;
|
||||
const virtual_count = allClassrooms.filter(c => Boolean(c.is_virtual || (c.code || '').includes('VIRTUAL'))).length;
|
||||
|
||||
const dateFilters = {
|
||||
today: new Date().toISOString().split('T')[0],
|
||||
@@ -80,7 +97,7 @@ const handleClassroomsList = async (req, res) => {
|
||||
|
||||
// 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 c_res = allReservations.filter(r => (c.id !== undefined && (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;
|
||||
@@ -88,15 +105,15 @@ const handleClassroomsList = async (req, res) => {
|
||||
commMap.set(cid, {
|
||||
commission_id: cid,
|
||||
commission_code: (r.commission && r.commission.code) || 'C1',
|
||||
subject_name: r.subject_name || r.purpose,
|
||||
subject_name: r.subject_name || r.purpose || 'Clase Regular',
|
||||
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_res = c_res.filter(r => (r.start_time || '').startsWith('2026-09-18') || (r.start_time || '').startsWith('2026-09-15'));
|
||||
const week_res = c_res.filter(r => (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));
|
||||
@@ -129,8 +146,8 @@ const handleClassroomsList = async (req, res) => {
|
||||
total_count: 0
|
||||
};
|
||||
|
||||
const virtualClassroomObj = allClassrooms.find(c => Boolean(c.is_virtual || (c.code && c.code.includes('VIRTUAL'))));
|
||||
const virtual_classroom_id = virtualClassroomObj ? virtualClassroomObj.id : 32;
|
||||
const virtualClassroomObj = allClassrooms.find(c => Boolean(c.is_virtual || ((c.code || '').includes('VIRTUAL'))));
|
||||
const virtual_classroom_id = virtualClassroomObj ? virtualClassroomObj.id : null;
|
||||
|
||||
if (view_mode === 'virtual') {
|
||||
try {
|
||||
@@ -162,12 +179,14 @@ const handleClassroomsList = async (req, res) => {
|
||||
|
||||
virtual_rooms = Array.from(commMap.values()).map(data => {
|
||||
const resList = data.reservations;
|
||||
const dayCnt = resList.filter(r => r.start_time.startsWith(todayStr)).length;
|
||||
const dayCnt = resList.filter(r => (r.start_time || '').startsWith(todayStr)).length;
|
||||
const weekCnt = resList.filter(r => {
|
||||
if (!r.start_time) return false;
|
||||
const d = new Date(r.start_time);
|
||||
return d >= weekStart && d <= weekEnd;
|
||||
}).length;
|
||||
const monthCnt = resList.filter(r => {
|
||||
if (!r.start_time) return false;
|
||||
const d = new Date(r.start_time);
|
||||
return d >= monthStart && d <= monthEnd;
|
||||
}).length;
|
||||
@@ -186,27 +205,29 @@ const handleClassroomsList = async (req, res) => {
|
||||
week_count: weekCnt,
|
||||
month_count: monthCnt,
|
||||
total_count: resList.length,
|
||||
reservations: resList.sort((a, b) => a.start_time.localeCompare(b.start_time))
|
||||
reservations: resList.sort((a, b) => (a.start_time || '').localeCompare(b.start_time || ''))
|
||||
};
|
||||
});
|
||||
|
||||
if (search) {
|
||||
virtual_rooms = virtual_rooms.filter(room =>
|
||||
room.subject_name.toLowerCase().includes(search) ||
|
||||
room.commission_code.toLowerCase().includes(search) ||
|
||||
room.subject_code.toLowerCase().includes(search)
|
||||
(room.subject_name || '').toLowerCase().includes(search) ||
|
||||
(room.commission_code || '').toLowerCase().includes(search) ||
|
||||
(room.subject_code || '').toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
virtual_rooms.sort((a, b) => a.subject_name.localeCompare(b.subject_name));
|
||||
virtual_rooms.sort((a, b) => (a.subject_name || '').localeCompare(b.subject_name || ''));
|
||||
|
||||
global_virtual_metrics = {
|
||||
today_count: virtualReservations.filter(r => r.start_time.startsWith(todayStr)).length,
|
||||
today_count: virtualReservations.filter(r => (r.start_time || '').startsWith(todayStr)).length,
|
||||
week_count: virtualReservations.filter(r => {
|
||||
if (!r.start_time) return false;
|
||||
const d = new Date(r.start_time);
|
||||
return d >= weekStart && d <= weekEnd;
|
||||
}).length,
|
||||
month_count: virtualReservations.filter(r => {
|
||||
if (!r.start_time) return false;
|
||||
const d = new Date(r.start_time);
|
||||
return d >= monthStart && d <= monthEnd;
|
||||
}).length,
|
||||
@@ -247,10 +268,11 @@ const handleClassroomsList = async (req, res) => {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in classrooms list route:', error.message);
|
||||
const view_mode = (req.query.view_mode || 'physical').toLowerCase();
|
||||
res.render('classrooms/list', {
|
||||
title: 'Aulas y Espacios (Offline)',
|
||||
title: 'Aulas y Espacios - Edu-Space Admin',
|
||||
classrooms: { items: [], total: 0, pages: 1, page: 1, has_prev: false, has_next: false, iter_pages: () => [1] },
|
||||
view_mode: 'physical',
|
||||
view_mode,
|
||||
search: '',
|
||||
building_filter: '',
|
||||
floor_filter: '',
|
||||
@@ -259,8 +281,17 @@ const handleClassroomsList = async (req, res) => {
|
||||
virtual_count: 0,
|
||||
buildings: [],
|
||||
floors: [],
|
||||
occupancy_metrics: { get: (id, defVal) => defVal },
|
||||
date_filters: {}
|
||||
occupancy_metrics: {},
|
||||
date_filters: {},
|
||||
virtual_rooms: [],
|
||||
global_virtual_metrics: {
|
||||
today_count: 0,
|
||||
week_count: 0,
|
||||
month_count: 0,
|
||||
commissions_count: 0,
|
||||
total_count: 0
|
||||
},
|
||||
virtual_classroom_id: null
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -270,38 +301,53 @@ const { createFormMock } = require('../utils/formHelper');
|
||||
const handleAddClassroom = async (req, res) => {
|
||||
try {
|
||||
let allClassrooms = [];
|
||||
let allBuildings = [];
|
||||
try {
|
||||
const response = await req.apiClient.get('/classrooms');
|
||||
allClassrooms = (response.data && response.data.classrooms) || [];
|
||||
const [cRes, bRes] = await Promise.allSettled([
|
||||
req.apiClient.get('/classrooms'),
|
||||
req.apiClient.get('/buildings')
|
||||
]);
|
||||
if (cRes.status === 'fulfilled' && cRes.value.data?.classrooms) {
|
||||
allClassrooms = cRes.value.data.classrooms;
|
||||
}
|
||||
if (bRes.status === 'fulfilled' && bRes.value.data?.buildings) {
|
||||
allBuildings = bRes.value.data.buildings;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not fetch classrooms for building floors:', e.message);
|
||||
console.warn('Could not fetch classrooms/buildings for add page:', e.message);
|
||||
}
|
||||
|
||||
// Map floors per building
|
||||
const buildingFloors = {
|
||||
'1': ['PB', '1', '2', '3'],
|
||||
'2': ['Virtual']
|
||||
};
|
||||
const buildingsMap = new Map();
|
||||
buildingsMap.set('1', { id: 1, name: 'Edificio Central' });
|
||||
buildingsMap.set('2', { id: 2, name: 'Campus Virtual' });
|
||||
allBuildings.forEach(b => {
|
||||
if (b.id && b.name) {
|
||||
buildingsMap.set(String(b.id), { id: b.id, name: b.name });
|
||||
}
|
||||
});
|
||||
|
||||
const buildingFloors = {};
|
||||
allBuildings.forEach(b => {
|
||||
const bId = String(b.id);
|
||||
buildingFloors[bId] = b.is_virtual ? ['Digital'] : ['PB', '1', '2'];
|
||||
});
|
||||
|
||||
allClassrooms.forEach(c => {
|
||||
const bId = String(c.building_id || 1);
|
||||
if (!buildingFloors[bId]) buildingFloors[bId] = [];
|
||||
if (c.floor && !buildingFloors[bId].includes(c.floor)) {
|
||||
buildingFloors[bId].push(c.floor);
|
||||
}
|
||||
if (c.building_id && c.building) {
|
||||
buildingsMap.set(bId, { id: c.building_id, name: c.building });
|
||||
const bId = String(c.building_id || '');
|
||||
if (bId) {
|
||||
if (!buildingFloors[bId]) buildingFloors[bId] = [];
|
||||
if (c.floor && !buildingFloors[bId].includes(c.floor)) {
|
||||
buildingFloors[bId].push(c.floor);
|
||||
}
|
||||
if (c.building) {
|
||||
buildingsMap.set(bId, { id: c.building_id, name: c.building });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const buildings = Array.from(buildingsMap.values());
|
||||
const buildingOptions = [
|
||||
{ value: '', text: '-- Selecciona Edificio / Sede --' },
|
||||
{ value: '', text: buildings.length > 0 ? '-- Selecciona Edificio / Sede --' : '-- Sin sedes registradas (Cree una abajo) --' },
|
||||
...buildings.map(b => ({ value: b.id, text: b.name })),
|
||||
{ value: 'new', text: '+ Crear nuevo edificio' }
|
||||
{ value: 'new', text: '+ Crear nuevo edificio / sede' }
|
||||
];
|
||||
|
||||
const form = createFormMock({
|
||||
@@ -327,19 +373,12 @@ const handlePostClassroom = async (req, res) => {
|
||||
try {
|
||||
const { building_id, new_building_name, floor, room_number, capacity, description, is_active } = req.body;
|
||||
|
||||
let targetBuilding = building_id;
|
||||
if (building_id === 'new' && new_building_name) {
|
||||
targetBuilding = new_building_name;
|
||||
}
|
||||
|
||||
let bId = parseInt(targetBuilding, 10);
|
||||
if (isNaN(bId) || bId <= 0) {
|
||||
bId = (String(targetBuilding).toLowerCase().includes('virtual')) ? 2 : 1;
|
||||
}
|
||||
const isNewBuilding = building_id === 'new' || !building_id;
|
||||
const bName = (new_building_name || '').trim();
|
||||
const bId = (!isNewBuilding && building_id) ? parseInt(building_id, 10) : null;
|
||||
|
||||
const isVirtual = Boolean(
|
||||
bId === 2 ||
|
||||
String(targetBuilding).toLowerCase().includes('virtual') ||
|
||||
(bName && bName.toLowerCase().includes('virtual')) ||
|
||||
String(room_number).toUpperCase().includes('VIRTUAL') ||
|
||||
String(floor).toUpperCase() === 'DIGITAL'
|
||||
);
|
||||
@@ -348,12 +387,19 @@ const handlePostClassroom = async (req, res) => {
|
||||
code: room_number || `Aula-${Date.now()}`,
|
||||
capacity: (isVirtual || capacity === '∞' || capacity === '0' || !capacity) ? 0 : parseInt(capacity, 10),
|
||||
floor: floor || (isVirtual ? 'Digital' : 'PB'),
|
||||
building_id: bId,
|
||||
description: description || '',
|
||||
is_virtual: isVirtual,
|
||||
is_active: is_active === 'true' || is_active === 'on' || is_active === true
|
||||
};
|
||||
|
||||
if (bId && !isNaN(bId) && bId > 0) {
|
||||
payload.building_id = bId;
|
||||
} else if (bName) {
|
||||
payload.building_name = bName;
|
||||
} else {
|
||||
payload.building_name = isVirtual ? 'Campus Virtual' : 'Edificio Central';
|
||||
}
|
||||
|
||||
try {
|
||||
await req.apiClient.post('/classrooms', payload);
|
||||
} catch (apiErr) {
|
||||
|
||||
@@ -88,6 +88,11 @@
|
||||
<i class="bi bi-window-desktop me-2 text-primary"></i>Interfaz & Dashboard
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link text-danger" id="tab-maintenance-btn" data-bs-toggle="tab" data-bs-target="#tab-maintenance" type="button" role="tab">
|
||||
<i class="bi bi-radioactive me-2 text-danger"></i>Mantenimiento & Reinicio
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -549,6 +554,93 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- PESTAÑA 5: MANTENIMIENTO & REINICIO -->
|
||||
<!-- ========================================== -->
|
||||
<div class="tab-pane fade" id="tab-maintenance" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<!-- Danger Zone Card -->
|
||||
<div class="card border border-danger shadow-sm rounded-3 overflow-hidden">
|
||||
<div class="card-header bg-danger text-white py-3">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-exclamation-triangle-fill fs-4 me-2"></i>
|
||||
<div>
|
||||
<h5 class="h6 fw-bold mb-0 text-white">Zona de Peligro: Reinicialización Total del Sistema</h5>
|
||||
<small class="text-white-50">Depuración integral de datos operacionales para inicio de ciclo o migración en limpio</small>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge bg-white text-danger font-monospace px-2 py-1 small">Requiere Clave Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-4 bg-body">
|
||||
<p class="mb-3 text-body">
|
||||
Esta herramienta permite vaciar completamente los datos operacionales generados por la actividad académica y de uso diario en la plataforma, restableciendo el sistema a su estado inicial.
|
||||
</p>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 rounded-3 bg-danger-subtle border border-danger-subtle h-100">
|
||||
<h6 class="fw-bold text-danger mb-2 d-flex align-items-center">
|
||||
<i class="bi bi-trash3-fill me-2"></i>Datos que se eliminarán:
|
||||
</h6>
|
||||
<ul class="small mb-0 ps-3 text-danger-emphasis">
|
||||
<li><strong>Historiales:</strong> Toda la bitácora histórica y registros previos de auditoría.</li>
|
||||
<li><strong>Reservas:</strong> Todas las reservas de aulas, asignaciones de espacios y estados.</li>
|
||||
<li><strong>Espacios y Sedes:</strong> Todas las aulas físicas, virtuales, recursos y edificios/sedes.</li>
|
||||
<li><strong>Académica:</strong> Carreras, materias, comisiones, docentes asignados, matrículas, notas y ciclos lectivos.</li>
|
||||
<li><strong>Colas de sincronización:</strong> Tareas pendientes o fallidas hacia sistemas externos.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 rounded-3 bg-success-subtle border border-success-subtle h-100">
|
||||
<h6 class="fw-bold text-success mb-2 d-flex align-items-center">
|
||||
<i class="bi bi-shield-check me-2"></i>Datos que se PRESERVAN intactos:
|
||||
</h6>
|
||||
<ul class="small mb-0 ps-3 text-success-emphasis">
|
||||
<li><strong>Cuentas de Usuarios:</strong> Todos los usuarios (Administradores, Bedeles, Docentes, Alumnos) con sus contraseñas activas.</li>
|
||||
<li><strong>Roles y Permisos (RBAC):</strong> Estructura completa de perfiles institucionales.</li>
|
||||
<li><strong>Configuración Global:</strong> Preferencias de correo SMTP, Single Sign-On, Moodle y opciones de interfaz.</li>
|
||||
<li><strong>Tipos de Hitos:</strong> Tipos maestros configurados (Parciales, Finales, TP, etc.).</li>
|
||||
<li><strong>Módulo de Administración:</strong> Todos los componentes de la pestaña Administración.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning py-2 px-3 small d-flex align-items-center mb-4">
|
||||
<i class="bi bi-shield-lock-fill text-warning fs-5 me-2 flex-shrink-0"></i>
|
||||
<div>
|
||||
Por estrictas razones de seguridad institucional, para ejecutar esta acción deberá ingresar la <strong>contraseña del usuario Administrador</strong> actualmente autenticado.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="button" class="btn btn-danger btn-sm px-4 fw-semibold shadow-sm" data-bs-toggle="modal" data-bs-target="#modalResetSystem">
|
||||
<i class="bi bi-radioactive me-2"></i>Reinicializar Sistema...
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info / Good Practices -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 bg-body-tertiary p-3 rounded-3 mb-3">
|
||||
<h6 class="fw-bold mb-2"><i class="bi bi-info-circle text-primary me-1"></i>¿Cuándo usar esta función?</h6>
|
||||
<p class="small text-muted mb-2">
|
||||
Utilice la reinicialización cuando desee comenzar un nuevo ciclo lectivo desde cero, tras finalizar períodos de prueba o capacitación, o para cargar una nueva grilla de sedes y carreras sin arrastrar información residual.
|
||||
</p>
|
||||
<p class="small text-muted mb-0">
|
||||
Al conservar intactos los usuarios y las configuraciones de seguridad, el personal administrativo y docente mantiene su acceso ininterrumpido a la plataforma.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -580,6 +672,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Confirmar Reinicialización del Sistema -->
|
||||
<div class="modal fade" id="modalResetSystem" tabindex="-1" aria-labelledby="modalResetSystemLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-danger text-white py-2">
|
||||
<h6 class="modal-title fw-bold" id="modalResetSystemLabel">
|
||||
<i class="bi bi-shield-exclamation me-2"></i>Confirmación Crítica: Reinicio Total del Sistema
|
||||
</h6>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<div class="alert alert-danger py-2 px-3 small mb-3">
|
||||
<i class="bi bi-exclamation-triangle-fill me-1"></i>
|
||||
<strong>¡Atención!</strong> Esta acción es irreversible. Se eliminarán todas las reservas, aulas, sedes, materias, carreras, comisiones, notas y registros de auditoría previa.
|
||||
</div>
|
||||
|
||||
<p class="small text-muted mb-3">
|
||||
Las cuentas de usuario y la configuración del sistema permanecerán intactas. Para autorizar la purga definitiva, ingrese su <strong>contraseña de Administrador</strong>:
|
||||
</p>
|
||||
|
||||
<div id="resetFeedbackAlert"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="admin_reset_password" class="form-label small fw-bold">Contraseña del Administrador en sesión <span class="text-danger">*</span></label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-body-tertiary"><i class="bi bi-key-fill text-danger"></i></span>
|
||||
<input type="password" class="form-control" id="admin_reset_password" placeholder="Ingrese su contraseña institucional" required autocomplete="current-password">
|
||||
<button class="btn btn-outline-secondary pass-toggle-btn" type="button" onclick="togglePassVisibility('admin_reset_password')" title="Mostrar/ocultar contraseña">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="confirm_reset_checkbox" onchange="document.getElementById('btnConfirmSystemReset').disabled = !this.checked">
|
||||
<label class="form-check-label small text-body" for="confirm_reset_checkbox">
|
||||
Entiendo las consecuencias irreversibles de esta purga y confirmo la reinicialización de los datos.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer py-2 bg-light">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal" id="btnCancelReset">Cancelar</button>
|
||||
<button type="button" class="btn btn-sm btn-danger fw-semibold px-3" id="btnConfirmSystemReset" disabled onclick="executeSystemReset()">
|
||||
<i class="bi bi-radioactive me-1"></i>Confirmar Reinicio Completo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showAlert(message, type = 'success') {
|
||||
const container = document.getElementById('settingsAlertContainer');
|
||||
@@ -1109,6 +1251,63 @@ async function saveUiConfig(e) {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Reinicialización Total del Sistema (Requiere Contraseña Admin)
|
||||
// -------------------------------------------------------------
|
||||
async function executeSystemReset() {
|
||||
const passwordInput = document.getElementById('admin_reset_password');
|
||||
const feedback = document.getElementById('resetFeedbackAlert');
|
||||
const btnConfirm = document.getElementById('btnConfirmSystemReset');
|
||||
const btnCancel = document.getElementById('btnCancelReset');
|
||||
const check = document.getElementById('confirm_reset_checkbox');
|
||||
|
||||
const password = (passwordInput ? passwordInput.value : '').trim();
|
||||
if (!password) {
|
||||
feedback.innerHTML = '<div class="alert alert-warning py-2 px-3 small mb-3"><i class="bi bi-exclamation-circle me-1"></i>Por favor ingrese su contraseña de administrador.</div>';
|
||||
if (passwordInput) passwordInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
btnConfirm.disabled = true;
|
||||
btnCancel.disabled = true;
|
||||
btnConfirm.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Reinicializando sistema...';
|
||||
feedback.innerHTML = '<div class="alert alert-info py-2 px-3 small mb-3"><span class="spinner-border spinner-border-sm me-1"></span>Procesando purga de datos operacionales...</div>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/admin/settings/reset_system', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok && data.status === 'success') {
|
||||
feedback.innerHTML = `<div class="alert alert-success py-2 px-3 small mb-3"><i class="bi bi-check-circle-fill me-1"></i>${data.message}</div>`;
|
||||
showAlert(data.message, 'success');
|
||||
passwordInput.value = '';
|
||||
if (check) check.checked = false;
|
||||
|
||||
setTimeout(() => {
|
||||
const modalEl = document.getElementById('modalResetSystem');
|
||||
const modalInstance = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modalInstance) modalInstance.hide();
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
feedback.innerHTML = `<div class="alert alert-danger py-2 px-3 small mb-3"><i class="bi bi-x-circle-fill me-1"></i>${data.message || 'Error al autorizar la reinicialización.'}</div>`;
|
||||
btnConfirm.disabled = false;
|
||||
btnCancel.disabled = false;
|
||||
btnConfirm.innerHTML = '<i class="bi bi-radioactive me-1"></i>Confirmar Reinicio Completo';
|
||||
if (passwordInput) passwordInput.focus();
|
||||
}
|
||||
} catch (err) {
|
||||
feedback.innerHTML = `<div class="alert alert-danger py-2 px-3 small mb-3"><i class="bi bi-exclamation-triangle-fill me-1"></i>Error de comunicación: ${err.message}</div>`;
|
||||
btnConfirm.disabled = false;
|
||||
btnCancel.disabled = false;
|
||||
btnConfirm.innerHTML = '<i class="bi bi-radioactive me-1"></i>Confirmar Reinicio Completo';
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar estadísticas y tareas al iniciar
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
refreshQueueStats();
|
||||
|
||||
@@ -206,7 +206,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function updatePreview() {
|
||||
var bText = '-';
|
||||
if (buildingSelect) {
|
||||
if (buildingSelect.value === '__new__') {
|
||||
if (buildingSelect.value === '__new__' || buildingSelect.value === 'new') {
|
||||
bText = newBuildingNameInput.value.trim() || '{{ _("Nuevo Edificio") }}';
|
||||
} else if (buildingSelect.selectedIndex >= 0) {
|
||||
bText = buildingSelect.options[buildingSelect.selectedIndex].text;
|
||||
@@ -222,7 +222,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (buildingSelect) {
|
||||
buildingSelect.addEventListener('change', function() {
|
||||
if (this.value === '__new__') {
|
||||
if (this.value === '__new__' || this.value === 'new') {
|
||||
newBuildingContainer.style.display = 'block';
|
||||
if (newBuildingNameInput) newBuildingNameInput.focus();
|
||||
} else {
|
||||
@@ -232,7 +232,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
updatePreview();
|
||||
});
|
||||
// Initial call
|
||||
if (buildingSelect.value === '__new__') {
|
||||
if (buildingSelect.value === '__new__' || buildingSelect.value === 'new') {
|
||||
newBuildingContainer.style.display = 'block';
|
||||
} else if (buildingSelect.value) {
|
||||
updateFloorsDatalist(buildingSelect.value);
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Grilla y Tabla de Aulas Físicas -->
|
||||
{% if classrooms and classrooms.items %}
|
||||
{% if classrooms and classrooms.items and (classrooms.items|length > 0) %}
|
||||
|
||||
<!-- 1. VISTA DE TARJETAS / MOSAICO -->
|
||||
<div data-view-context="classrooms" data-view-container="cards">
|
||||
@@ -708,7 +708,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Catálogo de Aulas Virtuales por Comisión -->
|
||||
{% if virtual_rooms %}
|
||||
{% if virtual_rooms and (virtual_rooms|length > 0) %}
|
||||
<div class="row g-3">
|
||||
{% for room in virtual_rooms %}
|
||||
<div class="col-xxl-3 col-xl-3 col-lg-4 col-md-6 col-sm-12 mb-2">
|
||||
@@ -1008,7 +1008,7 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Modales de Eliminación de Aulas Físicas -->
|
||||
{% if classrooms and classrooms.items %}
|
||||
{% if classrooms and classrooms.items and (classrooms.items|length > 0) %}
|
||||
{% for classroom in classrooms.items %}
|
||||
<div class="modal fade text-start" id="modalDeleteClassroom{{ classroom.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
|
||||
Reference in New Issue
Block a user