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
+16
View File
@@ -54,6 +54,11 @@ header { background:var(--navy); border-bottom:3px solid var(--gold); padding:0
.btn-logout { font-size:11px; padding:4px 10px; border:1px solid rgba(255,255,255,.2); border-radius:6px;
background:transparent; color:rgba(255,255,255,.7); transition:all .15s; }
.btn-logout:hover { background:rgba(255,255,255,.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; }
.cal-toolbar { background:#fff; border-bottom:1px solid var(--gray-100); padding:12px 24px;
display:flex; align-items:center; gap:14px; flex-wrap:wrap; box-shadow:var(--shadow); }
@@ -273,6 +278,9 @@ footer.legend .pill { display:inline-block; padding:2px 8px; border-radius:20px;
<div class="user-avatar" id="user-avatar">?</div>
<span class="user-name" id="user-name">…</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>
<a class="btn-logout" href="/auth/logout.php" onclick="return confirm('¿Cerrar sesión?');">Cerrar sesión</a>
</div>
</header>
@@ -363,6 +371,14 @@ let search = '';
let selected = null;
let authUser = { email:'', name:'' };
// Gear icon solo visible para socios (window.DONNA inyectado por Bootstrap)
if (window.DONNA && window.DONNA.isSocio) {
document.addEventListener('DOMContentLoaded', () => {
const g = document.getElementById('btn-gear');
if (g) g.style.display = 'inline-flex';
});
}
function $(id){ return document.getElementById(id); }
function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function stripTime(d){ const o=new Date(d); o.setHours(0,0,0,0); return o; }