feat(settings): agregar opcion global para habilitar o deshabilitar saludo y selector de rol en dashboard
This commit is contained in:
@@ -25,6 +25,10 @@ El formato está basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1
|
||||
- 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.
|
||||
- **Personalización de Interfaz y Saludo en Dashboard (Configuración Global):**
|
||||
- **Nueva Pestaña de Interfaz & Dashboard (`/admin/settings`):** Módulo en el panel de configuración global para personalizar elementos visuales del sistema.
|
||||
- **Desactivación Opcional del Saludo y Selector de Rol:** Switch interactivo para habilitar o deshabilitar el banner superior `¡Hola, {Usuario}!` y el selector de perspectiva de roles en `dashboard.html`. Al deshabilitarlo, el usuario accede directamente a los indicadores y accesos operativos de su rol sin distracciones ni dudas de contexto.
|
||||
- **Persistencia y Endpoint REST:** Parámetro `dashboard_role_banner_enabled` administrado vía `POST /api/v1/admin/settings/ui`, respaldado por `SystemSetting` y registrado en `AuditLog`.
|
||||
|
||||
### Corregido
|
||||
- **Motor de Purga y Reinicio de Datos Académicos:**
|
||||
|
||||
@@ -1832,13 +1832,18 @@ def get_all_settings():
|
||||
'sync_interval_minutes': int(SystemSetting.get_value('moodle_sync_interval_minutes', 15))
|
||||
}
|
||||
|
||||
ui_config = {
|
||||
'show_role_banner': SystemSetting.get_value('dashboard_role_banner_enabled', 'true') in ['true', 'True', '1', True]
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'settings': {
|
||||
'smtp': smtp_config,
|
||||
'auth_providers': auth_providers,
|
||||
'google_oauth': google_oauth,
|
||||
'moodle': moodle_config
|
||||
'moodle': moodle_config,
|
||||
'ui': ui_config
|
||||
}
|
||||
}), 200
|
||||
|
||||
@@ -2021,6 +2026,47 @@ def update_auth_providers():
|
||||
}), 200
|
||||
|
||||
|
||||
@api_admin_bp.route('/settings/ui', methods=['POST'])
|
||||
@jwt_required
|
||||
def update_ui_settings():
|
||||
"""Habilita o deshabilita elementos de interfaz de usuario, como el banner de saludo institucional y selector de roles."""
|
||||
from app.models.setting import SystemSetting
|
||||
from app.models.audit_log import AuditLog
|
||||
|
||||
acting_user = getattr(g, 'jwt_user', None)
|
||||
if not (acting_user and (acting_user.is_admin() or getattr(acting_user, 'role', '').upper() == 'ADMIN')):
|
||||
return jsonify({'error': 'Forbidden', 'message': 'Solo administradores pueden modificar las preferencias de interfaz.'}), 403
|
||||
|
||||
data = request.get_json(silent=True) or request.form.to_dict() or {}
|
||||
show_banner_input = data.get('show_role_banner')
|
||||
if show_banner_input is None:
|
||||
show_banner_input = data.get('dashboard_role_banner_enabled')
|
||||
|
||||
show_val = 'true' if show_banner_input in [True, 'true', '1', 'on', 1] else 'false'
|
||||
SystemSetting.set_value('dashboard_role_banner_enabled', show_val, 'Mostrar banner de saludo y selector de rol en Dashboard', category='ui')
|
||||
|
||||
try:
|
||||
audit = AuditLog(
|
||||
user_id=acting_user.id,
|
||||
user_email=acting_user.email,
|
||||
action='UPDATE_UI_SETTINGS',
|
||||
module='settings',
|
||||
details=f"Preferencia de interfaz actualizada: banner_saludo_rol={show_val}"
|
||||
)
|
||||
db.session.add(audit)
|
||||
db.session.commit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Preferencia de interfaz guardada correctamente.',
|
||||
'ui': {
|
||||
'show_role_banner': show_val == 'true'
|
||||
}
|
||||
}), 200
|
||||
|
||||
|
||||
@api_admin_bp.route('/settings/demo-mode', methods=['GET', 'POST'])
|
||||
@jwt_required
|
||||
def toggle_demo_mode_api():
|
||||
|
||||
@@ -11,6 +11,7 @@ from app import db
|
||||
|
||||
from app.models.milestone import AcademicMilestone
|
||||
from app.models.subject import Commission
|
||||
from app.models.setting import SystemSetting
|
||||
|
||||
api_dashboard_bp = Blueprint('api_dashboard', __name__, url_prefix='/api/v1/dashboard')
|
||||
|
||||
@@ -137,9 +138,9 @@ def get_stats():
|
||||
recent_audits = AuditLog.query.order_by(AuditLog.created_at.desc()).limit(6).all()
|
||||
audit_data = [{
|
||||
'created_at': a.created_at.strftime('%Y-%m-%d %H:%M') if a.created_at else None,
|
||||
'action_type': a.action_type,
|
||||
'action_type': getattr(a, 'action', getattr(a, 'action_type', 'AUDIT')),
|
||||
'ip_address': a.ip_address,
|
||||
'user_email': getattr(a.user, 'email', 'System') if hasattr(a, 'user') else 'System'
|
||||
'user_email': getattr(a, 'user_email', None) or (getattr(a.user, 'email', 'System') if hasattr(a, 'user') else 'System')
|
||||
} for a in recent_audits]
|
||||
|
||||
occupancy_rate = min(100, int((today_reservations / max(1, physical_classrooms * 3)) * 100)) if physical_classrooms > 0 else 0
|
||||
@@ -307,6 +308,9 @@ def get_stats():
|
||||
'today_schedule_list': today_schedule_list,
|
||||
'upcoming_reservations': upcoming_reservations,
|
||||
'milestones': milestones,
|
||||
'ui': {
|
||||
'show_role_banner': SystemSetting.get_value('dashboard_role_banner_enabled', 'true') in ['true', 'True', '1', True]
|
||||
},
|
||||
'bedelia': {
|
||||
'unassigned_commissions_week': unassigned_commissions_week,
|
||||
'unassigned_commissions_month': unassigned_commissions_month,
|
||||
|
||||
@@ -1406,6 +1406,9 @@ const handleGlobalSettings = async (req, res) => {
|
||||
if (settings && settings.moodle) {
|
||||
settings.moodle.ws_token_masked = settings.moodle.ws_token_masked || settings.moodle.ws_token || '';
|
||||
}
|
||||
if (settings) {
|
||||
settings.ui = settings.ui || { show_role_banner: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const demoResp = await req.apiClient.get('/settings/demo-mode');
|
||||
@@ -1503,5 +1506,6 @@ router.post('/settings/moodle', forwardSettingsAction);
|
||||
router.post('/settings/moodle/test', forwardSettingsAction);
|
||||
router.post('/settings/moodle/sync', forwardSettingsAction);
|
||||
router.get('/settings/moodle/stats', forwardSettingsAction);
|
||||
router.post('/settings/ui', forwardSettingsAction);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -266,6 +266,11 @@ router.get('/', async (req, res) => {
|
||||
{ start_time: '18:30', end_time: '22:30', date_display: '24/09', subject_name: 'Ingeniería de Software', subject_code: 'IS-301', shift: 'Noche', commission: { code: 'COM-3' }, status: 'PENDING', classroom: { code: 'Aula Magna' }, user: { name: 'Ing. Marcos Ruiz' } }
|
||||
];
|
||||
|
||||
let show_role_banner = true;
|
||||
if (apiData && apiData.ui && apiData.ui.show_role_banner !== undefined) {
|
||||
show_role_banner = Boolean(apiData.ui.show_role_banner);
|
||||
}
|
||||
|
||||
res.render('dashboard', {
|
||||
title: role === 'bedelia' ? 'Panel Operativo de Bedelía - Edu-Space Admin' :
|
||||
role === 'docente' ? 'Panel Docente - Edu-Space Admin' :
|
||||
@@ -274,6 +279,7 @@ router.get('/', async (req, res) => {
|
||||
user: req.user,
|
||||
role,
|
||||
actual_role,
|
||||
show_role_banner,
|
||||
stats,
|
||||
role_data,
|
||||
monthly_data,
|
||||
|
||||
@@ -83,6 +83,11 @@
|
||||
<i class="bi bi-mortarboard me-2 text-danger"></i>Moodle 4.1 & Cola de Sync
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-ui-btn" data-bs-toggle="tab" data-bs-target="#tab-ui" type="button" role="tab">
|
||||
<i class="bi bi-window-desktop me-2 text-primary"></i>Interfaz & Dashboard
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -495,6 +500,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- PESTAÑA 4: INTERFAZ & DASHBOARD -->
|
||||
<!-- ========================================== -->
|
||||
<div class="tab-pane fade" id="tab-ui" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<form id="uiConfigForm" onsubmit="saveUiConfig(event)">
|
||||
<div class="card border shadow-none bg-body-tertiary p-3 mb-4 rounded-3">
|
||||
<h6 class="fw-bold mb-3 d-flex align-items-center">
|
||||
<i class="bi bi-layout-text-window-reverse text-primary me-2"></i>Personalización Visual del Dashboard
|
||||
</h6>
|
||||
|
||||
<div class="form-check form-switch p-0 d-flex align-items-center justify-content-between mb-2 border rounded p-3 bg-body">
|
||||
<div class="pe-3">
|
||||
<label class="form-check-label fw-bold mb-1 d-block text-body" for="show_role_banner_switch">
|
||||
Mostrar banner de saludo y selector de roles en el Dashboard
|
||||
</label>
|
||||
<div class="text-muted small">
|
||||
Habilita el banner superior <code>¡Hola, {Usuario}!</code> junto a la insignia de rol y la barra de cambio de perspectiva de roles (Admin, Bedelía, Docente, Alumno) en el Dashboard.
|
||||
<div class="text-warning-emphasis small mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>Si se desactiva, el dashboard se mostrará directamente con los indicadores operativos del rol del usuario, evitando confusiones.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input class="form-check-input ms-2 fs-4" type="checkbox" role="switch" id="show_role_banner_switch"
|
||||
{% if settings.ui and settings.ui.show_role_banner %}checked{% elif not settings.ui or settings.ui.show_role_banner is not defined %}checked{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary btn-sm px-4" id="btnSaveUi">
|
||||
<i class="bi bi-check2-circle me-1"></i>Guardar Preferencias de Interfaz
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Panel de Consejos / UX -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 bg-primary-subtle text-primary-emphasis p-3 rounded-3 mb-3">
|
||||
<h6 class="fw-bold mb-2"><i class="bi bi-lightbulb me-1"></i>Experiencia de Usuario (UX)</h6>
|
||||
<p class="small mb-0">
|
||||
El banner de saludo institucional y selector de perspectiva es ideal durante capacitaciones. Al deshabilitarlo para el uso diario institucional, los administradores y usuarios acceden de inmediato a sus módulos de gestión y carteleras sin distracciones ni dudas de rol.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1024,6 +1078,37 @@ function escapeHtml(str) {
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Guardar Configuración de Interfaz (UI & Dashboard)
|
||||
// -------------------------------------------------------------
|
||||
async function saveUiConfig(e) {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnSaveUi');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...';
|
||||
|
||||
const showRoleBanner = document.getElementById('show_role_banner_switch').checked;
|
||||
|
||||
try {
|
||||
const res = await fetch('/admin/settings/ui', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ show_role_banner: showRoleBanner })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(data.message || 'Preferencias de interfaz guardadas correctamente.', 'success');
|
||||
} else {
|
||||
showAlert(data.message || 'Error al guardar preferencias de interfaz.', 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Error de comunicación con el servidor: ' + err.message, 'danger');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-check2-circle me-1"></i>Guardar Preferencias de Interfaz';
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar estadísticas y tareas al iniciar
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
refreshQueueStats();
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<!-- ========================================================================= -->
|
||||
<!-- 1. HEADER & ROLE PERSPECTIVE SWITCHER -->
|
||||
<!-- ========================================================================= -->
|
||||
{% if show_role_banner %}
|
||||
<div class="dashboard-role-banner mb-3 shadow-sm">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
|
||||
<div>
|
||||
@@ -78,6 +79,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<!-- ========================================================================= -->
|
||||
|
||||
Reference in New Issue
Block a user