NUEVA APP /horas/
* public/horas/index.php — Bootstrap::inject del view
* public/horas/api.php — endpoint GET ?action=list que retorna:
- entries: entradas detalladas con join a tramites_legales (link,
título, cliente, departamento, estatus)
- by_user: agregado por persona (solo socio/admin)
- avail_users: dropdown de usuarios para filtrar (solo socio/admin)
- total_hrs, is_socio, my_name
Filtros: ?from=YYYY-MM-DD, ?to=YYYY-MM-DD, ?user=ID
* views/horas.html — UI con:
- Header estandarizado (Portal, sync, user-chip, gear si socio, logout)
- Toolbar de filtros: rango de fechas + dropdown usuario (solo socio)
+ atajos rápidos (Hoy / Semana / Mes / Año / Todo)
- KPIs: Total horas, Entradas, Personas con horas (socio)
- Tarjetas "Por persona" (solo socio), click filtra por ese usuario
- Tabla detalle con link al trámite (?edit=CODIGO) + departamento +
horas en h:mm + descripción
VISIBILIDAD
* Abogado/usuario ve solo sus horas (match por h.abogado = mi_name).
* Socio/admin ve todas las horas. Puede filtrar por persona.
* Admin oculto del dropdown de personas (queries excluyen role=admin).
ICONO DE RELOJ EN HEADERS
* Agregado .btn-clock (alias visual de .btn-gear) en:
escrituras.html, tramites.html, calendario.html, admin.html,
horas.html, public/index.php (landing).
* Posición: al lado del user-chip, antes del gear/logout.
* Visible para todos los autenticados (no restringido por rol).
488 lines
23 KiB
HTML
488 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Panel de Control — Donna AALN</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--navy:#1B3A6B; --navy-d:#112446; --navy-l:#2D5090;
|
|
--gold:#C9A84C; --gold-l:#E8C96A; --gold-d:#9E7C2E;
|
|
--off:#F7F6F3;
|
|
--gray-100:#EEEDE9; --gray-200:#D6D4CC; --gray-400:#9B9A96;
|
|
--gray-700:#4A4946; --gray-900:#1A1916;
|
|
--red:#c0392b; --orange:#e67e22; --green:#27ae60;
|
|
--shadow:0 1px 3px rgba(0,0,0,.06),0 4px 16px rgba(0,0,0,.06);
|
|
}
|
|
*,*::before,*::after { box-sizing:border-box; margin:0; padding:0; }
|
|
body { font-family:'DM Sans',system-ui,sans-serif; color:var(--gray-900); background:var(--off); font-size:13.5px; }
|
|
a { color:inherit; text-decoration:none; }
|
|
button { font-family:inherit; cursor:pointer; }
|
|
code { font-family:ui-monospace,monospace; background:var(--gray-100); padding:1px 5px; border-radius:3px; font-size:11.5px; }
|
|
|
|
header { background:var(--navy); border-bottom:3px solid var(--gold); padding:0 24px;
|
|
display:flex; align-items:center; justify-content:space-between; height:58px;
|
|
position:sticky; top:0; z-index:200; }
|
|
.header-left { display:flex; align-items:center; gap:14px; }
|
|
.btn-back { display:inline-flex; align-items:center; gap:6px;
|
|
background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.18);
|
|
color:rgba(255,255,255,.9); padding:6px 12px 6px 9px; border-radius:8px;
|
|
font-size:12px; transition:all .15s; white-space:nowrap; }
|
|
.btn-back:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); }
|
|
.btn-back svg { width:14px; height:14px; }
|
|
.logo { display:flex; align-items:center; gap:14px; }
|
|
.logo-mark { width:32px; height:32px; background:var(--gold); border-radius:6px;
|
|
display:flex; align-items:center; justify-content:center;
|
|
font-family:'DM Serif Display',serif; font-size:15px; color:var(--navy); }
|
|
.logo-text { display:flex; flex-direction:column; line-height:1.2; }
|
|
.logo-firm { font-size:11px; font-weight:600; color:var(--gold); letter-spacing:.08em; text-transform:uppercase; }
|
|
.logo-tool { font-size:13px; font-weight:400; color:rgba(255,255,255,.85); }
|
|
.header-right { display:flex; align-items:center; gap:16px; }
|
|
.user-chip { display:flex; align-items:center; gap:8px; background:rgba(255,255,255,.1);
|
|
border:1px solid rgba(255,255,255,.15); border-radius:20px; padding:5px 12px 5px 6px; }
|
|
.user-avatar { width:24px; height:24px; border-radius:50%; background:var(--gold); color:var(--navy);
|
|
display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; }
|
|
.user-name { font-size:12px; color:rgba(255,255,255,.85); max-width:160px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
.btn-logout { font-size:11px; padding:4px 10px; border:1px solid rgba(255,255,255,.2); border-radius:6px;
|
|
background:transparent; color:rgba(255,255,255,.7); transition:all .15s; }
|
|
.btn-logout:hover { background:rgba(255,255,255,.1); color:#fff; }
|
|
.btn-clock { display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px;
|
|
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; }
|
|
.btn-clock:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); }
|
|
.btn-clock svg { width:16px; height:16px; }
|
|
|
|
.intro { max-width:1100px; margin:24px auto 0; padding:0 24px; }
|
|
.intro h1 { font-family:'DM Serif Display',serif; font-size:30px; color:var(--navy); font-weight:400; margin-bottom:6px; }
|
|
.intro p { font-size:13.5px; color:var(--gray-700); line-height:1.55; max-width:820px; }
|
|
|
|
.toolbar { max-width:1100px; margin:18px auto 0; padding:0 24px; display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
|
.toolbar input, .toolbar select { padding:7px 11px; border:1px solid var(--gray-200); border-radius:8px; font-family:inherit; font-size:12.5px; background:#fff; }
|
|
.toolbar input { flex:1; min-width:200px; max-width:320px; }
|
|
.btn-primary { padding:7px 16px; background:var(--navy); color:#fff; border:none; border-radius:8px;
|
|
font-size:13px; font-weight:500; transition:all .15s; }
|
|
.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); }
|
|
table { width:100%; border-collapse:collapse; font-size:12.5px; }
|
|
th { background:var(--navy); color:rgba(255,255,255,.9); padding:10px 12px; text-align:left;
|
|
font-size:10.5px; font-weight:600; text-transform:uppercase; letter-spacing:.07em;
|
|
white-space:nowrap; border-right:1px solid rgba(255,255,255,.06); }
|
|
th:last-child { border-right:none; }
|
|
td { padding:9px 12px; border-bottom:1px solid var(--gray-100); color:var(--gray-700); vertical-align:middle; }
|
|
tr:last-child td { border-bottom:none; }
|
|
tr:hover td { background:#F9FAFC; }
|
|
tr.inactive td { color:var(--gray-400); background:#FAFAF7; font-style:italic; }
|
|
tr.is-me td { background:#FFFBEB; font-weight:500; }
|
|
|
|
.role-pill { display:inline-block; padding:2px 9px; border-radius:14px; font-size:10.5px; font-weight:700;
|
|
letter-spacing:.04em; text-transform:uppercase; }
|
|
.role-admin { background:#1B3A6B; color:#fff; }
|
|
.role-socio { background:#FCE4EC; color:#880E4F; }
|
|
.role-abogado { background:#E1F5EE; color:#085041; }
|
|
.role-usuario { background:#EEEDE9; color:#5F5E5A; }
|
|
|
|
.activo-pill { display:inline-block; padding:2px 8px; border-radius:14px; font-size:10.5px; font-weight:600; }
|
|
.activo-1 { background:#D4EDDA; color:#155724; }
|
|
.activo-0 { background:#F5C2C7; color:#842029; }
|
|
|
|
.placeholder { font-style:italic; color:var(--gray-400); font-size:11px; }
|
|
|
|
.actions { display:flex; gap:6px; }
|
|
.btn-icon { width:28px; height:28px; border:1px solid var(--gray-200); border-radius:6px; background:#fff;
|
|
color:var(--gray-700); display:inline-flex; align-items:center; justify-content:center; transition:all .15s; }
|
|
.btn-icon:hover { background:var(--gold); border-color:var(--gold); color:var(--navy); }
|
|
|
|
.last-login { font-size:11px; color:var(--gray-400); white-space:nowrap; }
|
|
|
|
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(17,36,70,.6);
|
|
z-index:1000; align-items:center; justify-content:center; padding:24px; }
|
|
.modal-overlay.open { display:flex; }
|
|
.modal { background:#fff; border-radius:14px; width:100%; max-width:520px; max-height:90vh;
|
|
overflow:hidden; display:flex; flex-direction:column; box-shadow:0 20px 60px rgba(0,0,0,.3); }
|
|
.modal-head { background:var(--navy); color:#fff; padding:16px 22px; display:flex;
|
|
justify-content:space-between; align-items:center; border-bottom:2px solid var(--gold); }
|
|
.modal-head h3 { font-family:'DM Serif Display',serif; font-size:18px; font-weight:400; margin:0; }
|
|
.modal-head .close { background:transparent; border:none; color:rgba(255,255,255,.7); font-size:22px; line-height:1; }
|
|
.modal-head .close:hover { color:#fff; }
|
|
.modal-body { padding:18px 22px; overflow-y:auto; flex:1; }
|
|
.fg { margin-bottom:14px; }
|
|
.fg label { display:block; font-size:10.5px; font-weight:600; color:var(--navy);
|
|
margin-bottom:5px; text-transform:uppercase; letter-spacing:.08em; }
|
|
.fg input, .fg select { width:100%; padding:8px 11px; border:1px solid var(--gray-200);
|
|
border-radius:8px; font-family:'DM Sans',sans-serif; font-size:13px; background:#fff; }
|
|
.fg input:focus, .fg select:focus { outline:none; border-color:var(--navy-l);
|
|
box-shadow:0 0 0 3px rgba(27,58,107,.10); }
|
|
.fg-hint { font-size:10.5px; color:var(--gray-400); margin-top:4px; line-height:1.4; }
|
|
.fg-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
|
.fg-check { display:flex; align-items:center; gap:8px; font-size:13px; color:var(--gray-700); cursor:pointer; }
|
|
.fg-check input { width:auto; margin:0; }
|
|
.modal-foot { padding:14px 22px; border-top:1px solid var(--gray-100); display:flex; justify-content:flex-end;
|
|
gap:10px; background:var(--off); }
|
|
|
|
.toast { position:fixed; bottom:24px; right:24px; background:var(--navy-d); color:#fff;
|
|
padding:11px 18px; border-radius:8px; font-size:12.5px; border-left:3px solid var(--gold);
|
|
box-shadow:0 4px 20px rgba(0,0,0,.2); transform:translateY(20px); opacity:0;
|
|
transition:all .25s; z-index:2000; }
|
|
.toast.show { transform:translateY(0); opacity:1; }
|
|
.toast.error { border-left-color:#ef5350; }
|
|
|
|
.empty { text-align:center; padding:40px; color:var(--gray-400); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<div class="header-left">
|
|
<a class="btn-back" href="/" title="Volver al portal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
|
Portal
|
|
</a>
|
|
<div class="logo">
|
|
<div class="logo-mark">A</div>
|
|
<div class="logo-text">
|
|
<span class="logo-firm">AALN Law</span>
|
|
<span class="logo-tool">Panel de Control</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<div class="user-chip">
|
|
<div class="user-avatar" id="user-avatar">?</div>
|
|
<span class="user-name" id="user-name">…</span>
|
|
</div>
|
|
<a class="btn-clock" href="/horas/" title="Reporte de horas">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
</a>
|
|
<a class="btn-logout" href="/auth/logout.php" onclick="return confirm('¿Cerrar sesión?');">Cerrar sesión</a>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="intro">
|
|
<h1>Usuarios y permisos</h1>
|
|
<p>Administra la lista unificada de usuarios del sistema. El campo <b>Nombre completo</b> es lo que aparece en el dropdown "Abogado Solicitante" (solo si el rol es <b>Socio</b> o <b>Abogado</b>). Los emails que empiezan con <code>placeholder-</code> 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.</p>
|
|
</div>
|
|
|
|
<div class="toolbar">
|
|
<input type="search" id="search" placeholder="Buscar por email o nombre…">
|
|
<select id="f-role">
|
|
<option value="">— Todos los roles —</option>
|
|
<option value="admin">Admin</option>
|
|
<option value="socio">Socio</option>
|
|
<option value="abogado">Abogado</option>
|
|
<option value="usuario">Usuario</option>
|
|
</select>
|
|
<select id="f-activo-filter">
|
|
<option value="">— Activos e inactivos —</option>
|
|
<option value="1">Solo activos</option>
|
|
<option value="0">Solo inactivos</option>
|
|
</select>
|
|
<button class="btn-primary" onclick="openCreate()">+ Nuevo usuario</button>
|
|
</div>
|
|
|
|
<div class="tbl-card">
|
|
<div class="tbl-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Email</th>
|
|
<th>Nombre completo</th>
|
|
<th>Rol</th>
|
|
<th>Activo</th>
|
|
<th>Orden</th>
|
|
<th>Último login</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="tbody"></tbody>
|
|
</table>
|
|
<div id="empty" class="empty" style="display:none;">Sin usuarios que coincidan con los filtros.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="modal">
|
|
<div class="modal">
|
|
<header class="modal-head">
|
|
<h3 id="modal-title">Nuevo usuario</h3>
|
|
<button class="close" onclick="closeModal()" title="Esc">✕</button>
|
|
</header>
|
|
<div class="modal-body">
|
|
<div class="fg">
|
|
<label>Email *</label>
|
|
<input type="email" id="f-email" placeholder="usuario@aal-law.com">
|
|
<div class="fg-hint" id="hint-email">El email debe coincidir exactamente con la cuenta de Mailcow que el usuario usa para entrar.</div>
|
|
</div>
|
|
<div class="fg-row">
|
|
<div class="fg">
|
|
<label>Nombre completo</label>
|
|
<input type="text" id="f-name" placeholder="Ej: Carlos Abrego">
|
|
<div class="fg-hint">Aparece en el dropdown "Abogado Solicitante" si el rol es Socio o Abogado.</div>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Rol</label>
|
|
<select id="f-role-edit">
|
|
<option value="usuario">Usuario (pasante / asistente)</option>
|
|
<option value="abogado">Abogado</option>
|
|
<option value="socio">Socio (modo Dios)</option>
|
|
<option value="admin" id="opt-admin" style="display:none;">Admin (programador, modo Dios oculto)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="fg-row">
|
|
<div class="fg">
|
|
<label>Orden</label>
|
|
<input type="number" id="f-orden" value="0" step="1">
|
|
<div class="fg-hint">Para ordenar el dropdown manualmente.</div>
|
|
</div>
|
|
<div class="fg">
|
|
<label> </label>
|
|
<label class="fg-check"><input type="checkbox" id="f-activo" checked> Activo — puede entrar a Donna</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<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>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
const ROLES = ['admin','socio','abogado','usuario'];
|
|
const ROLE_LABEL = { admin:'Admin', socio:'Socio', abogado:'Abogado', usuario:'Usuario' };
|
|
const MY_EMAIL = (window.DONNA && window.DONNA.me) || '';
|
|
const I_AM_ADMIN = !!(window.DONNA && window.DONNA.isAdmin);
|
|
|
|
// Mostrar la opción "admin" en el dropdown solo si el usuario actual es admin
|
|
if (I_AM_ADMIN) {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const opt = document.getElementById('opt-admin');
|
|
if (opt) opt.style.display = '';
|
|
});
|
|
}
|
|
|
|
let users = [];
|
|
let editingId = null;
|
|
let authUser = { email: MY_EMAIL, name: (window.DONNA && window.DONNA.name) || '' };
|
|
|
|
function $(id){ return document.getElementById(id); }
|
|
function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
|
function toast(msg, type='ok'){
|
|
const el=$('toast'); el.textContent=msg;
|
|
el.className='toast show'+(type==='error'?' error':'');
|
|
setTimeout(()=>el.className='toast'+(type==='error'?' error':''),3500);
|
|
}
|
|
function initials(text){
|
|
const parts=String(text||'').replace(/[@._-]/g,' ').trim().split(/\s+/);
|
|
return ((parts[0]||'').charAt(0)+(parts[1]||'').charAt(0)).toUpperCase()||'?';
|
|
}
|
|
function fmtDate(dt){
|
|
if(!dt) return '—';
|
|
const d=new Date(dt); if(isNaN(d)) return dt;
|
|
return d.toLocaleDateString('es-PA',{day:'2-digit',month:'short',year:'numeric'})
|
|
+' '+d.toLocaleTimeString('es-PA',{hour:'2-digit',minute:'2-digit'});
|
|
}
|
|
function setUserChip(){
|
|
$('user-avatar').textContent = initials(authUser.name || authUser.email);
|
|
$('user-name').textContent = authUser.name || authUser.email || '—';
|
|
$('user-name').title = authUser.email;
|
|
}
|
|
|
|
async function apiCall(method, action, body=null, query=''){
|
|
const url = '/admin/api.php?action=' + action + (query?'&'+query:'');
|
|
const opts = { method, headers:{Accept:'application/json'}, credentials:'same-origin' };
|
|
if(body){ opts.headers['Content-Type']='application/json'; opts.body=JSON.stringify(body); }
|
|
const res = await fetch(url, opts);
|
|
let j;
|
|
try { j = await res.json(); } catch(e){ throw new Error('Respuesta inválida'); }
|
|
if(res.status===401 && j.auth_required){
|
|
window.location.href = j.login_url || '/auth/login.php?return_to=/admin/';
|
|
return null;
|
|
}
|
|
if(!res.ok || !j.ok) throw new Error(j.error || ('HTTP '+res.status));
|
|
if(j.user) authUser = j.user;
|
|
return j.data;
|
|
}
|
|
|
|
async function loadUsers(){
|
|
try {
|
|
users = (await apiCall('GET','list_users')) || [];
|
|
setUserChip();
|
|
render();
|
|
} catch(e){
|
|
toast('Error al cargar usuarios: '+e.message, 'error');
|
|
}
|
|
}
|
|
|
|
function visibleUsers(){
|
|
const q = $('search').value.toLowerCase().trim();
|
|
const fR = $('f-role').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||'')).toLowerCase();
|
|
if (!blob.includes(q)) return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function render(){
|
|
const tbody = $('tbody');
|
|
const list = visibleUsers();
|
|
if (!list.length) {
|
|
tbody.innerHTML = '';
|
|
$('empty').style.display = 'block';
|
|
return;
|
|
}
|
|
$('empty').style.display = 'none';
|
|
tbody.innerHTML = list.map(u => {
|
|
const isMe = u.email === MY_EMAIL;
|
|
const isPlaceholder = (u.email||'').startsWith('placeholder-');
|
|
const rowCls = (u.activo === 0 ? 'inactive' : '') + (isMe ? ' is-me' : '');
|
|
return `<tr class="${rowCls}">
|
|
<td><b>${esc(u.email)}</b>${isPlaceholder?' <span class="placeholder">(placeholder)</span>':''}${isMe?' <span style="color:var(--gold-d);font-size:10px;">(tú)</span>':''}</td>
|
|
<td>${esc(u.name) || '<span class="placeholder">—</span>'}</td>
|
|
<td><span class="role-pill role-${u.role}">${ROLE_LABEL[u.role] || u.role}</span></td>
|
|
<td><span class="activo-pill activo-${u.activo}">${u.activo ? 'Activo' : 'Inactivo'}</span></td>
|
|
<td>${u.orden}</td>
|
|
<td class="last-login">${u.last_login_at ? fmtDate(u.last_login_at) : '<span class="placeholder">Nunca</span>'}</td>
|
|
<td><div class="actions"><button class="btn-icon" title="Editar" onclick="openEdit(${u.id})">✎</button></div></td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
function openCreate(){
|
|
editingId = null;
|
|
$('modal-title').textContent = 'Nuevo usuario';
|
|
$('f-email').value = ''; $('f-email').readOnly = false;
|
|
$('f-name').value = '';
|
|
$('f-role-edit').value = 'usuario';
|
|
$('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);
|
|
}
|
|
|
|
function openEdit(id){
|
|
const u = users.find(x => x.id === id);
|
|
if (!u) return;
|
|
editingId = u.id;
|
|
$('modal-title').textContent = 'Editar usuario · ' + u.email;
|
|
$('f-email').value = u.email;
|
|
$('f-email').readOnly = false;
|
|
$('f-name').value = u.name || '';
|
|
$('f-role-edit').value = u.role;
|
|
$('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. 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);
|
|
}
|
|
|
|
function closeModal(){
|
|
$('modal').classList.remove('open');
|
|
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(),
|
|
name: $('f-name').value.trim(),
|
|
role: $('f-role-edit').value,
|
|
orden: parseInt($('f-orden').value, 10) || 0,
|
|
activo: $('f-activo').checked ? 1 : 0,
|
|
};
|
|
if (!payload.email) { toast('Email requerido','error'); $('f-email').focus(); return; }
|
|
|
|
const btn = $('btn-save');
|
|
btn.disabled = true;
|
|
try {
|
|
let saved;
|
|
if (editingId) {
|
|
payload._id = editingId;
|
|
saved = await apiCall('PUT','update_user', payload);
|
|
} else {
|
|
saved = await apiCall('POST','create_user', payload);
|
|
}
|
|
if (!saved) return;
|
|
const idx = users.findIndex(u => u.id === saved.id);
|
|
if (idx >= 0) users[idx] = saved; else users.unshift(saved);
|
|
closeModal();
|
|
render();
|
|
toast(editingId ? 'Usuario actualizado' : 'Usuario creado');
|
|
} catch(e){
|
|
toast('Error: '+e.message, 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', e => {
|
|
// 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);
|
|
$(id).addEventListener('change', render);
|
|
});
|
|
|
|
loadUsers();
|
|
</script>
|
|
</body>
|
|
</html>
|