diff --git a/public/admin/api.php b/public/admin/api.php index 61d363b..9276348 100644 --- a/public/admin/api.php +++ b/public/admin/api.php @@ -119,6 +119,27 @@ try { ok($row); } + if ($method === 'DELETE' && $action === 'delete_user') { + $id = (int)($_GET['id'] ?? 0); + if (!$id) err('id requerido'); + if ($id === $me) err('No puedes eliminarte a ti mismo', 400); + + $sel = $pdo->prepare('SELECT id, email, role FROM users WHERE id = :id'); + $sel->execute([':id' => $id]); + $u = $sel->fetch(); + if (!$u) err('Usuario no encontrado', 404); + if ($u['role'] === 'socio') { + err('No puedes eliminar a otro Socio. Cambia su rol a Abogado o Usuario primero.', 400); + } + + // CASCADE de tramites_involucrados limpia los vínculos automáticamente. + // Los strings históricos (tramite.creado_por, tramite.abogado, obs.autor_email, + // hours.autor_email) permanecen como referencia muerta — eso es lo deseado para + // preservar la trazabilidad de trámites antiguos. + $pdo->prepare('DELETE FROM users WHERE id = :id')->execute([':id' => $id]); + ok(['deleted' => $id, 'email' => $u['email']]); + } + err('Acción no reconocida', 404); } catch (PDOException $e) { diff --git a/views/admin.html b/views/admin.html index 7cb81a3..85abce7 100644 --- a/views/admin.html +++ b/views/admin.html @@ -62,6 +62,9 @@ header { background:var(--navy); border-bottom:3px solid var(--gold); padding:0 .btn-primary:hover { background:var(--navy-d); } .btn-secondary { padding:7px 14px; background:transparent; border:1px solid var(--gray-200); color:var(--gray-700); border-radius:8px; font-size:13px; } .btn-secondary:hover { background:var(--gray-100); } +.btn-danger { padding:7px 14px; background:#fff; border:1px solid #e74c3c; color:#c0392b; border-radius:8px; font-size:13px; } +.btn-danger:hover { background:#c0392b; color:#fff; } +.btn-danger:disabled { opacity:.5; cursor:not-allowed; } .tbl-card { max-width:1100px; margin:18px auto 32px; padding:0 24px; } .tbl-wrap { background:#fff; border:1px solid var(--gray-100); border-radius:12px; overflow:hidden; box-shadow:var(--shadow); } @@ -196,7 +199,7 @@ tr.is-me td { background:#FFFBEB; font-weight:500; } - - @@ -351,6 +357,7 @@ function openCreate(){ $('f-orden').value = '0'; $('f-activo').checked = true; $('hint-email').textContent = 'El email debe coincidir exactamente con la cuenta de Mailcow que el usuario usa para entrar.'; + $('btn-delete').style.display = 'none'; // solo en modo edición $('modal').classList.add('open'); setTimeout(()=>$('f-email').focus(), 50); } @@ -367,8 +374,16 @@ function openEdit(id){ $('f-orden').value = u.orden; $('f-activo').checked = u.activo === 1; $('hint-email').textContent = (u.email||'').startsWith('placeholder-') - ? 'Reemplaza este email placeholder por el email real del abogado.' + ? 'Reemplaza este email placeholder por el email real del abogado. Si el email real ya existe como otro usuario, mejor borra el placeholder con el botón de abajo.' : 'Cambiar el email afecta la asociación con Mailcow OAuth.'; + // Mostrar botón Borrar excepto para uno mismo o para otro socio + const canDelete = (u.id !== (window.DONNA && window.DONNA.users.find(x => x.email === MY_EMAIL)?.id)) + && (u.role !== 'socio'); + // Más simple: solo ocultar si es uno mismo. La API rechaza al socio. + const isSelf = u.email === MY_EMAIL; + $('btn-delete').style.display = isSelf ? 'none' : 'inline-block'; + $('btn-delete').disabled = (u.role === 'socio' && !isSelf); + $('btn-delete').title = (u.role === 'socio') ? 'Demuévelo a Abogado o Usuario primero' : 'Eliminar permanentemente'; $('modal').classList.add('open'); setTimeout(()=>$('f-name').focus(), 50); } @@ -378,6 +393,30 @@ function closeModal(){ editingId = null; } +async function deleteUser(){ + if (!editingId) return; + const u = users.find(x => x.id === editingId); + if (!u) return; + const msg = (u.email||'').startsWith('placeholder-') + ? `¿Eliminar el placeholder "${u.name}" (${u.email})?\n\nEsto libera el "apellido" del dropdown para que el usuario real lo tome.` + : `¿Eliminar permanentemente al usuario ${u.name || u.email}?\n\nEsto es irreversible. Los trámites históricos que creó o donde está como abogado/involucrado quedarán como referencia (sus vínculos N:M se borran, los strings de email/nombre quedan como historial).\n\nSi solo quieres bloquearle el acceso, mejor destilda "Activo".`; + if (!confirm(msg)) return; + const btn = $('btn-delete'); + btn.disabled = true; + try { + const result = await apiCall('DELETE', 'delete_user', null, 'id=' + editingId); + if (!result) return; + users = users.filter(x => x.id !== editingId); + closeModal(); + render(); + toast('Usuario eliminado'); + } catch(e) { + toast('Error: ' + e.message, 'error'); + } finally { + btn.disabled = false; + } +} + async function saveUser(){ const payload = { email: $('f-email').value.trim().toLowerCase(), @@ -412,7 +451,10 @@ async function saveUser(){ } document.addEventListener('keydown', e => { - if (e.key === 'Escape') closeModal(); + // Solo cerrar con Esc si el modal está abierto Y el foco no está en un input + if (e.key === 'Escape' && $('modal').classList.contains('open')) { + closeModal(); + } }); ['search','f-role','f-activo-filter'].forEach(id => { $(id).addEventListener('input', render);