diff --git a/public/admin/api.php b/public/admin/api.php index 2331e23..61d363b 100644 --- a/public/admin/api.php +++ b/public/admin/api.php @@ -41,27 +41,26 @@ try { 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); + $name = trim((string)($b['name'] ?? '')); + $role = in_array(($b['role'] ?? ''), ROLES, true) ? $b['role'] : 'usuario'; + $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)' + 'INSERT INTO users (email, name, role, activo, orden, first_seen_at, last_login_at) + VALUES (:email, :name, :role, :activo, :orden, NULL, NULL)' ); $ins->execute([ ':email' => $email, ':name' => $name, ':role' => $role, - ':apellido' => $apellido, ':activo' => $activo, ':orden' => $orden, + ':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 = $pdo->prepare('SELECT id, email, name, role, 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']; @@ -97,22 +96,21 @@ try { 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); + $name = trim((string)($b['name'] ?? '')); + $orden = (int)($b['orden'] ?? 0); $upd = $pdo->prepare( 'UPDATE users SET email = :email, name = :name, role = :role, - apellido = :apellido, activo = :activo, orden = :orden + activo = :activo, orden = :orden WHERE id = :id' ); $upd->execute([ ':email' => $newEmail, ':name' => $name, ':role' => $newRole, - ':apellido' => $apellido, ':activo' => $newActivo, ':orden' => $orden, + ':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 = $pdo->prepare('SELECT id, email, name, role, 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']; diff --git a/public/index.php b/public/index.php index 9865e77..89b3848 100644 --- a/public/index.php +++ b/public/index.php @@ -106,6 +106,9 @@ a{color:inherit;text-decoration:none;} .user-email{font-size:10.5px;color:rgba(255,255,255,0.55);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .btn-logout{font-size:11px;padding:6px 12px;border:1px solid rgba(255,255,255,0.18);border-radius:18px;background:transparent;color:rgba(255,255,255,0.7);cursor:pointer;font-family:'DM Sans',sans-serif;transition:all .15s;} .btn-logout:hover{background:rgba(255,255,255,0.12);color:#fff;border-color:rgba(255,255,255,0.3);} +.btn-gear{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;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;font-family:'DM Sans',sans-serif;margin-right:8px;} +.btn-gear:hover{background:var(--gold);border-color:var(--gold);color:var(--navy);} +.btn-gear svg{width:16px;height:16px;} .dash-main{max-width:1180px;margin:0 auto;padding:48px 32px 80px;} .dash-hero{margin-bottom:40px;} @@ -199,13 +202,20 @@ a{color:inherit;text-decoration:none;} Portal interno -
-
-
- - +
+ + + + + +
+
+ + Cerrar sesión
- Cerrar sesión
diff --git a/public/tramites/api.php b/public/tramites/api.php index 8e0f3e5..9037a4c 100644 --- a/public/tramites/api.php +++ b/public/tramites/api.php @@ -91,7 +91,7 @@ function hydrate(PDO $pdo, array $row): array { }, $hrs->fetchAll()); $inv = $pdo->prepare( - 'SELECT u.id, u.email, u.name, u.apellido + 'SELECT u.id, u.email, u.name FROM tramites_involucrados ti JOIN users u ON u.id = ti.user_id WHERE ti.tramite_id = :t @@ -109,7 +109,7 @@ function hydrate(PDO $pdo, array $row): array { /** * ¿El usuario actual puede ver/editar este trámite? * Socio → siempre - * Resto → si es creador, o abogado solicitante (apellido), o involucrado + * Resto → si es creador, o abogado solicitante (name), o involucrado */ function canView(PDO $pdo, int $tid): bool { if (Auth::isSocio()) return true; @@ -117,7 +117,7 @@ function canView(PDO $pdo, int $tid): bool { $uid = Auth::userId(); $stmt = $pdo->prepare( "SELECT 1 FROM tramites_legales t - LEFT JOIN users ua ON ua.apellido = t.abogado AND ua.activo = 1 + LEFT JOIN users ua ON ua.name = t.abogado AND ua.activo = 1 LEFT JOIN tramites_involucrados ti ON ti.tramite_id = t.id AND ti.user_id = :uid WHERE t.id = :tid AND (t.creado_por = :email OR ua.id = :uid2 OR ti.user_id IS NOT NULL) @@ -153,7 +153,7 @@ try { } else { $stmt = $pdo->prepare( "SELECT DISTINCT t.* FROM tramites_legales t - LEFT JOIN users ua ON ua.apellido = t.abogado AND ua.activo = 1 + LEFT JOIN users ua ON ua.name = t.abogado AND ua.activo = 1 LEFT JOIN tramites_involucrados ti ON ti.tramite_id = t.id AND ti.user_id = :uid WHERE t.creado_por = :email OR ua.id = :uid2 diff --git a/sql/007_drop_apellido.sql b/sql/007_drop_apellido.sql new file mode 100644 index 0000000..a6c31b5 --- /dev/null +++ b/sql/007_drop_apellido.sql @@ -0,0 +1,26 @@ +-- ============================================================ +-- Donna — Schema 007: Eliminar apellido, usar Nombre Completo +-- ============================================================ +-- Decisión: simplificar el modelo trabajando solo con `name` en lugar de +-- mantener apellido + name. El dropdown "Abogado Solicitante" muestra el +-- nombre completo en lugar del apellido suelto. + +-- 1) Migrar nombres de placeholders desde apellido +UPDATE users + SET name = apellido + WHERE (name = '' OR name = '(Asignar email real)') + AND apellido != ''; + +-- 2) Migrar tramites a usar el name en lugar del apellido +UPDATE escrituras_tramites t + JOIN users u ON u.apellido = t.abogado AND u.apellido != '' AND u.name != '' + SET t.abogado = u.name; + +UPDATE tramites_legales t + JOIN users u ON u.apellido = t.abogado AND u.apellido != '' AND u.name != '' + SET t.abogado = u.name; + +-- 3) Drop columna apellido + su índice +ALTER TABLE users + DROP INDEX idx_apellido, + DROP COLUMN apellido; diff --git a/src/Auth.php b/src/Auth.php index b1406f8..75048c4 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -47,7 +47,7 @@ final class Auth { if (!$email) return self::$userRowCache = self::defaultRow(); try { $stmt = Db::pdo()->prepare( - 'SELECT id, email, name, role, apellido, activo + 'SELECT id, email, name, role, activo FROM users WHERE email = :e LIMIT 1' ); $stmt->execute([':e' => $email]); @@ -61,12 +61,12 @@ final class Auth { } private static function defaultRow(): array { - return ['id' => 0, 'email' => '', 'name' => '', 'role' => 'usuario', 'apellido' => '', 'activo' => 1]; + return ['id' => 0, 'email' => '', 'name' => '', 'role' => 'usuario', 'activo' => 1]; } public static function userId(): int { return (int)(self::userRow()['id'] ?? 0); } public static function role(): string { return (string)(self::userRow()['role'] ?? 'usuario'); } - public static function apellido(): string { return (string)(self::userRow()['apellido'] ?? ''); } + public static function userName(): string { return (string)(self::userRow()['name'] ?? ''); } public static function isSocio(): bool { return self::role() === 'socio'; } public static function isAbogado(): bool { return in_array(self::role(), ['socio','abogado'], true); } diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 272994b..8a146b1 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -20,7 +20,7 @@ final class Bootstrap { 'isAbogado' => Auth::isAbogado(), 'canCreate' => Auth::canCreate(), 'canDelete' => Auth::canDelete(), - 'abogados' => Lookups::abogadoApellidos(), + 'abogados' => Lookups::abogadoNames(), 'users' => Lookups::involucrablesUsers(), ]; $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP); diff --git a/src/Lookups.php b/src/Lookups.php index 6763410..11c7f6a 100644 --- a/src/Lookups.php +++ b/src/Lookups.php @@ -5,25 +5,25 @@ require_once __DIR__ . '/Db.php'; /** * Lookups — vistas de solo lectura sobre la tabla users para poblar - * dropdowns y resolver mapeo email ↔ apellido. + * dropdowns y resolver visibilidad. */ final class Lookups { - /** Apellidos para el dropdown "Abogado Solicitante" (socios + abogados activos). */ - public static function abogadoApellidos(): array { + /** Nombres completos para el dropdown "Abogado Solicitante" (socios + abogados activos). */ + public static function abogadoNames(): array { $rows = Db::pdo()->query( - "SELECT DISTINCT apellido FROM users + "SELECT DISTINCT name FROM users WHERE activo = 1 AND role IN ('socio','abogado') - AND apellido != '' - ORDER BY orden ASC, apellido ASC" + AND name != '' + ORDER BY orden ASC, name ASC" )->fetchAll(); - return array_map(fn($r) => $r['apellido'], $rows); + return array_map(fn($r) => $r['name'], $rows); } /** Todos los usuarios activos para el picker de Involucrados. */ public static function involucrablesUsers(): array { $rows = Db::pdo()->query( - "SELECT id, email, name, role, apellido + "SELECT id, email, name, role FROM users WHERE activo = 1 AND email NOT LIKE 'placeholder-%' @@ -36,10 +36,10 @@ final class Lookups { /** Todos los users (incluye inactivos y placeholders) para el Panel de Control. */ public static function allUsers(): array { $rows = Db::pdo()->query( - "SELECT id, email, name, role, apellido, activo, orden, + "SELECT id, email, name, role, activo, orden, first_seen_at, last_login_at FROM users - ORDER BY role DESC, apellido ASC, name ASC, email ASC" + ORDER BY role DESC, name ASC, email ASC" )->fetchAll(); foreach ($rows as &$r) { $r['id'] = (int)$r['id']; diff --git a/views/admin.html b/views/admin.html index 516058e..7cb81a3 100644 --- a/views/admin.html +++ b/views/admin.html @@ -157,18 +157,18 @@ tr.is-me td { background:#FFFBEB; font-weight:500; }

Usuarios y permisos

-

Administra la lista unificada de usuarios del sistema. El campo Apellido es lo que aparece en el dropdown "Abogado Solicitante" (solo si el rol es Socio o Abogado). Los emails que empiezan con placeholder- son los apellidos sembrados al inicio para que los dropdowns no salieran vacíos; reemplázalos con los emails reales conforme los abogados se logueen.

+

Administra la lista unificada de usuarios del sistema. El campo Nombre completo es lo que aparece en el dropdown "Abogado Solicitante" (solo si el rol es Socio o Abogado). Los emails que empiezan con placeholder- son los nombres sembrados al inicio para que los dropdowns no salieran vacíos; reemplázalos con los emails reales conforme los abogados se logueen.

- + - @@ -182,8 +182,7 @@ tr.is-me td { background:#FFFBEB; font-weight:500; } Email - Nombre - Apellido + Nombre completo Rol Activo Orden @@ -209,15 +208,11 @@ tr.is-me td { background:#FFFBEB; font-weight:500; }
El email debe coincidir exactamente con la cuenta de Mailcow que el usuario usa para entrar.
-
- - -
- - -
Solo aparece en dropdowns si el rol es Socio o Abogado.
+ + +
Aparece en el dropdown "Abogado Solicitante" si el rol es Socio o Abogado.
@@ -310,12 +305,12 @@ async function loadUsers(){ function visibleUsers(){ const q = $('search').value.toLowerCase().trim(); const fR = $('f-role').value; - const fA = $('f-activo').value; + const fA = $('f-activo-filter').value; return users.filter(u => { if (fR && u.role !== fR) return false; if (fA !== '' && String(u.activo) !== fA) return false; if (q) { - const blob = ((u.email||'')+' '+(u.name||'')+' '+(u.apellido||'')).toLowerCase(); + const blob = ((u.email||'')+' '+(u.name||'')).toLowerCase(); if (!blob.includes(q)) return false; } return true; @@ -338,7 +333,6 @@ function render(){ return ` ${esc(u.email)}${isPlaceholder?' (placeholder)':''}${isMe?' (tú)':''} ${esc(u.name) || '—'} - ${esc(u.apellido) || '—'} ${ROLE_LABEL[u.role] || u.role} ${u.activo ? 'Activo' : 'Inactivo'} ${u.orden} @@ -353,7 +347,6 @@ function openCreate(){ $('modal-title').textContent = 'Nuevo usuario'; $('f-email').value = ''; $('f-email').readOnly = false; $('f-name').value = ''; - $('f-apellido').value = ''; $('f-role-edit').value = 'usuario'; $('f-orden').value = '0'; $('f-activo').checked = true; @@ -370,7 +363,6 @@ function openEdit(id){ $('f-email').value = u.email; $('f-email').readOnly = false; $('f-name').value = u.name || ''; - $('f-apellido').value = u.apellido || ''; $('f-role-edit').value = u.role; $('f-orden').value = u.orden; $('f-activo').checked = u.activo === 1; @@ -390,7 +382,6 @@ async function saveUser(){ const payload = { email: $('f-email').value.trim().toLowerCase(), name: $('f-name').value.trim(), - apellido: $('f-apellido').value.trim(), role: $('f-role-edit').value, orden: parseInt($('f-orden').value, 10) || 0, activo: $('f-activo').checked ? 1 : 0, @@ -423,7 +414,7 @@ async function saveUser(){ document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); }); -['search','f-role','f-activo'].forEach(id => { +['search','f-role','f-activo-filter'].forEach(id => { $(id).addEventListener('input', render); $(id).addEventListener('change', render); });