feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Gestión de Ciclo Lectivo - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 fw-bold mb-1">
|
||||
<i class="bi bi-calendar-range text-primary me-2"></i>Gestión de Ciclo Lectivo y Períodos Académicos
|
||||
</h1>
|
||||
<p class="text-muted mb-0 small">
|
||||
Administración de cuatrimestres, años académicos, fechas de inicio/fin y selección del ciclo lectivo activo.
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if can_edit %}
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalAddTerm">
|
||||
<i class="bi bi-plus-lg me-1"></i>Nuevo Ciclo Lectivo
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="/schedule/commissions_view" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-diagram-3 me-1"></i>Ver Cursadas
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Academic Terms Table -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Nombre del Ciclo Lectivo</th>
|
||||
<th>Año</th>
|
||||
<th>Período / Modalidad</th>
|
||||
<th>Fechas del Período</th>
|
||||
<th>Comisiones</th>
|
||||
<th>Estado Institucional</th>
|
||||
{% if can_edit %}
|
||||
<th class="text-end">Acciones</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for term in terms %}
|
||||
{% set t_stat = stats[term.id] or {'commissions_count': 0} %}
|
||||
<tr class="{% if term.is_current %}table-primary-subtle{% endif %}">
|
||||
<td>
|
||||
<span class="badge font-monospace bg-primary-subtle text-primary border fs-6">{{ term.code }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold text-body">{{ term.name }}</div>
|
||||
<small class="text-muted">ID: #{{ term.id }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-semibold text-body">{{ term.year }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary-subtle text-secondary border">
|
||||
{{ term.period_type }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if term.start_date and term.end_date %}
|
||||
<small class="text-body font-monospace">
|
||||
<i class="bi bi-calendar3 me-1 text-primary"></i>{{ term.start_date }} - {{ term.end_date }}
|
||||
</small>
|
||||
{% else %}
|
||||
<small class="text-muted">Sin fechas fijadas</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info-subtle text-info border">
|
||||
<i class="bi bi-diagram-3 me-1"></i>{{ t_stat.commissions_count }} comisiones
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if term.is_current %}
|
||||
<span class="badge bg-success text-white px-2 py-1 shadow-sm">
|
||||
<i class="bi bi-check-circle-fill me-1"></i>Ciclo Activo Actual
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border">
|
||||
{% if term.is_active %}Habilitado{% else %}Archivado{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if can_edit %}
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if not term.is_current %}
|
||||
<form method="POST" action="/admin/academic_term_set_current" class="d-inline"
|
||||
onsubmit="return confirm('¿Desea establecer este ciclo lectivo como el Activo para todo el sistema?');">
|
||||
<input type="hidden" name="id" value="{{ term.id }}">
|
||||
<button type="submit" class="btn btn-outline-success py-0 px-2" title="Establecer como Ciclo Activo">
|
||||
<i class="bi bi-check2-circle me-1"></i>Activar
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-secondary py-0 px-2"
|
||||
data-bs-toggle="modal" data-bs-target="#modalEditTerm{{ term.id }}"
|
||||
title="Editar Ciclo Lectivo">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Editar Ciclo Lectivo -->
|
||||
<div class="modal fade text-start" id="modalEditTerm{{ term.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/academic_term_edit">
|
||||
<input type="hidden" name="id" value="{{ term.id }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title h6 fw-bold">
|
||||
<i class="bi bi-pencil-square text-primary me-2"></i>Editar Ciclo Lectivo
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label small fw-semibold">Código *</label>
|
||||
<input type="text" name="code" class="form-control form-control-sm font-monospace" value="{{ term.code }}" required>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label small fw-semibold">Año Calendario *</label>
|
||||
<input type="number" name="year" class="form-control form-control-sm" value="{{ term.year }}" min="2020" max="2050" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Nombre Descriptivo *</label>
|
||||
<input type="text" name="name" class="form-control form-control-sm" value="{{ term.name }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Tipo de Período</label>
|
||||
<select name="period_type" class="form-select form-select-sm">
|
||||
<option value="Primer Cuatrimestre" {% if term.period_type == 'Primer Cuatrimestre' %}selected{% endif %}>Primer Cuatrimestre</option>
|
||||
<option value="Segundo Cuatrimestre" {% if term.period_type == 'Segundo Cuatrimestre' %}selected{% endif %}>Segundo Cuatrimestre</option>
|
||||
<option value="Anual" {% if term.period_type == 'Anual' %}selected{% endif %}>Anual</option>
|
||||
<option value="Curso de Verano" {% if term.period_type == 'Curso de Verano' %}selected{% endif %}>Curso de Verano</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">Fecha Inicio</label>
|
||||
<input type="date" name="start_date" class="form-control form-control-sm" value="{{ term.start_date if term.start_date else '' }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">Fecha Fin</label>
|
||||
<input type="date" name="end_date" class="form-control form-control-sm" value="{{ term.end_date if term.end_date else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="is_current" id="isCurr{{ term.id }}" {% if term.is_current %}checked{% endif %}>
|
||||
<label class="form-check-label small fw-semibold text-primary" for="isCurr{{ term.id }}">
|
||||
Establecer como Ciclo Lectivo Activo Actual
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" id="isAct{{ term.id }}" {% if term.is_active %}checked{% endif %}>
|
||||
<label class="form-check-label small" for="isAct{{ term.id }}">Habilitado en el Sistema</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Guardar Cambios</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-calendar-range fs-1 d-block mb-2"></i>
|
||||
No se encontraron ciclos lectivos registrados.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Nuevo Ciclo Lectivo -->
|
||||
{% if can_edit %}
|
||||
<div class="modal fade" id="modalAddTerm" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/academic_term_add">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title h6 fw-bold">
|
||||
<i class="bi bi-calendar-plus text-primary me-2"></i>Nuevo Ciclo Lectivo / Período Académico
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label small fw-semibold">Código *</label>
|
||||
<input type="text" name="code" class="form-control form-control-sm font-monospace" placeholder="ej. 2027-1" required>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label small fw-semibold">Año Calendario *</label>
|
||||
<input type="number" name="year" class="form-control form-control-sm" value="2026" min="2020" max="2050" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Nombre Descriptivo *</label>
|
||||
<input type="text" name="name" class="form-control form-control-sm" placeholder="ej. Ciclo Lectivo 2027 - 1º Cuatrimestre" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Tipo de Período</label>
|
||||
<select name="period_type" class="form-select form-select-sm">
|
||||
<option value="Primer Cuatrimestre">Primer Cuatrimestre</option>
|
||||
<option value="Segundo Cuatrimestre" selected>Segundo Cuatrimestre</option>
|
||||
<option value="Anual">Anual</option>
|
||||
<option value="Curso de Verano">Curso de Verano</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">Fecha Inicio</label>
|
||||
<input type="date" name="start_date" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-semibold">Fecha Fin</label>
|
||||
<input type="date" name="end_date" class="form-control form-control-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_current" id="addIsCurr">
|
||||
<label class="form-check-label small fw-semibold text-primary" for="addIsCurr">
|
||||
Establecer de inmediato como Ciclo Lectivo Activo Actual
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Crear Ciclo Lectivo</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,161 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Bitácora de Auditoría - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-shield-check text-primary me-2"></i>Registro de Auditoría Institucional
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">Trazabilidad completa de operaciones, modificaciones y eventos de seguridad</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/main/dashboard" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Volver al Panel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtros -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-3">
|
||||
<form method="GET" action="/admin/audit_logs" class="row g-2 align-items-center">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-transparent"><i class="bi bi-search"></i></span>
|
||||
<input type="text" name="search" class="form-control" placeholder="Buscar por email, IP o detalles..." value="{{ search }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="module" class="form-select form-select-sm">
|
||||
<option value="">Todos los módulos</option>
|
||||
{% for mod in modules %}
|
||||
<option value="{{ mod }}" {% if current_module == mod %}selected{% endif %}>{{ mod|capitalize }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="action" class="form-select form-select-sm">
|
||||
<option value="">Todas las acciones</option>
|
||||
{% for act in actions %}
|
||||
<option value="{{ act }}" {% if current_action == act %}selected{% endif %}>{{ act }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex gap-1">
|
||||
<button type="submit" class="btn btn-primary btn-sm flex-grow-1">
|
||||
<i class="bi bi-filter me-1"></i>Filtrar
|
||||
</button>
|
||||
{% if search or current_module or current_action %}
|
||||
<a href="/admin/audit_logs" class="btn btn-outline-secondary btn-sm" title="Limpiar filtros">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de Auditoría -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
{% if audit_logs %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light small text-uppercase">
|
||||
<tr>
|
||||
<th class="ps-3">Fecha / Hora</th>
|
||||
<th>Usuario</th>
|
||||
<th>Acción</th>
|
||||
<th>Módulo</th>
|
||||
<th>ID Entidad</th>
|
||||
<th>Detalles</th>
|
||||
<th class="pe-3">IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in audit_logs %}
|
||||
<tr>
|
||||
<td class="ps-3 text-nowrap">
|
||||
<span class="small font-monospace">{{ log.created_at }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold small">{{ log.user_email or 'Sistema' }}</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if log.action == 'CREATE' %}
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">{{ log.action }}</span>
|
||||
{% elif log.action == 'UPDATE' %}
|
||||
<span class="badge bg-primary-subtle text-primary border border-primary-subtle">{{ log.action }}</span>
|
||||
{% elif log.action == 'DELETE' %}
|
||||
<span class="badge bg-danger-subtle text-danger border border-danger-subtle">{{ log.action }}</span>
|
||||
{% elif 'LOGIN' in log.action %}
|
||||
<span class="badge bg-info-subtle text-info border border-info-subtle">{{ log.action }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border">{{ log.action }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-body-secondary text-body border font-monospace small">{{ log.module }}</span>
|
||||
</td>
|
||||
<td class="font-monospace small text-muted">
|
||||
{{ log.entity_id or '-' }}
|
||||
</td>
|
||||
<td class="small text-muted" style="max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="{{ log.details }}">
|
||||
{{ log.details or '-' }}
|
||||
</td>
|
||||
<td class="pe-3 font-monospace small text-muted">
|
||||
{{ log.ip_address or '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Paginación -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="p-3 border-top d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">
|
||||
Mostrando página {{ pagination.page }} de {{ pagination.pages }} ({{ pagination.total }} registros)
|
||||
</small>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
<li class="page-item {% if not pagination.has_prev %}disabled{% endif %}">
|
||||
<a class="page-link" href="/admin/audit_logs">
|
||||
«
|
||||
</a>
|
||||
</li>
|
||||
{% for p in pagination.iter_pages(left_edge=2, left_current=2, right_current=3, right_edge=2) %}
|
||||
{% if p %}
|
||||
<li class="page-item {% if p == pagination.page %}active{% endif %}">
|
||||
<a class="page-link" href="/admin/audit_logs">{{ p }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
|
||||
<a class="page-link" href="/admin/audit_logs">
|
||||
»
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-shield-slash text-muted fs-1 d-block mb-2"></i>
|
||||
<h6 class="fw-bold">No se encontraron registros de auditoría</h6>
|
||||
<p class="text-muted small">Los eventos del sistema se registrarán automáticamente a medida que ocurran cambios.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,210 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Gestión de Carreras - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 fw-bold mb-1">
|
||||
<i class="bi bi-mortarboard text-primary me-2"></i>Gestión de Carreras y Titulaciones
|
||||
</h1>
|
||||
<p class="text-muted mb-0 small">
|
||||
Administración de programas académicos, tecnicaturas, licenciaturas y planes de estudio.
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if can_edit %}
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalAddCareer">
|
||||
<i class="bi bi-plus-lg me-1"></i>Nueva Carrera
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="/admin/subjects_list" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-book me-1"></i>Ver Asignaturas
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search & Filter Bar -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body py-2">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-body-tertiary"><i class="bi bi-search"></i></span>
|
||||
<input type="text" name="search" class="form-control"
|
||||
placeholder="Buscar carrera por nombre, código o descripción..."
|
||||
value="{{ search }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<button type="submit" class="btn btn-sm btn-primary me-1">
|
||||
<i class="bi bi-funnel me-1"></i>Filtrar
|
||||
</button>
|
||||
<a href="/admin/careers_list" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-x-circle me-1"></i>Limpiar
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Careers Table -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Carrera / Titulación</th>
|
||||
<th>Código</th>
|
||||
<th>Asignaturas</th>
|
||||
<th>Comisiones Activas</th>
|
||||
<th>Descripción</th>
|
||||
{% if can_edit %}
|
||||
<th class="text-end">Acciones</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for career in careers %}
|
||||
{% set c_stat = stats[career.id] or {'subjects_count': 0, 'commissions_count': 0} %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="rounded-circle p-2 bg-primary-subtle text-primary me-3">
|
||||
<i class="bi bi-mortarboard-fill"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span class="fw-bold text-body">{{ career.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if career.code %}
|
||||
<span class="badge font-monospace bg-secondary-subtle text-secondary border">{{ career.code }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/admin/subjects_list" class="badge bg-info-subtle text-info border text-decoration-none">
|
||||
<i class="bi bi-book me-1"></i>{{ c_stat.subjects_count }} asignaturas
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary-subtle text-primary border">
|
||||
<i class="bi bi-diagram-3 me-1"></i>{{ c_stat.commissions_count }} comisiones
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="small text-muted text-truncate d-inline-block" style="max-width: 250px;">
|
||||
{{ career.description or '-' }}
|
||||
</span>
|
||||
</td>
|
||||
{% if can_edit %}
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary py-0 px-2"
|
||||
data-bs-toggle="modal" data-bs-target="#modalEditCareer{{ career.id }}"
|
||||
title="Editar Carrera">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
{% if c_stat.subjects_count == 0 %}
|
||||
<form method="POST" action="/admin/career_delete" class="d-inline"
|
||||
onsubmit="return confirm('¿Confirma que desea eliminar esta carrera permanentemente?');">
|
||||
<input type="hidden" name="id" value="{{ career.id }}">
|
||||
<button type="submit" class="btn btn-outline-danger py-0 px-2" title="Eliminar">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Modal Editar Carrera -->
|
||||
<div class="modal fade text-start" id="modalEditCareer{{ career.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/career_edit">
|
||||
<input type="hidden" name="id" value="{{ career.id }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title h6 fw-bold">
|
||||
<i class="bi bi-pencil-square text-primary me-2"></i>Editar Carrera
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Nombre de la Carrera *</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ career.name }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Código / Sigla</label>
|
||||
<input type="text" name="code" class="form-control" value="{{ career.code or '' }}" placeholder="ej. LIC-SIS, PUEP">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Descripción</label>
|
||||
<textarea name="description" class="form-control" rows="3">{{ career.description or '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Guardar Cambios</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-mortarboard fs-1 d-block mb-2"></i>
|
||||
No se encontraron carreras registradas.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Nueva Carrera -->
|
||||
{% if can_edit %}
|
||||
<div class="modal fade" id="modalAddCareer" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/career_add">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title h6 fw-bold">
|
||||
<i class="bi bi-mortarboard text-primary me-2"></i>Nueva Carrera / Titulación
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Nombre de la Carrera *</label>
|
||||
<input type="text" name="name" class="form-control" placeholder="ej. Licenciatura en Tecnologías Digitales" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Código / Sigla</label>
|
||||
<input type="text" name="code" class="form-control" placeholder="ej. TEC-DIG">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Descripción</label>
|
||||
<textarea name="description" class="form-control" rows="3" placeholder="Objetivos y alcance de la titulación..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Crear Carrera</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,275 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Comisión {{ commission.full_code or commission.code }} — Edu-Space{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb small">
|
||||
<li class="breadcrumb-item"><a href="/main/dashboard">Inicio</a></li>
|
||||
<li class="breadcrumb-item"><a href="/admin/commissions_list">Comisiones</a></li>
|
||||
<li class="breadcrumb-item active">{{ commission.full_code or commission.code }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Left: Commission Info + Teacher Assignment -->
|
||||
<div class="col-12 col-lg-4">
|
||||
|
||||
<!-- Commission Card -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header border-0 bg-primary text-white rounded-top py-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-diagram-3 fs-4"></i>
|
||||
<div>
|
||||
<div class="fw-bold font-monospace">{{ commission.full_code or commission.code }}</div>
|
||||
<small class="opacity-75">{{ commission.semester }} {{ commission.year }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-1">{{ commission.subject.name }}</h5>
|
||||
<p class="text-muted small mb-3">{{ commission.subject.code }}
|
||||
{% if commission.subject.career_obj %}— {{ commission.subject.career_obj.name }}{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
{% if commission.shift %}
|
||||
{% set shift_colors = {'Mañana': 'warning', 'Tarde': 'info', 'Vespertino': 'secondary', 'Noche': 'dark'} %}
|
||||
<span class="badge bg-{{ shift_colors[commission.shift] or 'secondary' }}-subtle text-{{ shift_colors[commission.shift] or 'secondary' }} border border-{{ shift_colors[commission.shift] or 'secondary' }}-subtle">
|
||||
<i class="bi bi-clock me-1"></i>{{ commission.shift }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="badge {% if commission.active %}bg-success-subtle text-success{% else %}bg-secondary-subtle text-secondary{% endif %}">
|
||||
{{ 'Activa' if commission.active else 'Inactiva' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul class="list-unstyled small mb-0">
|
||||
<li class="d-flex justify-content-between py-1 border-bottom">
|
||||
<span class="text-muted">Cupo máximo</span>
|
||||
<strong>{{ commission.max_students }}</strong>
|
||||
</li>
|
||||
<li class="d-flex justify-content-between py-1 border-bottom">
|
||||
<span class="text-muted">Inscriptos</span>
|
||||
<strong class="{% if (commission.current_students or 0) >= commission.max_students %}text-danger{% else %}text-success{% endif %}">
|
||||
{{ commission.current_students or 0 }}
|
||||
</strong>
|
||||
</li>
|
||||
{% if commission.schedule %}
|
||||
<li class="d-flex justify-content-between py-1 border-bottom">
|
||||
<span class="text-muted">Horario</span>
|
||||
<span>{{ commission.schedule }}</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if commission.virtual_link %}
|
||||
<li class="py-1">
|
||||
<a href="{{ commission.virtual_link }}" target="_blank" class="btn btn-sm btn-outline-info w-100">
|
||||
<i class="bi bi-camera-video me-1"></i>Acceso Virtual
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Teacher Assignment -->
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header border-0 bg-transparent py-2 d-flex align-items-center gap-2">
|
||||
<i class="bi bi-person-video3 text-info"></i>
|
||||
<span class="fw-semibold">Docente Asignado</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if commission.teacher %}
|
||||
<div class="d-flex align-items-center gap-3 mb-3 p-2 rounded bg-info-subtle border border-info-subtle">
|
||||
<div class="avatar-sm bg-info rounded-circle d-flex align-items-center justify-content-center" style="width:40px;height:40px;flex-shrink:0;">
|
||||
<i class="bi bi-person-fill text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-semibold small">{{ commission.teacher.name }}</div>
|
||||
<div class="text-muted" style="font-size:.75rem;">{{ commission.teacher.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning py-2 small mb-3">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>Sin docente asignado
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit %}
|
||||
<form method="POST" action="/admin/commission_assign_teacher" id="assign-teacher-form">
|
||||
<input type="hidden" name="id" value="{{ commission.id }}">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-semibold mb-1">Cambiar / Asignar Docente</label>
|
||||
<select name="teacher_id" class="form-select form-select-sm" id="teacher-select">
|
||||
<option value="">— Sin asignar —</option>
|
||||
{% for teacher in teachers %}
|
||||
<option value="{{ teacher.id }}" {% if commission.teacher_id == teacher.id %}selected{% endif %}>
|
||||
{{ teacher.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-info btn-sm w-100" id="assign-teacher-btn">
|
||||
<i class="bi bi-person-check me-1"></i>Guardar Docente
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right: Students -->
|
||||
<div class="col-12 col-lg-8">
|
||||
|
||||
<!-- Enrolled Students -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header border-0 bg-transparent py-2 d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-mortarboard text-success"></i>
|
||||
<span class="fw-semibold">Alumnos Inscriptos</span>
|
||||
<span class="badge bg-success-subtle text-success">{{ enrollments | length }}</span>
|
||||
</div>
|
||||
{% if can_edit %}
|
||||
<button class="btn btn-sm btn-success" data-bs-toggle="collapse" data-bs-target="#enroll-form" id="add-student-btn">
|
||||
<i class="bi bi-person-plus me-1"></i>Inscribir Alumno
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Enroll Student Form (collapsible) -->
|
||||
{% if can_edit %}
|
||||
<div class="collapse" id="enroll-form">
|
||||
<div class="card-body border-top bg-success-subtle py-3">
|
||||
<form method="POST" action="/admin/commission_enroll_student" id="enroll-student-form">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label small fw-semibold mb-1">Seleccionar Alumno</label>
|
||||
<select name="student_id" class="form-select form-select-sm" required id="student-select">
|
||||
<option value="">— Elegir alumno —</option>
|
||||
{% for student in students %}
|
||||
{% if student.id not in enrolled_ids %}
|
||||
<option value="{{ student.id }}">{{ student.name }} — {{ student.email }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label small fw-semibold mb-1">Observaciones (opcional)</label>
|
||||
<input type="text" name="notes" class="form-control form-control-sm" placeholder="Notas…" id="enroll-notes">
|
||||
</div>
|
||||
<div class="col-12 col-md-2">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100" id="enroll-submit-btn">
|
||||
<i class="bi bi-plus-circle me-1"></i>Inscribir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body p-0">
|
||||
{% if enrollments %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle" id="enrollments-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-3">#</th>
|
||||
<th>Alumno</th>
|
||||
<th class="text-center">Estado</th>
|
||||
<th class="text-center">Fecha Inscripción</th>
|
||||
<th class="text-center">Notas</th>
|
||||
{% if can_edit %}<th class="text-end pe-3">Acciones</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in enrollments %}
|
||||
<tr id="enrollment-row-{{ e.student_id }}">
|
||||
<td class="ps-3 text-muted small">{{ loop.index }}</td>
|
||||
<td>
|
||||
<div class="fw-semibold small">{{ e.student.name if e.student else '—' }}</div>
|
||||
<div class="text-muted" style="font-size:.75rem;">{{ e.student.email if e.student else '' }}</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% set st_colors = {'activo': 'success', 'retirado': 'danger', 'condicional': 'warning'} %}
|
||||
<span class="badge bg-{{ st_colors[e.status] or 'secondary' }}-subtle text-{{ st_colors[e.status] or 'secondary' }} border border-{{ st_colors[e.status] or 'secondary' }}-subtle">
|
||||
{{ e.status | capitalize }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center small text-muted">
|
||||
{{ e.enrolled_at if e.enrolled_at else '—' }}
|
||||
</td>
|
||||
<td class="text-center small text-muted">{{ e.notes or '—' }}</td>
|
||||
{% if can_edit %}
|
||||
<td class="text-end pe-3">
|
||||
<div class="d-flex gap-1 justify-content-end">
|
||||
<!-- Quick status update -->
|
||||
<form method="POST" action="/admin/commission_update_enrollment" class="d-inline">
|
||||
<select name="status" class="form-select form-select-sm d-inline-block w-auto"
|
||||
onchange="this.form.submit()" title="Cambiar estado"
|
||||
id="status-select-{{ e.student_id }}">
|
||||
<option value="activo" {% if e.status == 'activo' %}selected{% endif %}>Activo</option>
|
||||
<option value="condicional" {% if e.status == 'condicional' %}selected{% endif %}>Condicional</option>
|
||||
<option value="retirado" {% if e.status == 'retirado' %}selected{% endif %}>Retirado</option>
|
||||
</select>
|
||||
</form>
|
||||
<!-- Remove button -->
|
||||
{% set st_display = e.student.name if e.student else 'este alumno' %}
|
||||
<form method="POST" action="/admin/commission_unenroll_student"
|
||||
onsubmit="return confirm('¿Dar de baja a {{ st_display }} de la comisión?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Dar de baja"
|
||||
id="unenroll-btn-{{ e.student_id }}">
|
||||
<i class="bi bi-person-dash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-people display-5 text-muted opacity-40"></i>
|
||||
<p class="text-muted mt-3 small">No hay alumnos inscriptos en esta comisión.</p>
|
||||
{% if can_edit %}
|
||||
<button class="btn btn-outline-success btn-sm" data-bs-toggle="collapse" data-bs-target="#enroll-form">
|
||||
<i class="bi bi-person-plus me-1"></i>Inscribir primer alumno
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Capacity Bar -->
|
||||
{% set enrolled_count = enrollments | length %}
|
||||
{% set max_students = commission.max_students or 1 %}
|
||||
{% set cap_pct = [((enrolled_count / max_students) * 100) | round | int, 100] | min %}
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body py-3">
|
||||
<div class="d-flex justify-content-between small fw-semibold mb-1">
|
||||
<span><i class="bi bi-bar-chart-fill me-1 text-primary"></i>Ocupación del cupo</span>
|
||||
<span>{{ enrolled_count }} / {{ max_students }} ({{ cap_pct }}%)</span>
|
||||
</div>
|
||||
<div class="progress" style="height:10px;">
|
||||
<div class="progress-bar {% if cap_pct >= 90 %}bg-danger{% elif cap_pct >= 70 %}bg-warning{% else %}bg-success{% endif %}"
|
||||
{{ ("style=\"width:" ~ cap_pct ~ "%;\"") | safe }} role="progressbar"
|
||||
aria-valuenow="{{ cap_pct }}" aria-valuemin="0" aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,200 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Gestión de Comisiones — Edu-Space{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex align-items-center justify-content-between mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 fw-bold mb-1">
|
||||
<i class="bi bi-diagram-3 me-2 text-primary"></i>Gestión de Comisiones
|
||||
</h1>
|
||||
<p class="text-muted mb-0 small">Asignación de docentes e inscripción de alumnos por comisión</p>
|
||||
</div>
|
||||
<a href="/admin/subjects_list" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-book me-1"></i>Ver Asignaturas
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
|
||||
|
||||
<!-- Filters Card -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body py-3">
|
||||
<form method="GET" class="row g-2 align-items-end" id="filter-form">
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label small fw-semibold mb-1">Buscar</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" name="search" class="form-control" placeholder="Código o asignatura…"
|
||||
value="{{ search or '' }}" id="search-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<label class="form-label small fw-semibold mb-1">Asignatura</label>
|
||||
<select name="subject_id" class="form-select form-select-sm" id="subject-filter">
|
||||
<option value="">Todas</option>
|
||||
{% for subj in subjects %}
|
||||
<option value="{{ subj.id }}" {% if subject_filter == subj.id %}selected{% endif %}>
|
||||
{{ subj.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label small fw-semibold mb-1">Turno</label>
|
||||
<select name="shift" class="form-select form-select-sm" id="shift-filter">
|
||||
<option value="">Todos</option>
|
||||
{% for shift in ['Mañana', 'Tarde', 'Vespertino', 'Noche'] %}
|
||||
<option value="{{ shift }}" {% if shift_filter == shift %}selected{% endif %}>{{ shift }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label small fw-semibold mb-1">Estado</label>
|
||||
<select name="active" class="form-select form-select-sm" id="active-filter">
|
||||
<option value="true" {% if active_filter == 'true' %}selected{% endif %}>Activas</option>
|
||||
<option value="false" {% if active_filter == 'false' %}selected{% endif %}>Inactivas</option>
|
||||
<option value="" {% if not active_filter %}selected{% endif %}>Todas</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-md-2 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm flex-fill" id="filter-btn">
|
||||
<i class="bi bi-funnel me-1"></i>Filtrar
|
||||
</button>
|
||||
<a href="/admin/commissions_list" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-x"></i>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Commissions Table -->
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-transparent border-bottom d-flex align-items-center justify-content-between py-2">
|
||||
<span class="fw-semibold small">
|
||||
<i class="bi bi-list-ul me-1 text-primary"></i>
|
||||
{{ pagination.total }} comisión(es) encontrada(s)
|
||||
</span>
|
||||
<span class="badge bg-primary-subtle text-primary">Página {{ pagination.page }} / {{ pagination.pages or 1 }}</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if commissions %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle" id="commissions-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="ps-3">Comisión</th>
|
||||
<th>Asignatura</th>
|
||||
<th>Docente</th>
|
||||
<th class="text-center">Turno</th>
|
||||
<th class="text-center">Alumnos</th>
|
||||
<th class="text-center">Estado</th>
|
||||
<th class="text-end pe-3">Acción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for comm in commissions %}
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<span class="badge bg-primary-subtle text-primary fw-semibold font-monospace">
|
||||
{{ comm.get_full_code() }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold">{{ comm.subject.name }}</div>
|
||||
<small class="text-muted">{{ comm.subject.code }} — {{ comm.semester }} {{ comm.year }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if comm.teacher %}
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="avatar-xs bg-info-subtle rounded-circle d-flex align-items-center justify-content-center" style="width:28px;height:28px;">
|
||||
<i class="bi bi-person-video3 small text-info"></i>
|
||||
</div>
|
||||
<span class="small">{{ comm.teacher.name }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-muted small fst-italic"><i class="bi bi-exclamation-circle me-1 text-warning"></i>Sin asignar</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if comm.shift %}
|
||||
{% set shift_colors = {'Mañana': 'warning', 'Tarde': 'info', 'Vespertino': 'secondary', 'Noche': 'dark'} %}
|
||||
<span class="badge bg-{{ shift_colors[comm.shift] or 'secondary' }}-subtle text-{{ shift_colors[comm.shift] or 'secondary' }} border border-{{ shift_colors[comm.shift] or 'secondary' }}-subtle">
|
||||
{{ comm.shift }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="fw-semibold">{{ comm.current_students or 0 }}</span>
|
||||
<span class="text-muted small">/ {{ comm.max_students }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if comm.active %}
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">Activa</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">Inactiva</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<a href="/admin/commission_detail?id={{ comm.id }}"
|
||||
class="btn btn-sm btn-outline-primary" title="Ver y gestionar comisión">
|
||||
<i class="bi bi-gear me-1"></i>Gestionar
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="d-flex justify-content-center py-3">
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if pagination.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="/admin/commissions_list">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for pg in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if pg %}
|
||||
<li class="page-item {% if pg == pagination.page %}active{% endif %}">
|
||||
<a class="page-link" href="/admin/commissions_list">{{ pg }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if pagination.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="/admin/commissions_list">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-diagram-3 display-4 text-muted opacity-50"></i>
|
||||
<p class="mt-3 text-muted">No se encontraron comisiones con los filtros aplicados.</p>
|
||||
<a href="/admin/commissions_list" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>Limpiar filtros
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,242 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Import from Google Sheets - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-file-earmark-spreadsheet text-success me-2"></i>Google Sheets Academic Sync
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">Extract and synchronize classrooms, careers, subjects, and weekly schedules directly from the official Google Sheet.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/main/dashboard" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics Cards -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-6 col-xl-2">
|
||||
<div class="card border-0 shadow-sm text-center p-3 h-100">
|
||||
<div class="text-primary mb-1"><i class="bi bi-mortarboard fs-3"></i></div>
|
||||
<div class="h4 mb-0 fw-bold">{{ metrics.total_careers }}</div>
|
||||
<small class="text-muted">Careers</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-2">
|
||||
<div class="card border-0 shadow-sm text-center p-3 h-100">
|
||||
<div class="text-info mb-1"><i class="bi bi-book fs-3"></i></div>
|
||||
<div class="h4 mb-0 fw-bold">{{ metrics.total_subjects }}</div>
|
||||
<small class="text-muted">Subjects</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-2">
|
||||
<div class="card border-0 shadow-sm text-center p-3 h-100">
|
||||
<div class="text-success mb-1"><i class="bi bi-door-open fs-3"></i></div>
|
||||
<div class="h4 mb-0 fw-bold">{{ metrics.total_classrooms }}</div>
|
||||
<small class="text-muted">Classrooms</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card border-0 shadow-sm text-center p-3 h-100">
|
||||
<div class="text-warning mb-1"><i class="bi bi-people fs-3"></i></div>
|
||||
<div class="h4 mb-0 fw-bold">{{ metrics.total_commissions }}</div>
|
||||
<small class="text-muted">Commissions</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card border-0 shadow-sm text-center p-3 h-100">
|
||||
<div class="text-danger mb-1"><i class="bi bi-calendar-check fs-3"></i></div>
|
||||
<div class="h4 mb-0 fw-bold">{{ metrics.total_reservations }}</div>
|
||||
<small class="text-muted">Calendar Classes</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Action Banner -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-8 mb-3 mb-lg-0">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle px-3 py-1 me-2 fw-semibold">
|
||||
<i class="bi bi-cloud-check me-1"></i>Google Sheets Live Integration
|
||||
</span>
|
||||
<span class="badge bg-light text-muted border">UniCABA 2026</span>
|
||||
</div>
|
||||
<h4 class="h5 mb-2 fw-bold">Official Schedule Board (Cartelera de Aulas)</h4>
|
||||
<p class="text-muted small mb-2">
|
||||
Reads published CSV sheets for all 6 days of the week, automatically creating or updating academic careers, classrooms, subject codes, commissions, and weekly calendar entries without duplicating records.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for s in metrics.sheets %}
|
||||
<span class="badge bg-body-tertiary text-secondary border small">
|
||||
<i class="bi bi-calendar-event me-1 text-primary"></i>{{ s.name }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 text-lg-end">
|
||||
<form method="POST" id="syncForm" onsubmit="startSync()">
|
||||
<button type="submit" id="syncBtn" class="btn btn-success btn-lg px-4 py-2 shadow fw-semibold">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Sync Data Now
|
||||
</button>
|
||||
</form>
|
||||
<div id="syncSpinner" class="d-none mt-2 text-muted small">
|
||||
<div class="spinner-border spinner-border-sm text-success me-1" role="status"></div>
|
||||
Connecting to Google Sheets and processing records...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Results (if just completed) -->
|
||||
{% if stats %}
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-success bg-opacity-10 text-success py-3">
|
||||
<h5 class="card-title h6 mb-0 fw-bold">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>Synchronization Report
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 text-center mb-3">
|
||||
<div class="col-md-2">
|
||||
<div class="p-2 border rounded bg-body-tertiary">
|
||||
<div class="fw-bold fs-5">{{ stats.total_rows_processed }}</div>
|
||||
<small class="text-muted">Rows Processed</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="p-2 border rounded bg-body-tertiary">
|
||||
<div class="fw-bold fs-5 text-primary">+{{ stats.careers_created }}</div>
|
||||
<small class="text-muted">Careers Created</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="p-2 border rounded bg-body-tertiary">
|
||||
<div class="fw-bold fs-5 text-success">+{{ stats.classrooms_created }}</div>
|
||||
<small class="text-muted">Classrooms Created</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="p-2 border rounded bg-body-tertiary">
|
||||
<div class="fw-bold fs-5 text-info">+{{ stats.subjects_created }}</div>
|
||||
<small class="text-muted">Subjects Created</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="p-2 border rounded bg-body-tertiary">
|
||||
<div class="fw-bold fs-5 text-warning">+{{ stats.commissions_created }}</div>
|
||||
<small class="text-muted">Commissions Created</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="p-2 border rounded bg-body-tertiary">
|
||||
<div class="fw-bold fs-5 text-danger">+{{ stats.reservations_created }}</div>
|
||||
<small class="text-muted">Reservations Created</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if stats.errors and (stats.errors|length > 0) %}
|
||||
<div class="alert alert-warning mb-0 small">
|
||||
<h6 class="alert-heading fw-bold mb-1">Warnings / Errors detected:</h6>
|
||||
<ul class="mb-0 ps-3">
|
||||
{% for err in stats.errors %}
|
||||
<li>{{ err }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Database Preview Table -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-body-tertiary d-flex justify-content-between align-items-center py-3">
|
||||
<h5 class="card-title h6 mb-0">
|
||||
<i class="bi bi-table text-primary me-2"></i>Recently Synchronized Subjects & Comissions
|
||||
</h5>
|
||||
<a href="/schedule/calendar_view" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-calendar3 me-1"></i>View Calendar
|
||||
</a>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-body-tertiary">
|
||||
<tr>
|
||||
<th class="ps-4">Code</th>
|
||||
<th>Subject Name</th>
|
||||
<th>Career</th>
|
||||
<th>Commissions</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in recent_subjects %}
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<span class="badge bg-secondary-subtle text-secondary font-monospace border">
|
||||
{{ s.code }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold">{{ s.name }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-brand small">
|
||||
{{ s.career_obj.name if s.career_obj else (s.department or _('General')) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="small text-muted">
|
||||
{{ s.commissions|length }} commission(s)
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if s.is_active %}
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle small">
|
||||
<i class="bi bi-check-circle me-1"></i>Active
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary small">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-cloud-arrow-down fs-1 d-block mb-2 text-secondary"></i>
|
||||
No subjects in database yet. Click "Sync Data Now" to import.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function startSync() {
|
||||
const btn = document.getElementById('syncBtn');
|
||||
const spinner = document.getElementById('syncSpinner');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>Sincronizando...';
|
||||
}
|
||||
if (spinner) {
|
||||
spinner.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,343 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Gestión de Ocupación y Métricas - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.kpi-card {
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.07);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.kpi-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
.kpi-icon-circle {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.metric-bar-container {
|
||||
height: 10px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--bs-tertiary-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
.metric-bar-fill {
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.6s ease;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 fw-bold mb-1">
|
||||
<i class="bi bi-graph-up-arrow text-primary me-2"></i>Gestión de Ocupación y Métricas de Espacios
|
||||
</h1>
|
||||
<p class="text-muted mb-0 small">
|
||||
Analítica institucional de aprovechamiento de aulas físicas, demanda en Campus Virtual y distribución horaria.
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/classrooms/list_classrooms" class="btn btn-outline-primary">
|
||||
<i class="bi bi-buildings me-1"></i>Aulas Físicas
|
||||
</a>
|
||||
<a href="/classrooms/list_classrooms" class="btn btn-outline-info">
|
||||
<i class="bi bi-globe2 me-1"></i>Campus Virtual (∞)
|
||||
</a>
|
||||
<a href="/schedule/calendar_view" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-calendar-week me-1"></i>Ver Calendario
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1. Fila de KPIs Globales de Ocupación -->
|
||||
<div class="row g-3 mb-4">
|
||||
<!-- Ocupación Hoy -->
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="card kpi-card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.72rem;">Tasa de Ocupación Hoy</span>
|
||||
<h2 class="h3 fw-bold mb-0 text-primary mt-1">{{ avg_day_pct }}%</h2>
|
||||
</div>
|
||||
<div class="kpi-icon-circle bg-primary-subtle text-primary">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-bar-container mt-3">
|
||||
<div class="metric-bar-fill bg-primary" data-width="{{ avg_day_pct }}%"></div>
|
||||
</div>
|
||||
<small class="text-muted mt-2 d-block" style="font-size: 0.72rem;">
|
||||
Promedio sobre 15 hs operativas ({{ physical_rooms_count }} aulas presenciales)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ocupación Semana -->
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="card kpi-card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.72rem;">Ocupación Semanal</span>
|
||||
<h2 class="h3 fw-bold mb-0 text-info mt-1">{{ avg_week_pct }}%</h2>
|
||||
</div>
|
||||
<div class="kpi-icon-circle bg-info-subtle text-info">
|
||||
<i class="bi bi-calendar-week"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-bar-container mt-3">
|
||||
<div class="metric-bar-fill bg-info" data-width="{{ avg_week_pct }}%"></div>
|
||||
</div>
|
||||
<small class="text-muted mt-2 d-block" style="font-size: 0.72rem;">
|
||||
Semana en curso (Lunes a Sábado)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ocupación Mensual -->
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="card kpi-card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.72rem;">Ocupación del Mes</span>
|
||||
<h2 class="h3 fw-bold mb-0 text-success mt-1">{{ avg_month_pct }}%</h2>
|
||||
</div>
|
||||
<div class="kpi-icon-circle bg-success-subtle text-success">
|
||||
<i class="bi bi-calendar3"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-bar-container mt-3">
|
||||
<div class="metric-bar-fill bg-success" data-width="{{ avg_month_pct }}%"></div>
|
||||
</div>
|
||||
<small class="text-muted mt-2 d-block" style="font-size: 0.72rem;">
|
||||
Total mes calendario activo
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clases Totales Dictadas -->
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="card kpi-card border-0 shadow-sm p-3 h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<span class="text-muted small text-uppercase fw-semibold" style="font-size: 0.72rem;">Total Clases en el Mes</span>
|
||||
<h2 class="h3 fw-bold mb-0 text-warning mt-1">{{ total_month_classes }}</h2>
|
||||
</div>
|
||||
<div class="kpi-icon-circle bg-warning-subtle text-warning">
|
||||
<i class="bi bi-mortarboard"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-3 small font-monospace">
|
||||
<span class="badge bg-primary-subtle text-primary border">Físicas: {{ total_month_classes - v_month }}</span>
|
||||
<span class="badge bg-info-subtle text-info border">Virtuales: {{ v_month }}</span>
|
||||
</div>
|
||||
<small class="text-muted mt-2 d-block" style="font-size: 0.72rem;">
|
||||
Reservas y sesiones registradas en el período
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Detalle y Distribución Espacial -->
|
||||
<div class="row g-4 mb-4">
|
||||
<!-- Ocupación por Piso / Nivel -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title h6 fw-bold mb-0">
|
||||
<i class="bi bi-layers text-primary me-2"></i>Tasa de Ocupación por Piso / Nivel
|
||||
</h5>
|
||||
<span class="badge bg-secondary-subtle text-secondary border">Presencial</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
{% for fs in floor_summary %}
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<span class="fw-semibold text-body small">
|
||||
{{ fs.floor }} ({{ fs.count }} aulas)
|
||||
</span>
|
||||
<span class="fw-bold font-monospace small {% if fs.avg_pct > 50 %}text-danger{% elif fs.avg_pct > 25 %}text-warning{% else %}text-success{% endif %}">
|
||||
{{ fs.avg_pct }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric-bar-container">
|
||||
<div class="metric-bar-fill {% if fs.avg_pct > 50 %}bg-danger{% elif fs.avg_pct > 25 %}bg-warning{% else %}bg-success{% endif %}"
|
||||
data-width="{{ fs.avg_pct }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Distribución por Turno -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-body-tertiary border-0 py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title h6 fw-bold mb-0">
|
||||
<i class="bi bi-brightness-high text-warning me-2"></i>Distribución de Clases por Turno
|
||||
</h5>
|
||||
<span class="badge bg-secondary-subtle text-secondary border">Mes Completo</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2 text-center mb-3">
|
||||
<div class="col-3">
|
||||
<div class="p-2 rounded bg-body-tertiary border">
|
||||
<div class="small text-muted text-uppercase fw-semibold" style="font-size: 0.68rem;">Mañana</div>
|
||||
<div class="h5 fw-bold text-primary mb-0">{{ shift_counts.Mañana }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="p-2 rounded bg-body-tertiary border">
|
||||
<div class="small text-muted text-uppercase fw-semibold" style="font-size: 0.68rem;">Tarde</div>
|
||||
<div class="h5 fw-bold text-warning mb-0">{{ shift_counts.Tarde }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="p-2 rounded bg-body-tertiary border">
|
||||
<div class="small text-muted text-uppercase fw-semibold" style="font-size: 0.68rem;">Vespertino</div>
|
||||
<div class="h5 fw-bold text-info mb-0">{{ shift_counts.Vespertino }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="p-2 rounded bg-body-tertiary border">
|
||||
<div class="small text-muted text-uppercase fw-semibold" style="font-size: 0.68rem;">Noche</div>
|
||||
<div class="h5 fw-bold text-danger mb-0">{{ shift_counts.Noche }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campus Virtual Box -->
|
||||
<div class="p-3 rounded bg-primary-subtle border border-primary-subtle">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="fw-bold text-primary mb-1"><i class="bi bi-globe2 me-2"></i>Campus Virtual (A Demanda · ∞)</h6>
|
||||
<p class="small text-muted mb-0">Salas simultáneas sin límite físico de cupo ni colisión.</p>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="badge bg-primary text-white fs-6">{{ v_month }} clases mes</div>
|
||||
<small class="d-block text-muted mt-1">{{ v_today }} hoy · {{ v_week }} semana</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Top Aulas de Mayor Demanda vs Menor Demanda -->
|
||||
<div class="row g-4">
|
||||
<!-- Mayor Demanda -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||
<h5 class="card-title h6 fw-bold mb-0 text-success">
|
||||
<i class="bi bi-arrow-up-circle-fill me-2"></i>Top Aulas con Mayor Demanda y Ocupación
|
||||
</h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0 small">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Aula</th>
|
||||
<th>Piso</th>
|
||||
<th>Capacidad</th>
|
||||
<th>Ocup. Mes</th>
|
||||
<th class="text-end">Acción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rm in top_rooms %}
|
||||
<tr>
|
||||
<td class="fw-bold text-primary">{{ rm.classroom.code }}</td>
|
||||
<td>{{ rm.classroom.floor_display }}</td>
|
||||
<td>{{ rm.classroom.capacity }} asientos</td>
|
||||
<td>
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle font-monospace">
|
||||
{{ rm.month_pct }}% ({{ rm.month_count }} cl.)
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="/schedule/list_reservations"
|
||||
class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 0.72rem;">
|
||||
Ver Reservas
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menor Demanda / Oportunidades -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-body-tertiary border-0 py-3">
|
||||
<h5 class="card-title h6 fw-bold mb-0 text-secondary">
|
||||
<i class="bi bi-arrow-down-circle-fill me-2"></i>Aulas con Mayor Disponibilidad (Menor Uso)
|
||||
</h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0 small">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Aula</th>
|
||||
<th>Piso</th>
|
||||
<th>Capacidad</th>
|
||||
<th>Ocup. Mes</th>
|
||||
<th class="text-end">Acción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rm in low_rooms %}
|
||||
<tr>
|
||||
<td class="fw-bold text-body">{{ rm.classroom.code }}</td>
|
||||
<td>{{ rm.classroom.floor_display }}</td>
|
||||
<td>{{ rm.classroom.capacity }} asientos</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary-subtle text-secondary border font-monospace">
|
||||
{{ rm.month_pct }}% ({{ rm.month_count }} cl.)
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="/schedule/add_reservation?classroom_id={{ rm.classroom.id }}"
|
||||
class="btn btn-sm btn-outline-success py-0 px-2" style="font-size: 0.72rem;">
|
||||
Reservar
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.metric-bar-fill[data-width]').forEach(function(bar) {
|
||||
bar.style.width = bar.getAttribute('data-width');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,405 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Tipificaciones de Hitos y Categorías - Edu-Space{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||
<div>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-1 small">
|
||||
<li class="breadcrumb-item"><a href="/main/dashboard">Dashboard</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Tipificaciones</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="h3 fw-bold mb-1">
|
||||
<i class="bi bi-tags text-warning me-2"></i>Tipificaciones de Hitos Académicos
|
||||
</h1>
|
||||
<p class="text-muted mb-0 small">
|
||||
Catálogo dinámico y configurable de tipos de evaluaciones, parciales, TPs y eventos para el calendario institucional.
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if can_edit %}
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalAddType">
|
||||
<i class="bi bi-plus-lg me-1"></i>Nueva Tipificación
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert / Help Info -->
|
||||
<div class="alert alert-info border-0 shadow-sm d-flex align-items-center mb-4 py-2 px-3">
|
||||
<i class="bi bi-info-circle-fill text-info fs-5 me-3"></i>
|
||||
<div class="small">
|
||||
Las tipificaciones activas aparecen de forma inmediata en el selector de tipo de evento/hito tanto en el portal web como en la app móvil. Puedes desactivar una tipificación sin eliminar registros históricos asociados.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Card -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-body-tertiary">
|
||||
<tr>
|
||||
<th class="ps-4">Tipificación e Insignia</th>
|
||||
<th>Código / Slug</th>
|
||||
<th>Color Hex</th>
|
||||
<th>Descripción</th>
|
||||
<th class="text-center">Orden</th>
|
||||
<th class="text-center">Estado</th>
|
||||
<th class="text-end pe-4">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in types %}
|
||||
<tr id="row-{{ item.id }}">
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="rounded-circle shadow-xs me-3 flex-shrink-0 border"
|
||||
style="width: 22px; height: 22px; background-color: {{ item.color or '#0d6efd' }};"
|
||||
title="{{ item.color }}"></span>
|
||||
<div>
|
||||
<span class="fw-bold text-dark-emphasis">{{ item.name }}</span>
|
||||
<span class="badge rounded-pill text-white ms-2 shadow-xs" style="background-color: {{ item.color or '#0d6efd' }}; font-size: 0.70rem;">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-primary fw-semibold">{{ item.code }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="font-monospace small text-muted">{{ item.color or '#0d6efd' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted small">{{ item.description or '—' }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-body-tertiary text-secondary border font-monospace">#{{ item.sort_order }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if can_edit %}
|
||||
<form action="/admin/milestone_type_toggle" method="POST" class="d-inline">
|
||||
<input type="hidden" name="id" value="{{ item.id }}">
|
||||
<div class="form-check form-switch d-inline-block">
|
||||
<input class="form-check-input status-toggle" type="checkbox" role="switch"
|
||||
onchange="this.form.submit()"
|
||||
{% if item.is_active %}checked{% endif %}
|
||||
title="Cambiar estado">
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
{% if item.is_active %}
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">Activo</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">Inactivo</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if can_edit %}
|
||||
<button type="button" class="btn btn-outline-primary btn-edit"
|
||||
data-id="{{ item.id }}"
|
||||
data-name="{{ item.name }}"
|
||||
data-code="{{ item.code }}"
|
||||
data-color="{{ item.color or '#0d6efd' }}"
|
||||
data-description="{{ item.description or '' }}"
|
||||
data-sort-order="{{ item.sort_order }}"
|
||||
data-is-active="{{ 'true' if item.is_active else 'false' }}"
|
||||
title="Editar Tipificación">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-delete-type"
|
||||
data-id="{{ item.id }}"
|
||||
data-name="{{ item.name }}"
|
||||
data-action-url="/admin/milestone_type_delete"
|
||||
title="Eliminar Tipificación">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-muted small"><i class="bi bi-lock me-1"></i>Solo lectura</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
|
||||
No se han encontrado tipificaciones registradas.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Nueva Tipificación -->
|
||||
{% if can_edit %}
|
||||
<div class="modal fade" id="modalAddType" tabindex="-1" aria-labelledby="modalAddTypeLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-body-tertiary">
|
||||
<h5 class="modal-title fw-bold" id="modalAddTypeLabel">
|
||||
<i class="bi bi-plus-circle text-primary me-2"></i>Nueva Tipificación de Hito
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="/admin/milestone_type_add" method="POST">
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-3">
|
||||
<label for="add_name" class="form-label fw-semibold">Nombre Visible <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="add_name" name="name" placeholder="Ej: Parcial 3, TP Integrador" required>
|
||||
<div class="form-text small">Texto que se mostrará en los selectores y tarjetas.</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-7">
|
||||
<label for="add_code" class="form-label fw-semibold">Código / Identificador</label>
|
||||
<input type="text" class="form-control font-monospace" id="add_code" name="code" placeholder="ej. parcial_3">
|
||||
<div class="form-text small">Slug único (se autogenera si se deja vacío).</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="add_color" class="form-label fw-semibold">Color Distintivo</label>
|
||||
<input type="color" class="form-control form-control-color w-100" id="add_color" name="color" value="#B43E8E" title="Elegir color">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paleta rápida de colores UniCABA -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted mb-1">Paleta Institucional Rápida:</label>
|
||||
<div class="d-flex flex-wrap gap-2" id="presetColorsAdd">
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #B43E8E;" data-color="#B43E8E" title="Magenta UniCABA"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #0d6efd;" data-color="#0d6efd" title="Azul Institucional"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #198754;" data-color="#198754" title="Verde TP"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #fd7e14;" data-color="#fd7e14" title="Naranja Recuperatorio"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #dc3545;" data-color="#dc3545" title="Rojo Final"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #6f42c1;" data-color="#6f42c1" title="Púrpura Especial"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #20c997;" data-color="#20c997" title="Turquesa Cursada"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #6c757d;" data-color="#6c757d" title="Gris Neutro"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="add_description" class="form-label fw-semibold">Descripción / Notas</label>
|
||||
<textarea class="form-control" id="add_description" name="description" rows="2" placeholder="Detalle o alcance de este tipo de evaluación..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="add_sort_order" class="form-label fw-semibold">Orden de Visualización</label>
|
||||
<input type="number" class="form-control" id="add_sort_order" name="sort_order" value="10" min="0">
|
||||
</div>
|
||||
<div class="col-6 d-flex align-items-center pt-3">
|
||||
<div class="form-check form-switch pt-2">
|
||||
<input class="form-check-input" type="checkbox" id="add_is_active" name="is_active" value="1" checked>
|
||||
<label class="form-check-label fw-semibold" for="add_is_active">Activo de inmediato</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-body-tertiary">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save me-1"></i>Crear Tipificación
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Editar Tipificación -->
|
||||
<div class="modal fade" id="modalEditType" tabindex="-1" aria-labelledby="modalEditTypeLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header bg-body-tertiary">
|
||||
<h5 class="modal-title fw-bold" id="modalEditTypeLabel">
|
||||
<i class="bi bi-pencil-square text-primary me-2"></i>Editar Tipificación
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="formEditType" method="POST">
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-3">
|
||||
<label for="edit_name" class="form-label fw-semibold">Nombre Visible <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="edit_name" name="name" required>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-7">
|
||||
<label for="edit_code" class="form-label fw-semibold">Código / Slug <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control font-monospace" id="edit_code" name="code" required>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label for="edit_color" class="form-label fw-semibold">Color Distintivo</label>
|
||||
<input type="color" class="form-control form-control-color w-100" id="edit_color" name="color" title="Elegir color">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paleta rápida de colores en Edición -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted mb-1">Paleta Institucional Rápida:</label>
|
||||
<div class="d-flex flex-wrap gap-2" id="presetColorsEdit">
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #B43E8E;" data-color="#B43E8E" title="Magenta UniCABA"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #0d6efd;" data-color="#0d6efd" title="Azul Institucional"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #198754;" data-color="#198754" title="Verde TP"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #fd7e14;" data-color="#fd7e14" title="Naranja Recuperatorio"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #dc3545;" data-color="#dc3545" title="Rojo Final"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #6f42c1;" data-color="#6f42c1" title="Púrpura Especial"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #20c997;" data-color="#20c997" title="Turquesa Cursada"></button>
|
||||
<button type="button" class="btn btn-sm rounded-circle p-0 border" style="width: 24px; height: 24px; background-color: #6c757d;" data-color="#6c757d" title="Gris Neutro"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="edit_description" class="form-label fw-semibold">Descripción</label>
|
||||
<textarea class="form-control" id="edit_description" name="description" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="edit_sort_order" class="form-label fw-semibold">Orden</label>
|
||||
<input type="number" class="form-control" id="edit_sort_order" name="sort_order" min="0">
|
||||
</div>
|
||||
<div class="col-6 d-flex align-items-center pt-3">
|
||||
<div class="form-check form-switch pt-2">
|
||||
<input class="form-check-input" type="checkbox" id="edit_is_active" name="is_active" value="1">
|
||||
<label class="form-check-label fw-semibold" for="edit_is_active">Activo</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-body-tertiary">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-check-circle me-1"></i>Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Global de Eliminación de Tipificación (fuera de la tabla) -->
|
||||
<div class="modal fade" id="deleteMilestoneTypeModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow-lg" style="border-radius: 14px;">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="badge bg-danger-subtle text-danger p-2 rounded-circle me-2">
|
||||
<i class="bi bi-exclamation-triangle-fill fs-5"></i>
|
||||
</div>
|
||||
<h5 class="modal-title fw-bold text-danger mb-0">Confirmar Eliminación</h5>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-start py-3">
|
||||
<p class="mb-2">¿Está seguro de que desea eliminar la tipificación <strong id="deleteMilestoneTypeName" class="text-primary"></strong>?</p>
|
||||
<p class="text-muted small mb-0"><i class="bi bi-info-circle me-1"></i>Si existen hitos vinculados a este tipo, el sistema lo desactivará automáticamente para preservar el historial.</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<form id="deleteMilestoneTypeForm" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-danger px-3">
|
||||
<i class="bi bi-trash me-1"></i>Eliminar Tipificación
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Autogenerate code slug in Add Modal
|
||||
var addNameInput = document.getElementById('add_name');
|
||||
var addCodeInput = document.getElementById('add_code');
|
||||
if (addNameInput && addCodeInput) {
|
||||
addNameInput.addEventListener('input', function() {
|
||||
if (!addCodeInput.dataset.touched) {
|
||||
var slug = addNameInput.value.toLowerCase()
|
||||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
addCodeInput.value = slug;
|
||||
}
|
||||
});
|
||||
addCodeInput.addEventListener('input', function() {
|
||||
addCodeInput.dataset.touched = "true";
|
||||
});
|
||||
}
|
||||
|
||||
// Color preset clicks for Add Modal
|
||||
document.querySelectorAll('#presetColorsAdd button').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var color = this.getAttribute('data-color');
|
||||
var input = document.getElementById('add_color');
|
||||
if (input) input.value = color;
|
||||
});
|
||||
});
|
||||
|
||||
// Color preset clicks for Edit Modal
|
||||
document.querySelectorAll('#presetColorsEdit button').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var color = this.getAttribute('data-color');
|
||||
var input = document.getElementById('edit_color');
|
||||
if (input) input.value = color;
|
||||
});
|
||||
});
|
||||
|
||||
// Edit modal bindings
|
||||
var modalEditEl = document.getElementById('modalEditType');
|
||||
var modalEdit = modalEditEl ? new bootstrap.Modal(modalEditEl) : null;
|
||||
var formEdit = document.getElementById('formEditType');
|
||||
|
||||
document.querySelectorAll('.btn-edit').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var id = this.getAttribute('data-id');
|
||||
var name = this.getAttribute('data-name');
|
||||
var code = this.getAttribute('data-code');
|
||||
var color = this.getAttribute('data-color');
|
||||
var description = this.getAttribute('data-description');
|
||||
var sortOrder = this.getAttribute('data-sort-order');
|
||||
var isActive = this.getAttribute('data-is-active') === 'true';
|
||||
|
||||
if (formEdit) {
|
||||
formEdit.action = "/admin/milestone_types_list/edit/" + id;
|
||||
document.getElementById('edit_name').value = name;
|
||||
document.getElementById('edit_code').value = code;
|
||||
document.getElementById('edit_color').value = color;
|
||||
document.getElementById('edit_description').value = description;
|
||||
document.getElementById('edit_sort_order').value = sortOrder;
|
||||
document.getElementById('edit_is_active').checked = isActive;
|
||||
if (modalEdit) modalEdit.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete modal bindings
|
||||
var deleteModalEl = document.getElementById('deleteMilestoneTypeModal');
|
||||
var deleteModal = deleteModalEl ? new bootstrap.Modal(deleteModalEl) : null;
|
||||
var deleteNameEl = document.getElementById('deleteMilestoneTypeName');
|
||||
var deleteFormEl = document.getElementById('deleteMilestoneTypeForm');
|
||||
|
||||
document.querySelectorAll('.btn-delete-type').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var name = this.getAttribute('data-name');
|
||||
var actionUrl = this.getAttribute('data-action-url');
|
||||
if (deleteNameEl) deleteNameEl.textContent = name;
|
||||
if (deleteFormEl) deleteFormEl.action = actionUrl;
|
||||
if (deleteModal) deleteModal.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,194 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if role %}Editar Rol: {{ role.name }}{% else %}Nuevo Rol{% endif %} - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-shield-lock text-primary me-2"></i>
|
||||
{% if role %}Editar Rol y Permisos de Acceso{% else %}Crear Rol y Permisos de Acceso{% endif %}
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">Configure permisos granulares por módulo con la matriz de acceso estilo FortiGate.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/roles_list" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Volver a Roles
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
{% if role %}
|
||||
<input type="hidden" name="id" value="{{ role.id }}">
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<!-- Left Column: Role Details -->
|
||||
<div class="col-lg-4 mb-4">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-header bg-body-tertiary py-3">
|
||||
<h5 class="card-title h6 mb-0">
|
||||
<i class="bi bi-person-badge text-primary me-2"></i>Información del Rol
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label fw-semibold">Nombre del Rol <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="name" name="name"
|
||||
value="{{ role.name if role else '' }}" required
|
||||
placeholder="ej. Docente, Operador, Bedelía"
|
||||
{% if role and role.is_system %}readonly title="El nombre del rol del sistema no se puede modificar"{% endif %}>
|
||||
{% if role and role.is_system %}
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>Rol del sistema protegido.
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label fw-semibold">Descripción</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3"
|
||||
placeholder="Describa las responsabilidades o alcance de este rol...">{{ role.description if role else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary py-2 fw-semibold">
|
||||
<i class="bi bi-check-lg me-1"></i>Guardar Rol y Permisos
|
||||
</button>
|
||||
<a href="/admin/roles_list" class="btn btn-outline-secondary">
|
||||
Cancelar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: FortiGate Permissions Matrix -->
|
||||
<div class="col-lg-8 mb-4">
|
||||
<div class="fg-permission-card">
|
||||
<!-- Matrix Header -->
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold text-primary">
|
||||
<i class="bi bi-grid-3x3-gap-fill me-2"></i>Permisos de Acceso
|
||||
</span>
|
||||
|
||||
<!-- Set All Action Dropdown (FortiGate style) -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-fg-set-all dropdown-toggle" type="button" id="setAllDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-lightning-charge me-1"></i>Establecer Todos
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="setAllDropdown">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item py-2" onclick="setAllPermissions('none')">
|
||||
<i class="bi bi-slash-circle text-danger me-2"></i>Establecer Todos en Ninguno
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item py-2" onclick="setAllPermissions('read')">
|
||||
<i class="bi bi-eye text-info me-2"></i>Establecer Todos en Solo Lectura
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item py-2" onclick="setAllPermissions('read_write')">
|
||||
<i class="bi bi-pencil text-success me-2"></i>Establecer Todos en Lectura y Escritura
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Matrix Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="fg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 48%;">Módulo del Sistema</th>
|
||||
<th style="width: 52%;" class="text-end pe-4">Nivel de Permiso</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for mod in modules %}
|
||||
{% set cur_level = (current_perms[mod.id] if current_perms else (role.permissions[mod.id] if (role and role.permissions) else 'none')) or 'none' %}
|
||||
<tr data-module-row="{{ mod.id }}">
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="bg-primary bg-opacity-10 text-primary rounded p-2 me-3 d-inline-flex align-items-center justify-content-center" style="width: 36px; height: 36px;">
|
||||
<i class="bi {{ mod.icon }} fs-6"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-semibold">{{ mod.name }}</div>
|
||||
<small class="text-muted d-block">{{ mod.description }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<!-- Hidden Input to submit via POST -->
|
||||
<input type="hidden" name="perm_{{ mod.id }}" id="input_perm_{{ mod.id }}" value="{{ cur_level }}">
|
||||
|
||||
<!-- FortiGate Segmented Control -->
|
||||
<div class="fg-segmented-control" role="group" aria-label="Nivel de permiso para {{ mod.name }}">
|
||||
<button type="button"
|
||||
class="fg-seg-btn {% if cur_level == 'none' %}active{% endif %}"
|
||||
data-val="none"
|
||||
data-target="input_perm_{{ mod.id }}"
|
||||
onclick="selectPerm(this, 'input_perm_{{ mod.id }}', 'none')">
|
||||
<i class="bi bi-slash-circle"></i> Ninguno
|
||||
</button>
|
||||
<button type="button"
|
||||
class="fg-seg-btn {% if cur_level == 'read' %}active{% endif %}"
|
||||
data-val="read"
|
||||
data-target="input_perm_{{ mod.id }}"
|
||||
onclick="selectPerm(this, 'input_perm_{{ mod.id }}', 'read')">
|
||||
<i class="bi bi-eye"></i> Lectura
|
||||
</button>
|
||||
<button type="button"
|
||||
class="fg-seg-btn {% if cur_level == 'read_write' %}active{% endif %}"
|
||||
data-val="read_write"
|
||||
data-target="input_perm_{{ mod.id }}"
|
||||
onclick="selectPerm(this, 'input_perm_{{ mod.id }}', 'read_write')">
|
||||
<i class="bi bi-pencil"></i> Lectura/Escritura
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function selectPerm(btn, inputId, value) {
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
// Toggle active state in this segmented control
|
||||
const parentControl = btn.closest('.fg-segmented-control');
|
||||
if (parentControl) {
|
||||
parentControl.querySelectorAll('.fg-seg-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function setAllPermissions(level) {
|
||||
document.querySelectorAll('.fg-segmented-control').forEach(control => {
|
||||
const targetBtn = control.querySelector(`button[data-val="${level}"]`);
|
||||
if (targetBtn) {
|
||||
targetBtn.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,153 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Roles y Grupos de Acceso - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-person-badge text-primary me-2"></i>Roles y Grupos de Acceso
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">Administración de perfiles de usuario y matriz granular de permisos por módulo del sistema.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/role_add" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i>Nuevo Rol
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roles Grid / Table -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-body-tertiary">
|
||||
<tr>
|
||||
<th class="ps-4">Nombre del Rol</th>
|
||||
<th>Descripción</th>
|
||||
<th>Integrantes</th>
|
||||
<th>Resumen de Permisos</th>
|
||||
<th class="text-end pe-4">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for role in roles %}
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="badge-brand rounded-circle p-2 me-3 d-inline-flex align-items-center justify-content-center" style="width: 38px; height: 38px;">
|
||||
<i class="bi bi-shield-check fs-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold text-primary">{{ role.name }}</div>
|
||||
{% if role.is_system %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border small">Protegido del Sistema</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted small">{{ role.description or _('Sin descripción provista.') }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary-subtle text-primary border px-2 py-1">
|
||||
<i class="bi bi-people me-1"></i>{{ role.user_count or (role.users and role.users|length) or 0 }} usuarios
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for mod in modules %}
|
||||
{% set p_level = role.permissions[mod.id] if role.permissions else 'none' %}
|
||||
{% if p_level == 'read_write' %}
|
||||
<span class="badge bg-success-subtle text-success border small" title="{{ mod.name }}: Lectura y Escritura">
|
||||
<i class="bi {{ mod.icon }} me-1"></i>{{ mod.name }}: RW
|
||||
</span>
|
||||
{% elif p_level == 'read' %}
|
||||
<span class="badge bg-info-subtle text-info border small" title="{{ mod.name }}: Solo Lectura">
|
||||
<i class="bi {{ mod.icon }} me-1"></i>{{ mod.name }}: R
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-muted border small opacity-75" title="{{ mod.name }}: Sin Acceso">
|
||||
<i class="bi {{ mod.icon }} me-1"></i>{{ mod.name }}: -
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="/admin/roles/edit/{{ role.id }}" class="btn btn-outline-primary" title="Configurar Permisos">
|
||||
<i class="bi bi-pencil-square me-1"></i>Permisos
|
||||
</a>
|
||||
{% if not role.is_system %}
|
||||
<button type="button" class="btn btn-outline-danger btn-delete-role"
|
||||
data-role-id="{{ role.id }}"
|
||||
data-role-name="{{ role.name }}"
|
||||
data-action-url="/admin/role_delete"
|
||||
title="Eliminar Rol"
|
||||
{% if (role.user_count or 0) > 0 or (role.users and role.users|length > 0) %}disabled{% endif %}>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Global de Eliminación fuera de la tabla (evita desplazamientos visuales) -->
|
||||
<div class="modal fade" id="deleteRoleModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow-lg" style="border-radius: 14px;">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="badge bg-danger-subtle text-danger p-2 rounded-circle me-2">
|
||||
<i class="bi bi-exclamation-triangle-fill fs-5"></i>
|
||||
</div>
|
||||
<h5 class="modal-title fw-bold text-danger mb-0">Confirmar Eliminación de Rol</h5>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-start py-3">
|
||||
<p class="mb-2">¿Está seguro de que desea eliminar el rol <strong id="deleteRoleName" class="text-primary"></strong>?</p>
|
||||
<p class="text-muted small mb-0"><i class="bi bi-info-circle me-1"></i>Esta acción no se puede deshacer.</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<form id="deleteRoleForm" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-danger px-3">
|
||||
<i class="bi bi-trash me-1"></i>Eliminar Rol
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var deleteModalEl = document.getElementById('deleteRoleModal');
|
||||
var deleteModal = deleteModalEl ? new bootstrap.Modal(deleteModalEl) : null;
|
||||
var deleteRoleName = document.getElementById('deleteRoleName');
|
||||
var deleteRoleForm = document.getElementById('deleteRoleForm');
|
||||
|
||||
document.querySelectorAll('.btn-delete-role').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var roleName = this.getAttribute('data-role-name');
|
||||
var actionUrl = this.getAttribute('data-action-url');
|
||||
if (deleteRoleName) deleteRoleName.textContent = roleName;
|
||||
if (deleteRoleForm) deleteRoleForm.action = actionUrl;
|
||||
if (deleteModal) deleteModal.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,308 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Gestión de Asignaturas - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 fw-bold mb-1">
|
||||
<i class="bi bi-book text-primary me-2"></i>Gestión de Asignaturas y Materias
|
||||
</h1>
|
||||
<p class="text-muted mb-0 small">
|
||||
Catálogo institucional de asignaturas, códigos, créditos académicos y carreras vinculadas.
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if can_edit %}
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalAddSubject">
|
||||
<i class="bi bi-plus-lg me-1"></i>Nueva Asignatura
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="/schedule/commissions_view" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-diagram-3 me-1"></i>Ver Comisiones
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Bar -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body py-2">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<!-- Buscador -->
|
||||
<div class="col-md-4">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-body-tertiary"><i class="bi bi-search"></i></span>
|
||||
<input type="text" name="search" class="form-control"
|
||||
placeholder="Buscar por código, nombre o departamento..."
|
||||
value="{{ search }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtro Carrera -->
|
||||
<div class="col-md-4">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-body-tertiary"><i class="bi bi-mortarboard text-primary"></i></span>
|
||||
<select name="career_id" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">Todas las carreras</option>
|
||||
{% for c in careers %}
|
||||
<option value="{{ c.id }}" {% if career_filter == c.id %}selected{% endif %}>{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtro Estado -->
|
||||
<div class="col-md-2">
|
||||
<select name="active" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">Todos los estados</option>
|
||||
<option value="true" {% if active_filter == 'true' %}selected{% endif %}>Solo activas</option>
|
||||
<option value="false" {% if active_filter == 'false' %}selected{% endif %}>Solo inactivas</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 text-md-end">
|
||||
<button type="submit" class="btn btn-sm btn-primary me-1">
|
||||
<i class="bi bi-funnel me-1"></i>Filtrar
|
||||
</button>
|
||||
<a href="/admin/subjects_list" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-x-circle me-1"></i>Limpiar
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subjects Table -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Nombre de la Asignatura</th>
|
||||
<th>Carrera</th>
|
||||
<th>Departamento</th>
|
||||
<th>Créditos</th>
|
||||
<th>Comisiones</th>
|
||||
<th>Estado</th>
|
||||
{% if can_edit %}
|
||||
<th class="text-end">Acciones</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in subjects %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge font-monospace bg-primary-subtle text-primary border">{{ s.code }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-bold text-body">{{ s.name }}</span>
|
||||
{% if s.description %}
|
||||
<small class="text-muted d-block text-truncate" style="max-width: 300px;">{{ s.description }}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if s.career_obj %}
|
||||
<span class="badge bg-secondary-subtle text-secondary border">
|
||||
<i class="bi bi-mortarboard me-1"></i>{{ s.career_obj.name }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">General</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="small text-muted">{{ s.department or '-' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-light text-dark border">{{ s.credits }} cr.</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info-subtle text-info border">
|
||||
<i class="bi bi-diagram-3 me-1"></i>{{ s.commissions|length if s.commissions else 0 }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge rounded-pill {% if s.is_active %}bg-success-subtle text-success border border-success-subtle{% else %}bg-secondary-subtle text-secondary border{% endif %}">
|
||||
{% if s.is_active %}Activa{% else %}Inactiva{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
{% if can_edit %}
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary py-0 px-2"
|
||||
data-bs-toggle="modal" data-bs-target="#modalEditSubject{{ s.id }}"
|
||||
title="Editar Asignatura">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<form method="POST" action="/admin/subject_toggle" class="d-inline">
|
||||
<input type="hidden" name="id" value="{{ s.id }}">
|
||||
<button type="submit" class="btn btn-outline-warning py-0 px-2"
|
||||
title="{% if s.is_active %}Desactivar{% else %}Activar{% endif %}">
|
||||
<i class="bi {% if s.is_active %}bi-pause{% else %}bi-play{% endif %}"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Editar Asignatura -->
|
||||
<div class="modal fade text-start" id="modalEditSubject{{ s.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/subject_edit">
|
||||
<input type="hidden" name="id" value="{{ s.id }}">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title h6 fw-bold">
|
||||
<i class="bi bi-pencil-square text-primary me-2"></i>Editar Asignatura
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label small fw-semibold">Código *</label>
|
||||
<input type="text" name="code" class="form-control form-control-sm font-monospace" value="{{ s.code }}" required>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label small fw-semibold">Créditos</label>
|
||||
<input type="number" name="credits" class="form-control form-control-sm" value="{{ s.credits }}" min="1" max="20">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Nombre de la Asignatura *</label>
|
||||
<input type="text" name="name" class="form-control form-control-sm" value="{{ s.name }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Carrera Vinculada</label>
|
||||
<select name="career_id" class="form-select form-select-sm">
|
||||
<option value="">Sin carrera específica (General)</option>
|
||||
{% for c in careers %}
|
||||
<option value="{{ c.id }}" {% if s.career_id == c.id %}selected{% endif %}>{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Departamento / Área</label>
|
||||
<input type="text" name="department" class="form-control form-control-sm" value="{{ s.department or '' }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Descripción / Objetivos</label>
|
||||
<textarea name="description" class="form-control form-control-sm" rows="2">{{ s.description or '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" id="activeSubj{{ s.id }}" {% if s.is_active %}checked{% endif %}>
|
||||
<label class="form-check-label small" for="activeSubj{{ s.id }}">Asignatura Activa</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Guardar Cambios</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-book fs-1 d-block mb-2"></i>
|
||||
No se encontraron asignaturas con los criterios indicados.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Paginación -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="card-footer bg-body-tertiary border-0 py-2">
|
||||
<nav aria-label="Subjects pagination">
|
||||
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||
{% if pagination.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="/admin/subjects_list">Anterior</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for p in pagination.iter_pages(left_edge=2, right_edge=2, left_current=2, right_current=2) %}
|
||||
{% if p %}
|
||||
<li class="page-item {% if p == pagination.page %}active{% endif %}">
|
||||
<a class="page-link" href="/admin/subjects_list">{{ p }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="/admin/subjects_list">Siguiente</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Nueva Asignatura -->
|
||||
{% if can_edit %}
|
||||
<div class="modal fade" id="modalAddSubject" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/subject_add">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title h6 fw-bold">
|
||||
<i class="bi bi-book text-primary me-2"></i>Nueva Asignatura
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label small fw-semibold">Código *</label>
|
||||
<input type="text" name="code" class="form-control form-control-sm font-monospace" placeholder="ej. ASIG0015" required>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label small fw-semibold">Créditos</label>
|
||||
<input type="number" name="credits" class="form-control form-control-sm" value="4" min="1" max="20">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Nombre de la Asignatura *</label>
|
||||
<input type="text" name="name" class="form-control form-control-sm" placeholder="ej. Álgebra Lineal" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Carrera Vinculada</label>
|
||||
<select name="career_id" class="form-select form-select-sm">
|
||||
<option value="">Sin carrera específica (General)</option>
|
||||
{% for c in careers %}
|
||||
<option value="{{ c.id }}">{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Departamento / Área</label>
|
||||
<input type="text" name="department" class="form-control form-control-sm" placeholder="ej. Ciencias Exactas, Informática">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-semibold">Descripción</label>
|
||||
<textarea name="description" class="form-control form-control-sm" rows="2" placeholder="Programa de la materia..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Crear Asignatura</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,110 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if user %}Edit User: {{ user.name }}{% else %}New User{% endif %} - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-person-gear text-primary me-2"></i>
|
||||
{% if user %}Edit User Account{% else %}Create User Account{% endif %}
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">Configure user credentials, profile information, and role assignment.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/users_list" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back to Users
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-body-tertiary py-3">
|
||||
<h5 class="card-title h6 mb-0">
|
||||
<i class="bi bi-person-vcard text-primary me-2"></i>Account Details
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form method="POST">
|
||||
{% if user %}
|
||||
<input type="hidden" name="id" value="{{ user.id }}">
|
||||
{% endif %}
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label fw-semibold">Full Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="name" name="name"
|
||||
value="{{ user.name if user else '' }}" required
|
||||
placeholder="e.g. María González">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label fw-semibold">Email Address <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
value="{{ user.email if user else '' }}" required
|
||||
placeholder="e.g. mgonzalez@edu-space.com">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label fw-semibold">
|
||||
Password {% if not user %}<span class="text-danger">*</span>{% endif %}
|
||||
</label>
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
{% if not user %}required{% endif %}
|
||||
placeholder="{% if user %}Leave empty to keep current password{% else %}At least 6 characters{% endif %}">
|
||||
{% if user %}
|
||||
<small class="text-muted d-block mt-1">
|
||||
Only fill this in if you want to reset this user's password.
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="role_id" class="form-label fw-semibold">Role / Access Group <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="role_id" name="role_id" required>
|
||||
<option value="" disabled {% if not user or not user.role_id %}selected{% endif %}>Select a role...</option>
|
||||
{% for r in roles %}
|
||||
<option value="{{ r.id }}" {% if user and user.role_id == r.id %}selected{% endif %}>
|
||||
{{ r.name }} - {{ r.description or '' }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="text-muted d-block mt-1">
|
||||
<a href="/admin/roles_list" target="_blank" class="text-decoration-none">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i>View or manage roles and permissions
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="is_active" name="is_active"
|
||||
{% if not user or user.is_active %}checked{% endif %}
|
||||
{% if user and user.id == user.id %}disabled{% endif %}>
|
||||
<label class="form-check-label fw-semibold" for="is_active">
|
||||
Active Account
|
||||
</label>
|
||||
<small class="text-muted d-block">
|
||||
Inactive users are prevented from signing in to the platform.
|
||||
</small>
|
||||
{% if user and user.id == user.id %}
|
||||
<input type="hidden" name="is_active" value="on">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-2">
|
||||
<a href="/admin/users_list" class="btn btn-outline-secondary">
|
||||
Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary px-4 fw-semibold">
|
||||
<i class="bi bi-check-lg me-1"></i>Save User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,248 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}User Management - Edu-Space Admin{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid pt-5 mt-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="bi bi-people text-primary me-2"></i>User Management
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">Manage user accounts, assign roles, and control active status.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/user_add" class="btn btn-primary">
|
||||
<i class="bi bi-person-plus me-1"></i>New User
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters & Search -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-3">
|
||||
<form method="GET" class="row g-2 align-items-center">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-body-tertiary border-end-0">
|
||||
<i class="bi bi-search text-muted"></i>
|
||||
</span>
|
||||
<input type="text" name="search" class="form-control border-start-0"
|
||||
placeholder="Search by name or email..."
|
||||
value="{{ search }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="role" class="form-select form-select-sm">
|
||||
<option value="">All Roles</option>
|
||||
{% for r in roles %}
|
||||
<option value="{{ r.id }}" {% if role_filter == (r.id|string) %}selected{% endif %}>{{ r.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select name="status" class="form-select form-select-sm">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="active" {% if status_filter == 'active' %}selected{% endif %}>Active</option>
|
||||
<option value="inactive" {% if status_filter == 'inactive' %}selected{% endif %}>Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100">
|
||||
<i class="bi bi-filter me-1"></i>Filter
|
||||
</button>
|
||||
{% if search or role_filter or status_filter %}
|
||||
<a href="/admin/users_list" class="btn btn-outline-secondary btn-sm" title="Clear filters">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-body-tertiary">
|
||||
<tr>
|
||||
<th class="ps-4">User</th>
|
||||
<th>Email</th>
|
||||
<th>Role / Group</th>
|
||||
<th>Status</th>
|
||||
<th>Last Login</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="user-avatar-small me-2">
|
||||
{{ (u.first_name[0] if u.first_name else 'U')|upper }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ u.name }}</div>
|
||||
<small class="text-muted">ID: #{{ u.id }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="font-monospace small">{{ u.email }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if u.role_obj %}
|
||||
<span class="badge badge-brand">
|
||||
<i class="bi bi-shield-check me-1"></i>{{ u.role_obj.name }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">
|
||||
{{ u.role or _('No Role') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if u.is_active %}
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle px-2 py-1">
|
||||
<i class="bi bi-check-circle me-1"></i>Active
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger-subtle text-danger border border-danger-subtle px-2 py-1">
|
||||
<i class="bi bi-dash-circle me-1"></i>Inactive
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{% if u.last_login %}
|
||||
{{ format_datetime(u.last_login, format='short') }}
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</small>
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="/admin/users/edit/{{ u.id }}" class="btn btn-outline-primary" title="Edit User">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% if u.id != user.id %}
|
||||
<form action="/admin/user_toggle" method="POST" class="d-inline">
|
||||
<input type="hidden" name="id" value="{{ u.id }}">
|
||||
<button type="submit" class="btn {% if u.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}"
|
||||
title="{% if u.is_active %}Deactivate User{% else %}Activate User{% endif %}">
|
||||
<i class="bi {% if u.is_active %}bi-person-x{% else %}bi-person-check{% endif %}"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn btn-outline-danger btn-delete-user"
|
||||
data-user-id="{{ u.id }}"
|
||||
data-user-name="{{ u.name }}"
|
||||
data-user-email="{{ u.email }}"
|
||||
data-action-url="/admin/user_delete"
|
||||
title="Eliminar Usuario">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-people fs-1 d-block mb-2 text-secondary"></i>
|
||||
No users found matching the criteria.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="card-footer bg-body-tertiary d-flex justify-content-between align-items-center py-2 px-4">
|
||||
<small class="text-muted">
|
||||
Showing page {{ pagination.page }} of {{ pagination.pages }} ({{ pagination.total }} total)
|
||||
</small>
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if pagination.has_prev %}
|
||||
<li class="page-item"><a class="page-link" href="/admin/users_list">«</a></li>
|
||||
{% endif %}
|
||||
{% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if p %}
|
||||
<li class="page-item {% if p == pagination.page %}active{% endif %}">
|
||||
<a class="page-link" href="/admin/users_list">{{ p }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if pagination.has_next %}
|
||||
<li class="page-item"><a class="page-link" href="/admin/users_list">»</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Global de Eliminación de Usuario (fuera de la tabla) -->
|
||||
<div class="modal fade" id="deleteUserModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow-lg" style="border-radius: 14px;">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="badge bg-danger-subtle text-danger p-2 rounded-circle me-2">
|
||||
<i class="bi bi-person-x-fill fs-5"></i>
|
||||
</div>
|
||||
<h5 class="modal-title fw-bold text-danger mb-0">Confirmar Eliminación de Usuario</h5>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-start py-3">
|
||||
<p class="mb-2">¿Está seguro de que desea eliminar permanentemente al siguiente usuario?</p>
|
||||
<div class="p-2 rounded bg-body-tertiary mb-2">
|
||||
<div class="fw-bold text-primary" id="deleteUserName"></div>
|
||||
<div class="small text-muted font-monospace" id="deleteUserEmail"></div>
|
||||
</div>
|
||||
<p class="text-muted small mb-0"><i class="bi bi-exclamation-triangle me-1 text-warning"></i>Esta acción desvinculará sus comisiones y reservas asociadas. No se puede deshacer.</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<form id="deleteUserForm" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-danger px-3">
|
||||
<i class="bi bi-trash me-1"></i>Eliminar Usuario
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var deleteModalEl = document.getElementById('deleteUserModal');
|
||||
var deleteModal = deleteModalEl ? new bootstrap.Modal(deleteModalEl) : null;
|
||||
var nameEl = document.getElementById('deleteUserName');
|
||||
var emailEl = document.getElementById('deleteUserEmail');
|
||||
var formEl = document.getElementById('deleteUserForm');
|
||||
|
||||
document.querySelectorAll('.btn-delete-user').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var name = this.getAttribute('data-user-name');
|
||||
var email = this.getAttribute('data-user-email');
|
||||
var actionUrl = this.getAttribute('data-action-url');
|
||||
if (nameEl) nameEl.textContent = name;
|
||||
if (emailEl) emailEl.textContent = email;
|
||||
if (formEl) formEl.action = actionUrl;
|
||||
if (deleteModal) deleteModal.show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user