feat(perms): permisología completa con 3 roles (Socio/Abogado/Usuario) + Panel de Control

ESQUEMA (migraciones 005 + 006)
* users: agrega role ENUM('socio','abogado','usuario'), apellido, activo, orden
* Seed: cabrego@aal-law.com como socio + 15 placeholders por apellido para
  poblar el dropdown "Abogado Solicitante" hasta que los reales se logueen.
* tramites_involucrados: tabla N:M (tramite_id, user_id) con CASCADE delete.

AUTH (src/Auth.php)
* userRow() cachea el row del usuario actual por request.
* role(), isSocio(), isAbogado(), canCreate(), canDelete(), requireSocio().
* completeLogin() retorna bool: false si user.activo=0 → callback muestra
  pantalla "cuenta deshabilitada" en lugar de loguear.

BOOTSTRAP (src/Bootstrap.php + src/Lookups.php)
* Bootstrap::inject(view) reemplaza readfile en los wrappers; inyecta
  <script>window.DONNA={me,name,role,isSocio,isAbogado,canCreate,canDelete,
  abogados,users}</script> antes de </head> para que el frontend tenga
  contexto sin requests extra.

TRAMITES — visibilidad + involucrados
* api.php: list filtra por (socio | creador | abogado solicitante mapped via
  users.apellido | involucrado) excepto socios que ven todo. Todas las
  mutaciones validan canView(). create/delete validan rol. Persiste
  involucrados vía syncInvolucrados().
* tramites.html: nueva columna "Involucrados" después de "Abogado", chip
  picker multi-select en el modal, gear icon (oculto a non-socios), oculta
  botones "+ Nuevo" y ✕ para usuarios sin permisos. Auto-incluye al
  creador como involucrado por defecto.
* Calendario y demás apps consumen el API filtrado → heredan la
  visibilidad sin código nuevo.

PANEL DE CONTROL (/admin/)
* index.php: requireSocio() + Bootstrap injection.
* api.php: list_users, create_user, update_user. Salvaguardas: el socio
  no puede demoverse ni desactivarse a sí mismo.
* admin.html: tabla unificada de usuarios con filtros, modal CRUD,
  resaltado de fila propia, placeholders identificados visualmente.

GEAR ICON
* Agregado en escrituras.html, tramites.html, calendario.html con
  display:none por defecto; JS lo revela si window.DONNA.isSocio.
* Link a /admin/.
This commit is contained in:
Soporte ZH Consultores
2026-06-04 23:14:07 +00:00
parent cd9dae98bb
commit 2db8f23ba5
16 changed files with 1088 additions and 37 deletions
+20 -1
View File
@@ -77,6 +77,15 @@ header {
transition:all .15s;
}
.btn-logout:hover { background:rgba(255,255,255,0.1); color:#fff; }
.btn-gear {
display:inline-flex; align-items:center; justify-content:center;
width:34px; height:34px; border:1px solid rgba(255,255,255,0.18);
border-radius:8px; color:rgba(255,255,255,0.85);
transition:all .15s; text-decoration:none; cursor:pointer;
font-family:'DM Sans',sans-serif;
}
.btn-gear:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); }
.btn-gear svg { width:16px; height:16px; }
/* BACK TO PORTAL */
.header-left { display:flex; align-items:center; gap:14px; }
@@ -460,6 +469,9 @@ tr.editing-row td textarea.editable {
<div class="user-avatar" id="user-avatar">?</div>
<span class="user-name" id="user-name">Cargando…</span>
</div>
<a class="btn-gear" href="/admin/" id="btn-gear" title="Panel de Control" style="display:none;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</a>
<button class="btn-logout" onclick="doLogout()">Cerrar sesión</button>
</div>
</header>
@@ -601,7 +613,14 @@ tr.editing-row td textarea.editable {
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
const ABOGADOS = ['Arias','Abrego','Lopez','Altafulla','Álvarez','Gutiérrez','Cedeño','Mariscal','Ávila','Agudo','Martínez','De Gracia','Góndola','González','Valdés'];
const ABOGADOS = (window.DONNA && window.DONNA.abogados) || ['Arias','Abrego','Lopez','Altafulla','Álvarez','Gutiérrez','Cedeño','Mariscal','Ávila','Agudo','Martínez','De Gracia','Góndola','González','Valdés'];
// Gear icon visible solo para socios
if (window.DONNA && window.DONNA.isSocio) {
document.addEventListener('DOMContentLoaded', () => {
const g = document.getElementById('btn-gear');
if (g) g.style.display = 'inline-flex';
});
}
const ESTATUS = ['Solicitado','Recibido','En Proceso','Protocolizado','En Registro','Inscrito'];
const API = 'api.php';
const POLL_MS = 15000;