feat: catalogo dinamico de tipificaciones de hitos, matriz RBAC y desacoplamiento en modal de eventos

This commit is contained in:
Carlos Tello
2026-09-03 03:48:49 -03:00
parent 0c867e0031
commit 291e537574
13 changed files with 777 additions and 23 deletions
@@ -0,0 +1,322 @@
{% extends "base.html" %}
{% block title %}{% trans %}Tipificaciones de Hitos y Categorías{% endtrans %} - Edu-Space{% endblock %}
{% block content %}
<div class="container-fluid py-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="{{ url_for('main.dashboard') }}">{% trans %}Dashboard{% endtrans %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% trans %}Tipificaciones{% endtrans %}</li>
</ol>
</nav>
<h1 class="h3 fw-bold mb-1">
<i class="bi bi-tags text-warning me-2"></i>{% trans %}Tipificaciones de Hitos Académicos{% endtrans %}
</h1>
<p class="text-muted mb-0">
{% trans %}Catálogo dinámico y configurable de tipos de evaluaciones, parciales, TPs y eventos para el calendario institucional.{% endtrans %}
</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>{% trans %}Nueva Tipificación{% endtrans %}
</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">
{% trans %}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.{% endtrans %}
</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" style="width: 70px;">{% trans %}Color{% endtrans %}</th>
<th>{% trans %}Nombre Visible{% endtrans %}</th>
<th>{% trans %}Código / Slug{% endtrans %}</th>
<th>{% trans %}Descripción{% endtrans %}</th>
<th class="text-center">{% trans %}Orden{% endtrans %}</th>
<th class="text-center">{% trans %}Estado{% endtrans %}</th>
<th class="text-end pe-4">{% trans %}Acciones{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for item in types %}
<tr id="row-{{ item.id }}">
<td class="ps-4">
<span class="d-inline-block rounded-circle shadow-sm border border-light"
style="width: 26px; height: 26px; background-color: {{ item.color or '#0d6efd' }}; vertical-align: middle;"
title="{{ item.color }}"></span>
</td>
<td>
<div class="fw-bold fs-6">{{ item.name }}</div>
<span class="badge rounded-pill text-white shadow-xs" style="background-color: {{ item.color or '#0d6efd' }}; font-size: 0.72rem;">
{{ item.name }}
</span>
</td>
<td>
<code class="text-primary fw-semibold">{{ item.code }}</code>
</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="{{ url_for('admin.milestone_type_toggle', id=item.id) }}" method="POST" class="d-inline">
<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="{% trans %}Cambiar estado{% endtrans %}">
</div>
</form>
{% else %}
{% if item.is_active %}
<span class="badge bg-success-subtle text-success border border-success-subtle">{% trans %}Activo{% endtrans %}</span>
{% else %}
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">{% trans %}Inactivo{% endtrans %}</span>
{% endif %}
{% endif %}
</td>
<td class="text-end pe-4">
{% if can_edit %}
<button type="button" class="btn btn-sm btn-outline-primary py-1 px-2 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' }}">
<i class="bi bi-pencil-square me-1"></i>{% trans %}Editar{% endtrans %}
</button>
{% else %}
<span class="text-muted small"><i class="bi bi-lock me-1"></i>{% trans %}Solo lectura{% endtrans %}</span>
{% endif %}
</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>
{% trans %}No se han encontrado tipificaciones registradas.{% endtrans %}
</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>{% trans %}Nueva Tipificación de Hito{% endtrans %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{{ url_for('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">{% trans %}Nombre Visible{% endtrans %} <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">{% trans %}Texto que se mostrará en los selectores y tarjetas.{% endtrans %}</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-7">
<label for="add_code" class="form-label fw-semibold">{% trans %}Código / Identificador{% endtrans %}</label>
<input type="text" class="form-control font-monospace" id="add_code" name="code" placeholder="ej. parcial_3">
<div class="form-text small">{% trans %}Slug único (se autogenera si se deja vacío).{% endtrans %}</div>
</div>
<div class="col-md-5">
<label for="add_color" class="form-label fw-semibold">{% trans %}Color Distintivo{% endtrans %}</label>
<div class="input-group">
<input type="color" class="form-control form-control-color w-100" id="add_color" name="color" value="#0d6efd" title="Elegir color">
</div>
</div>
</div>
<!-- Paleta rápida de colores UniCABA -->
<div class="mb-3">
<label class="form-label small text-muted d-block mb-1">{% trans %}Colores sugeridos{% endtrans %}:</label>
<div class="d-flex gap-2 flex-wrap" id="presetColorsAdd">
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #0d6efd;" data-color="#0d6efd"></button>
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #198754;" data-color="#198754"></button>
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #fd7e14;" data-color="#fd7e14"></button>
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #ffc107;" data-color="#ffc107"></button>
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #dc3545;" data-color="#dc3545"></button>
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #6f42c1;" data-color="#6f42c1"></button>
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #0dcaf0;" data-color="#0dcaf0"></button>
<button type="button" class="btn p-0 rounded-circle border" style="width: 24px; height: 24px; background-color: #6c757d;" data-color="#6c757d"></button>
</div>
</div>
<div class="mb-3">
<label for="add_description" class="form-label fw-semibold">{% trans %}Descripción{% endtrans %}</label>
<textarea class="form-control" id="add_description" name="description" rows="2" placeholder="Opcional: detalles sobre el 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">{% trans %}Orden de Aparición{% endtrans %}</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">{% trans %}Activo{% endtrans %}</label>
</div>
</div>
</div>
</div>
<div class="modal-footer bg-body-tertiary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans %}Cancelar{% endtrans %}</button>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-check-circle me-1"></i>{% trans %}Crear Tipificación{% endtrans %}
</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>{% trans %}Editar Tipificación{% endtrans %}
</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">{% trans %}Nombre Visible{% endtrans %} <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">{% trans %}Código / Slug{% endtrans %} <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">{% trans %}Color Distintivo{% endtrans %}</label>
<input type="color" class="form-control form-control-color w-100" id="edit_color" name="color" title="Elegir color">
</div>
</div>
<div class="mb-3">
<label for="edit_description" class="form-label fw-semibold">{% trans %}Descripción{% endtrans %}</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">{% trans %}Orden{% endtrans %}</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">{% trans %}Activo{% endtrans %}</label>
</div>
</div>
</div>
</div>
<div class="modal-footer bg-body-tertiary">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans %}Cancelar{% endtrans %}</button>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-check-circle me-1"></i>{% trans %}Guardar Cambios{% endtrans %}
</button>
</div>
</form>
</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
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;
});
});
// 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 = "{{ url_for('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();
}
});
});
});
</script>
{% endblock %}
+1
View File
@@ -107,6 +107,7 @@
<li><a class="dropdown-item" href="{{ url_for('admin.users_list') }}"><i class="bi bi-people me-2"></i>{% trans %}Users{% endtrans %}</a></li>
<li><a class="dropdown-item" href="{{ url_for('admin.roles_list') }}"><i class="bi bi-person-badge me-2"></i>{% trans %}Roles & Groups{% endtrans %}</a></li>
<li><a class="dropdown-item" href="{{ url_for('buildings.list_buildings') }}"><i class="bi bi-buildings me-2"></i>{% trans %}Buildings{% endtrans %}</a></li>
<li><a class="dropdown-item" href="{{ url_for('admin.milestone_types_list') }}"><i class="bi bi-tags me-2 text-warning"></i>{% trans %}Tipificaciones{% endtrans %}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('admin.google_sheets_import') }}"><i class="bi bi-file-earmark-spreadsheet me-2 text-success"></i>{% trans %}Import from Google Sheets{% endtrans %}</a></li>
</ul>