feat(admin): Registro de transacciones — log de auditoría de todo el sistema

NUEVA INFRAESTRUCTURA
* SQL 010: tabla audit_log (id, fecha, user_email, user_name, modulo,
  accion, entity_type, entity_id, entity_label, detalles JSON, ip_addr)
  con índices por fecha/user/modulo/accion/entity.
* src/AuditLog.php: helper estático con dos métodos:
  - log() lee actor de $_SESSION (uso normal post-auth)
  - logAs() recibe email/name explícito (para login/logout y CLI)
  Falla silenciosamente si la DB no responde — no rompe la operación
  principal por un fallo de logging.

HOOKS EN MUTACIONES
* Auth.php: registra 'login' al completar OAuth y 'logout' antes de
  destruir la sesión.
* escrituras/api.php: registra create/update/delete con id+nombre.
* tramites/api.php: registra 8 mutaciones — create/update/delete del
  trámite, set_status (con nuevo estatus en detalles), add_obs,
  add_hours/update_hours/delete_hours (con abogado y horas en
  detalles), todos referenciados por codigo (ADM-001, etc.).
* admin/api.php: registra create_user/update_user/delete_user, con
  diff de cambios en update_user.
* GETs (list_*) no se loguean — serían ruido.

LECTOR
* admin/api.php nuevo endpoint GET ?action=list_log que devuelve
  entradas con filtros from/to/user/modulo/accion/search (LIKE)
  + catálogos para dropdowns. Límite 500 por query.

UI EN PANEL DE CONTROL
* admin.html ahora tiene tabs: "Usuarios" (existente) y "Registro de
  transacciones" (nuevo).
* Tab Registro: toolbar de filtros (rango fechas, usuario, módulo,
  acción, búsqueda libre, atajos Hoy/Semana/Mes/Todo) + tabla con
  Fecha·Hora · Usuario · Módulo (pill coloreado) · Acción · Entidad
  con label y tipo · Detalles JSON resumido.
This commit is contained in:
Soporte ZH Consultores
2026-06-04 20:30:46 -05:00
parent 2f2d20604b
commit cd7595832f
7 changed files with 403 additions and 5 deletions
+35 -3
View File
@@ -3,6 +3,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../../src/Auth.php';
require_once __DIR__ . '/../../src/Db.php';
require_once __DIR__ . '/../../src/AuditLog.php';
if (($_GET['action'] ?? '') === 'logout') {
header('Location: /auth/logout.php');
@@ -227,6 +228,8 @@ try {
if ($myId > 0 && !in_array($myId, $involIds, true)) $involIds[] = $myId;
syncInvolucrados($pdo, $id, $involIds, $user['email']);
AuditLog::log('tramites', 'create', 'tramite_legal', $codigo, $codigo, ['departamento'=>$dept, 'cliente'=>$b['cliente']??'']);
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$row->execute([':id' => $id]);
ok(hydrate($pdo, $row->fetch()));
@@ -290,6 +293,8 @@ try {
syncInvolucrados($pdo, $id, array_map('intval', $b['involucrados']), $user['email']);
}
AuditLog::log('tramites', 'update', 'tramite_legal', $before['codigo'], $before['codigo']);
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$row->execute([':id' => $id]);
ok(hydrate($pdo, $row->fetch()));
@@ -303,14 +308,16 @@ try {
if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403);
$cur = $pdo->prepare('SELECT departamento FROM tramites_legales WHERE id = :id');
$cur = $pdo->prepare('SELECT departamento, codigo FROM tramites_legales WHERE id = :id');
$cur->execute([':id' => $id]);
$dept = $cur->fetchColumn();
if (!$dept) err('Trámite no encontrado', 404);
$curRow = $cur->fetch();
if (!$curRow) err('Trámite no encontrado', 404);
$dept = $curRow['departamento'];
if (!in_array($val, STATUS_MAP[$dept], true)) err('Estatus inválido para el departamento');
$pdo->prepare('UPDATE tramites_legales SET estatus = :s, actualizado_por = :u WHERE id = :id')
->execute([':s' => $val, ':u' => $user['email'], ':id' => $id]);
AuditLog::log('tramites', 'set_status', 'tramite_legal', $curRow['codigo'], $curRow['codigo'], ['estatus'=>$val]);
ok(['id' => $id, 'estatus' => $val]);
}
@@ -330,6 +337,11 @@ try {
$pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id')
->execute([':u' => $user['email'], ':id' => $id]);
$codSel = $pdo->prepare('SELECT codigo FROM tramites_legales WHERE id = :id');
$codSel->execute([':id' => $id]);
$codigo = (string)$codSel->fetchColumn();
AuditLog::log('tramites', 'add_obs', 'tramite_legal', $codigo, $codigo, ['abogado'=>$abog, 'texto'=>$texto]);
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$row->execute([':id' => $id]);
ok(hydrate($pdo, $row->fetch()));
@@ -352,6 +364,11 @@ try {
$pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id')
->execute([':u' => $user['email'], ':id' => $id]);
$codSel = $pdo->prepare('SELECT codigo FROM tramites_legales WHERE id = :id');
$codSel->execute([':id' => $id]);
$codigo = (string)$codSel->fetchColumn();
AuditLog::log('tramites', 'add_hours', 'tramite_legal', $codigo, $codigo, ['abogado'=>$abog, 'hrs'=>$hrs]);
$row = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$row->execute([':id' => $id]);
ok(hydrate($pdo, $row->fetch()));
@@ -383,6 +400,11 @@ try {
$pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id')
->execute([':u' => $user['email'], ':id' => $tramiteId]);
$codSel = $pdo->prepare('SELECT codigo FROM tramites_legales WHERE id = :id');
$codSel->execute([':id' => $tramiteId]);
$codigo = (string)$codSel->fetchColumn();
AuditLog::log('tramites', 'update_hours', 'hour', (string)$hourId, $codigo, ['abogado'=>$abog, 'hrs'=>$hrs]);
$r = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$r->execute([':id' => $tramiteId]);
ok(hydrate($pdo, $r->fetch()));
@@ -405,6 +427,11 @@ try {
$pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id')
->execute([':u' => $user['email'], ':id' => $tramiteId]);
$codSel = $pdo->prepare('SELECT codigo FROM tramites_legales WHERE id = :id');
$codSel->execute([':id' => $tramiteId]);
$codigo = (string)$codSel->fetchColumn();
AuditLog::log('tramites', 'delete_hours', 'hour', (string)$hourId, $codigo);
$r = $pdo->prepare('SELECT * FROM tramites_legales WHERE id = :id');
$r->execute([':id' => $tramiteId]);
ok(hydrate($pdo, $r->fetch()));
@@ -418,7 +445,12 @@ try {
if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403);
$codSel = $pdo->prepare('SELECT codigo FROM tramites_legales WHERE id = :id');
$codSel->execute([':id' => $id]);
$codigo = (string)$codSel->fetchColumn();
$pdo->prepare('DELETE FROM tramites_legales WHERE id = :id')->execute([':id' => $id]);
AuditLog::log('tramites', 'delete', 'tramite_legal', $codigo, $codigo);
ok(['deleted' => $id]);
}