From 0a8dc6fc468346feb0adb64593f65f3b0d17e13f Mon Sep 17 00:00:00 2001 From: Soporte ZH Consultores Date: Thu, 4 Jun 2026 22:33:41 -0500 Subject: [PATCH] refactor(hours): FK user_id en tramites_hours y tramites_obs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hasta ahora el vínculo "esta hora/observación es de X" era por string (h.abogado = users.name). Se rompía cuando un Socio renombraba un usuario en el Panel de Control (las entradas históricas conservaban el nombre viejo y los filtros del Reporte de Horas no las hallaban). - Migración 014: ADD COLUMN user_id INT NULL + FK ON DELETE SET NULL en ambas tablas, con backfill via JOIN users.name = abogado - tramites/api.php: helper userIdByName() + add_obs/add_hours/ update_hours ahora persisten user_id junto con el string abogado - horas/api.php: filtro WHERE (user_id = :uid OR (user_id IS NULL AND abogado = :name)) — prioriza FK, cae al string solo para filas legacy sin user_id. by_user y by_user_tramite agrupan por COALESCE(u.name, h.abogado) para usar siempre el nombre actual. --- public/horas/api.php | 58 ++++++++++++++++++---------- public/tramites/api.php | 24 +++++++++--- sql/014_add_user_id_to_hours_obs.sql | 42 ++++++++++++++++++++ 3 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 sql/014_add_user_id_to_hours_obs.sql diff --git a/public/horas/api.php b/public/horas/api.php index 0de3744..0725b56 100644 --- a/public/horas/api.php +++ b/public/horas/api.php @@ -26,23 +26,26 @@ try { $to = clean_date($_GET['to'] ?? null); $userParam = isset($_GET['user']) ? (int)$_GET['user'] : 0; - // Determinar por qué nombre filtrar: - // - Socio/admin: por user seleccionado, o todos si user=0 - // - Resto: forzado a sus propias horas (matched por su name) - $filterName = null; + // Determinar por qué usuario filtrar: + // - Socio/admin: por user seleccionado (id+nombre), o todos si user=0 + // - Resto: forzado a su propio user_id (con fallback a su name para legacy) + $filterUserId = null; + $filterName = null; if (Auth::isSocio()) { if ($userParam > 0) { - $sel = $pdo->prepare('SELECT name FROM users WHERE id = :id'); + $sel = $pdo->prepare('SELECT id, name FROM users WHERE id = :id'); $sel->execute([':id' => $userParam]); - $name = $sel->fetchColumn(); - if ($name !== false && $name !== '') $filterName = (string)$name; + $u = $sel->fetch(); + if ($u) { $filterUserId = (int)$u['id']; $filterName = (string)$u['name']; } } } else { - $filterName = Auth::userName(); - if ($filterName === '') { + $filterUserId = Auth::userId(); + $filterName = Auth::userName(); + if ($filterUserId <= 0 && $filterName === '') { ok([ 'entries' => [], 'by_user' => [], + 'by_user_tramite' => [], 'avail_users' => [], 'total_hrs' => 0, 'is_socio' => false, @@ -52,9 +55,16 @@ try { } } + // Filtro por usuario: prefiere user_id (canónico); cae al name solo + // para filas legacy donde user_id quedó NULL. $where = ['1=1']; $params = []; - if ($filterName !== null) { + if ($filterUserId !== null && $filterUserId > 0) { + $where[] = '(h.user_id = :uid OR (h.user_id IS NULL AND h.abogado = :name))'; + $params[':uid'] = $filterUserId; + $params[':name'] = (string)$filterName; + } elseif ($filterName !== null && $filterName !== '') { + // Fallback: solo nombre disponible (no debería ocurrir tras 014) $where[] = 'h.abogado = :name'; $params[':name'] = $filterName; } @@ -102,10 +112,12 @@ try { if ($to) { $byWhere[] = 'h.fecha <= :to'; $byParams[':to'] = $to . ' 23:59:59'; } $whereByUser = implode(' AND ', $byWhere); - $bySql = "SELECT h.abogado AS name, SUM(h.hrs) AS total, COUNT(*) AS entries + $bySql = "SELECT COALESCE(u.name, h.abogado) AS name, + SUM(h.hrs) AS total, COUNT(*) AS entries FROM tramites_hours h + LEFT JOIN users u ON u.id = h.user_id WHERE $whereByUser - GROUP BY h.abogado + GROUP BY COALESCE(u.name, h.abogado) ORDER BY total DESC"; $byStmt = $pdo->prepare($bySql); $byStmt->execute($byParams); @@ -117,16 +129,18 @@ try { unset($bu); // Breakdown por usuario × trámite - $butSql = "SELECT h.abogado AS name, t.id AS tramite_id, t.codigo AS tramite_codigo, + $butSql = "SELECT COALESCE(u.name, h.abogado) AS name, + t.id AS tramite_id, t.codigo AS tramite_codigo, t.departamento AS departamento, COALESCE(NULLIF(t.nombre,''), NULLIF(t.tipo_tramite,''), NULLIF(t.tipo_proceso,''), t.codigo) AS tramite_title, t.cliente AS tramite_cliente, SUM(h.hrs) AS total, COUNT(*) AS entries FROM tramites_hours h JOIN tramites_legales t ON t.id = h.tramite_id + LEFT JOIN users u ON u.id = h.user_id WHERE $whereByUser - GROUP BY h.abogado, t.id - ORDER BY h.abogado ASC, total DESC"; + GROUP BY COALESCE(u.name, h.abogado), t.id + ORDER BY name ASC, total DESC"; $butStmt = $pdo->prepare($butSql); $butStmt->execute($byParams); $byUserTramite = $butStmt->fetchAll(); @@ -136,21 +150,23 @@ try { $r['entries'] = (int)$r['entries']; } unset($r); - } elseif ($filterName !== null) { - // Abogado/usuario regular: su propio desglose por trámite - $butParams = [':name' => $filterName]; - $butWhere = ['h.abogado = :name']; + } elseif ($filterUserId !== null && $filterUserId > 0) { + // Abogado/usuario regular: su propio desglose por trámite (vía user_id + fallback name) + $butParams = [':uid' => $filterUserId, ':name' => (string)$filterName]; + $butWhere = ['(h.user_id = :uid OR (h.user_id IS NULL AND h.abogado = :name))']; if ($from) { $butWhere[] = 'h.fecha >= :from'; $butParams[':from'] = $from . ' 00:00:00'; } if ($to) { $butWhere[] = 'h.fecha <= :to'; $butParams[':to'] = $to . ' 23:59:59'; } - $butSql = "SELECT h.abogado AS name, t.id AS tramite_id, t.codigo AS tramite_codigo, + $butSql = "SELECT COALESCE(u.name, h.abogado) AS name, + t.id AS tramite_id, t.codigo AS tramite_codigo, t.departamento AS departamento, COALESCE(NULLIF(t.nombre,''), NULLIF(t.tipo_tramite,''), NULLIF(t.tipo_proceso,''), t.codigo) AS tramite_title, t.cliente AS tramite_cliente, SUM(h.hrs) AS total, COUNT(*) AS entries FROM tramites_hours h JOIN tramites_legales t ON t.id = h.tramite_id + LEFT JOIN users u ON u.id = h.user_id WHERE " . implode(' AND ', $butWhere) . " - GROUP BY h.abogado, t.id + GROUP BY COALESCE(u.name, h.abogado), t.id ORDER BY total DESC"; $butStmt = $pdo->prepare($butSql); $butStmt->execute($butParams); diff --git a/public/tramites/api.php b/public/tramites/api.php index 1cc527a..0a97332 100644 --- a/public/tramites/api.php +++ b/public/tramites/api.php @@ -128,6 +128,15 @@ function canView(PDO $pdo, int $tid): bool { return $stmt->fetchColumn() !== false; } +/** Resuelve el user_id (activo) a partir del nombre completo; null si no match. */ +function userIdByName(PDO $pdo, string $name): ?int { + if ($name === '') return null; + $stmt = $pdo->prepare('SELECT id FROM users WHERE name = :n AND activo = 1 LIMIT 1'); + $stmt->execute([':n' => $name]); + $id = $stmt->fetchColumn(); + return $id === false ? null : (int)$id; +} + /** Reemplaza la lista de involucrados del trámite por la lista dada (array de user_id). */ function syncInvolucrados(PDO $pdo, int $tid, array $userIds, string $addedBy): void { $pdo->prepare('DELETE FROM tramites_involucrados WHERE tramite_id = :t')->execute([':t' => $tid]); @@ -331,8 +340,9 @@ try { if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403); - $pdo->prepare('INSERT INTO tramites_obs (tramite_id, texto, abogado, autor_email) VALUES (?,?,?,?)') - ->execute([$id, $texto, $abog, $user['email']]); + $uidObs = userIdByName($pdo, $abog); + $pdo->prepare('INSERT INTO tramites_obs (tramite_id, texto, abogado, user_id, autor_email) VALUES (?,?,?,?,?)') + ->execute([$id, $texto, $abog, $uidObs, $user['email']]); $pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id') ->execute([':u' => $user['email'], ':id' => $id]); @@ -358,8 +368,9 @@ try { if (!canView($pdo, $id)) err('No tienes acceso a este trámite', 403); - $pdo->prepare('INSERT INTO tramites_hours (tramite_id, abogado, hrs, descripcion, autor_email) VALUES (?,?,?,?,?)') - ->execute([$id, $abog, $hrs, $desc, $user['email']]); + $uidH = userIdByName($pdo, $abog); + $pdo->prepare('INSERT INTO tramites_hours (tramite_id, abogado, user_id, hrs, descripcion, autor_email) VALUES (?,?,?,?,?,?)') + ->execute([$id, $abog, $uidH, $hrs, $desc, $user['email']]); $pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id') ->execute([':u' => $user['email'], ':id' => $id]); @@ -394,8 +405,9 @@ try { $desc = trim((string)($b['descripcion'] ?? $row['descripcion'])); if ($hrs <= 0) err('horas debe ser > 0'); - $upd = $pdo->prepare('UPDATE tramites_hours SET abogado = :a, hrs = :h, descripcion = :d WHERE id = :id'); - $upd->execute([':a' => $abog, ':h' => $hrs, ':d' => $desc, ':id' => $hourId]); + $uidU = userIdByName($pdo, $abog); + $upd = $pdo->prepare('UPDATE tramites_hours SET abogado = :a, user_id = :uid, hrs = :h, descripcion = :d WHERE id = :id'); + $upd->execute([':a' => $abog, ':uid' => $uidU, ':h' => $hrs, ':d' => $desc, ':id' => $hourId]); $pdo->prepare('UPDATE tramites_legales SET actualizado_por = :u WHERE id = :id') ->execute([':u' => $user['email'], ':id' => $tramiteId]); diff --git a/sql/014_add_user_id_to_hours_obs.sql b/sql/014_add_user_id_to_hours_obs.sql new file mode 100644 index 0000000..c238739 --- /dev/null +++ b/sql/014_add_user_id_to_hours_obs.sql @@ -0,0 +1,42 @@ +-- ============================================================ +-- Donna — Schema 014: FK user_id en tramites_hours y tramites_obs +-- ============================================================ +-- Hasta ahora el vínculo "esta hora/observación es de Fulanito" +-- se hacía por string (h.abogado = users.name). Eso rompe cuando: +-- - Hay variantes legacy (parcial vs completo) → ya arreglado en 013 +-- - Un Socio renombra al usuario en el Panel de Control (las +-- entradas históricas conservan el nombre viejo y los filtros +-- no las encuentran al buscar por el nuevo nombre) +-- +-- Esta migración agrega user_id INT NULL con FK a users(id) y +-- ON DELETE SET NULL (si borran al usuario, la entrada queda con +-- user_id NULL pero conserva el string `abogado` como historia). +-- El backfill solo asocia donde hay match exacto y único. + +-- 1. tramites_hours +ALTER TABLE tramites_hours + ADD COLUMN IF NOT EXISTS user_id INT NULL AFTER abogado, + ADD INDEX IF NOT EXISTS idx_user_id (user_id); + +UPDATE tramites_hours h +JOIN users u ON u.activo = 1 AND u.name = h.abogado +SET h.user_id = u.id +WHERE h.user_id IS NULL; + +ALTER TABLE tramites_hours + ADD CONSTRAINT fk_hours_user + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; + +-- 2. tramites_obs +ALTER TABLE tramites_obs + ADD COLUMN IF NOT EXISTS user_id INT NULL AFTER abogado, + ADD INDEX IF NOT EXISTS idx_user_id (user_id); + +UPDATE tramites_obs o +JOIN users u ON u.activo = 1 AND u.name = o.abogado +SET o.user_id = u.id +WHERE o.user_id IS NULL; + +ALTER TABLE tramites_obs + ADD CONSTRAINT fk_obs_user + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;