feat: gestion RBAC granular, importacion sheets, filtrado de aulas virtuales y pisos, capacidad infinita y traduccion
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% if role %}{% trans %}Editar Rol{% endtrans %}: {{ role.name }}{% else %}{% trans %}Nuevo Rol{% endtrans %}{% 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 %}{% trans %}Editar Rol y Permisos de Acceso{% endtrans %}{% else %}{% trans %}Crear Rol y Permisos de Acceso{% endtrans %}{% endif %}
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">{% trans %}Configure permisos granulares por módulo con la matriz de acceso estilo FortiGate.{% endtrans %}</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url_for('admin.roles_list') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>{% trans %}Volver a Roles{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<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>{% trans %}Información del Rol{% endtrans %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label fw-semibold">{% trans %}Nombre del Rol{% endtrans %} <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="name" name="name"
|
||||
value="{{ role.name if role else '' }}" required
|
||||
placeholder="{% trans %}ej. Docente, Operador, Bedelía{% endtrans %}"
|
||||
{% if role and role.is_system %}readonly title="{% trans %}El nombre del rol del sistema no se puede modificar{% endtrans %}"{% endif %}>
|
||||
{% if role and role.is_system %}
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="bi bi-info-circle me-1"></i>{% trans %}Rol del sistema protegido.{% endtrans %}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label fw-semibold">{% trans %}Descripción{% endtrans %}</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3"
|
||||
placeholder="{% trans %}Describa las responsabilidades o alcance de este rol...{% endtrans %}">{{ 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>{% trans %}Guardar Rol y Permisos{% endtrans %}
|
||||
</button>
|
||||
<a href="{{ url_for('admin.roles_list') }}" class="btn btn-outline-secondary">
|
||||
{% trans %}Cancelar{% endtrans %}
|
||||
</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>{% trans %}Permisos de Acceso{% endtrans %}
|
||||
</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>{% trans %}Establecer Todos{% endtrans %}
|
||||
</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>{% trans %}Establecer Todos en Ninguno{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item py-2" onclick="setAllPermissions('read')">
|
||||
<i class="bi bi-eye text-info me-2"></i>{% trans %}Establecer Todos en Solo Lectura{% endtrans %}
|
||||
</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>{% trans %}Establecer Todos en Lectura y Escritura{% endtrans %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Matrix Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="fg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 48%;">{% trans %}Módulo del Sistema{% endtrans %}</th>
|
||||
<th style="width: 52%;" class="text-end pe-4">{% trans %}Nivel de Permiso{% endtrans %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for mod in modules %}
|
||||
{% set cur_level = current_perms.get(mod.id, '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> {% trans %}Ninguno{% endtrans %}
|
||||
</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> {% trans %}Lectura{% endtrans %}
|
||||
</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> {% trans %}Lectura/Escritura{% endtrans %}
|
||||
</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,124 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% trans %}Roles y Grupos de Acceso{% endtrans %} - 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>{% trans %}Roles y Grupos de Acceso{% endtrans %}
|
||||
</h1>
|
||||
<p class="text-muted small mb-0">{% trans %}Administración de perfiles de usuario y matriz granular de permisos por módulo del sistema.{% endtrans %}</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url_for('admin.role_add') }}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i>{% trans %}Nuevo Rol{% endtrans %}
|
||||
</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">{% trans %}Nombre del Rol{% endtrans %}</th>
|
||||
<th>{% trans %}Descripción{% endtrans %}</th>
|
||||
<th>{% trans %}Integrantes{% endtrans %}</th>
|
||||
<th>{% trans %}Resumen de Permisos{% endtrans %}</th>
|
||||
<th class="text-end pe-4">{% trans %}Acciones{% endtrans %}</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">{% trans %}Protegido del Sistema{% endtrans %}</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.users|length }} {% trans %}usuarios{% endtrans %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for mod in modules %}
|
||||
{% set p_level = role.get_permission(mod.id) %}
|
||||
{% if p_level == 'read_write' %}
|
||||
<span class="badge bg-success-subtle text-success border small" title="{{ mod.name }}: {% trans %}Lectura y Escritura{% endtrans %}">
|
||||
<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 }}: {% trans %}Solo Lectura{% endtrans %}">
|
||||
<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 }}: {% trans %}Sin Acceso{% endtrans %}">
|
||||
<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="{{ url_for('admin.role_edit', id=role.id) }}" class="btn btn-outline-primary" title="{% trans %}Configurar Permisos{% endtrans %}">
|
||||
<i class="bi bi-pencil-square me-1"></i>{% trans %}Permisos{% endtrans %}
|
||||
</a>
|
||||
{% if not role.is_system %}
|
||||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ role.id }}" title="{% trans %}Eliminar Rol{% endtrans %}" {% if role.users|length > 0 %}disabled{% endif %}>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not role.is_system %}
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal{{ role.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<div class="modal-header border-0">
|
||||
<h5 class="modal-title text-danger">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>{% trans %}Confirmar Eliminación de Rol{% endtrans %}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-start">
|
||||
<p>{% trans %}¿Está seguro de que desea eliminar el rol{% endtrans %} <strong>{{ role.name }}</strong>?</p>
|
||||
<p class="text-muted small mb-0">{% trans %}Esta acción no se puede deshacer.{% endtrans %}</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans %}Cancelar{% endtrans %}</button>
|
||||
<form action="{{ url_for('admin.role_delete', id=role.id) }}" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-danger">{% trans %}Eliminar Rol{% endtrans %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user