feat(admin): botón Borrar usuario + fix modal cierra al clic fondo

* admin/api.php: nuevo endpoint DELETE ?action=delete_user con salvaguardas:
  - No te puedes eliminar a ti mismo
  - No puedes eliminar a otro Socio (cambiar su rol primero)
  - CASCADE de tramites_involucrados limpia vínculos N:M automáticamente
  - Strings históricos (creado_por, abogado, autor_email) permanecen como
    referencia muerta para preservar trazabilidad de trámites antiguos
* admin.html: botón rojo "Borrar usuario" en el footer del modal, visible
  solo en modo edición (no en crear). Confirmación contextual según si es
  placeholder o usuario real. Pista en el hint del email sobre usar Borrar
  cuando hay conflicto.
* Fix: removido onclick del modal-overlay que cerraba el modal al hacer
  clic en el padding del backdrop. Ahora solo cierra con ✕, Cancelar,
  Esc, o Guardar exitoso. Más predecible.
This commit is contained in:
Soporte ZH Consultores
2026-06-05 00:01:02 +00:00
parent 072cc0b6e3
commit 684c83f2c9
2 changed files with 69 additions and 6 deletions
+21
View File
@@ -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) {
+48 -6
View File
@@ -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; }
</div>
</div>
<div class="modal-overlay" id="modal" onclick="if(event.target.id==='modal')closeModal()">
<div class="modal-overlay" id="modal">
<div class="modal">
<header class="modal-head">
<h3 id="modal-title">Nuevo usuario</h3>
@@ -235,9 +238,12 @@ tr.is-me td { background:#FFFBEB; font-weight:500; }
</div>
</div>
</div>
<div class="modal-foot">
<button class="btn-secondary" onclick="closeModal()">Cancelar</button>
<button class="btn-primary" id="btn-save" onclick="saveUser()">Guardar</button>
<div class="modal-foot" style="justify-content:space-between;">
<button class="btn-danger" id="btn-delete" onclick="deleteUser()" style="display:none;">Borrar usuario</button>
<div style="display:flex;gap:10px;margin-left:auto;">
<button class="btn-secondary" onclick="closeModal()">Cancelar</button>
<button class="btn-primary" id="btn-save" onclick="saveUser()">Guardar</button>
</div>
</div>
</div>
</div>
@@ -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);