Files
donna/public/admin/api.php
T
Soporte ZH Consultores 2db8f23ba5 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/.
2026-06-04 23:14:07 +00:00

133 lines
5.1 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/../../src/Auth.php';
require_once __DIR__ . '/../../src/Db.php';
require_once __DIR__ . '/../../src/Lookups.php';
Auth::requireSocio();
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
const ROLES = ['socio','abogado','usuario'];
function ok($data = null): void {
echo json_encode(['ok' => true, 'data' => $data, 'user' => Auth::user()], JSON_UNESCAPED_UNICODE);
exit;
}
function err(string $msg, int $code = 400): void {
http_response_code($code);
echo json_encode(['ok' => false, 'error' => $msg], JSON_UNESCAPED_UNICODE);
exit;
}
function body(): array {
return json_decode((string) file_get_contents('php://input'), true) ?? [];
}
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
$me = Auth::userId();
$pdo = Db::pdo();
try {
if ($method === 'GET' && $action === 'list_users') {
ok(Lookups::allUsers());
}
if ($method === 'POST' && $action === 'create_user') {
$b = body();
$email = strtolower(trim((string)($b['email'] ?? '')));
if ($email === '') err('email requerido');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) err('email inválido');
$name = trim((string)($b['name'] ?? ''));
$role = in_array(($b['role'] ?? ''), ROLES, true) ? $b['role'] : 'usuario';
$apellido = trim((string)($b['apellido'] ?? ''));
$activo = (int)($b['activo'] ?? 1) === 1 ? 1 : 0;
$orden = (int)($b['orden'] ?? 0);
$exists = $pdo->prepare('SELECT id FROM users WHERE email = :e');
$exists->execute([':e' => $email]);
if ($exists->fetch()) err('Ya existe un usuario con ese email', 409);
$ins = $pdo->prepare(
'INSERT INTO users (email, name, role, apellido, activo, orden, first_seen_at, last_login_at)
VALUES (:email, :name, :role, :apellido, :activo, :orden, NULL, NULL)'
);
$ins->execute([
':email' => $email, ':name' => $name, ':role' => $role,
':apellido' => $apellido, ':activo' => $activo, ':orden' => $orden,
]);
$id = (int)$pdo->lastInsertId();
$sel = $pdo->prepare('SELECT id, email, name, role, apellido, activo, orden, first_seen_at, last_login_at FROM users WHERE id = :id');
$sel->execute([':id' => $id]);
$row = $sel->fetch();
$row['id'] = (int)$row['id'];
$row['activo'] = (int)$row['activo'];
$row['orden'] = (int)$row['orden'];
ok($row);
}
if ($method === 'PUT' && $action === 'update_user') {
$b = body();
$id = (int)($b['_id'] ?? 0);
if (!$id) err('id requerido');
$sel = $pdo->prepare('SELECT id, email, role, activo FROM users WHERE id = :id');
$sel->execute([':id' => $id]);
$existing = $sel->fetch();
if (!$existing) err('Usuario no encontrado', 404);
$newRole = in_array(($b['role'] ?? $existing['role']), ROLES, true) ? $b['role'] : $existing['role'];
$newActivo = isset($b['activo']) ? ((int)$b['activo'] === 1 ? 1 : 0) : (int)$existing['activo'];
// Salvaguardas: el socio no puede demoverse ni desactivarse a sí mismo
if ((int)$existing['id'] === $me) {
if ($newRole !== 'socio') err('No puedes quitarte el rol de Socio a ti mismo', 400);
if ($newActivo === 0) err('No puedes desactivarte a ti mismo', 400);
}
$newEmail = isset($b['email']) ? strtolower(trim((string)$b['email'])) : $existing['email'];
if (!filter_var($newEmail, FILTER_VALIDATE_EMAIL)) err('email inválido');
if ($newEmail !== $existing['email']) {
$chk = $pdo->prepare('SELECT id FROM users WHERE email = :e AND id != :id');
$chk->execute([':e' => $newEmail, ':id' => $id]);
if ($chk->fetch()) err('Otro usuario ya tiene ese email', 409);
}
$name = trim((string)($b['name'] ?? ''));
$apellido = trim((string)($b['apellido'] ?? ''));
$orden = (int)($b['orden'] ?? 0);
$upd = $pdo->prepare(
'UPDATE users SET email = :email, name = :name, role = :role,
apellido = :apellido, activo = :activo, orden = :orden
WHERE id = :id'
);
$upd->execute([
':email' => $newEmail, ':name' => $name, ':role' => $newRole,
':apellido' => $apellido, ':activo' => $newActivo, ':orden' => $orden,
':id' => $id,
]);
$sel = $pdo->prepare('SELECT id, email, name, role, apellido, activo, orden, first_seen_at, last_login_at FROM users WHERE id = :id');
$sel->execute([':id' => $id]);
$row = $sel->fetch();
$row['id'] = (int)$row['id'];
$row['activo'] = (int)$row['activo'];
$row['orden'] = (int)$row['orden'];
ok($row);
}
err('Acción no reconocida', 404);
} catch (PDOException $e) {
error_log('admin db error: ' . $e->getMessage());
err('Error de base de datos', 500);
} catch (Throwable $e) {
error_log('admin error: ' . $e->getMessage());
err('Error interno', 500);
}