backend/cuentas.py: restablece en bloque (todos menos leo y claude) y da de alta a varios desde backend/cuentas/2026-10-23_alta-sitios.json (jesush, miguel, joel, antonio, danielg); la contraseña la pide en la terminal. Miembros con sitio, correo y correo_alt; marca contrasena_inicial con aviso en el sitio hasta que la persona la cambie. Los ids de responsable se recalculan con el equipo de hoy. Lote 2026-10-23_usuarios-campo. Pruebas: tests-cuentas.sh (47). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
229 lines
20 KiB
Markdown
229 lines
20 KiB
Markdown
---
|
|
title: "Equipo"
|
|
description: "Quién es quién: miembros, sitio, área, correo, acceso al sitio y pendientes de cada quien."
|
|
template: tablero.html
|
|
hide:
|
|
- toc
|
|
---
|
|
|
|
<style>.tb-equipo td.correo { min-width: 11em; font-size: 13.5px; } .tb-equipo td.correo a, .tb-equipo td.correo .id { overflow-wrap: normal; word-break: normal; }</style>
|
|
|
|
<div class="tb tb-equipo">
|
|
<header class="tb-cab">
|
|
<div class="tb-cab__t"><h1>Equipo</h1><span class="meta" id="meta">Cargando…</span></div>
|
|
<div class="tb-cab__acc"><a class="boton sec" href="/cuenta.html"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7zM5 20c.8-3.8 3.6-6 7-6s6.2 2.2 7 6"/></svg>Mi cuenta</a></div>
|
|
</header>
|
|
|
|
<details class="info tb-como">
|
|
<summary>Cómo se usa</summary>
|
|
<ol>
|
|
<li>Aquí está <b>quién es quién</b>: nombre, <b>sitio</b> donde trabaja (ICAVE, TIMSA, EIT, LCT o Querétaro), rol, área, correo, si tiene acceso al sitio y cuántos pendientes abiertos y vencidos trae cada quien. Filtra por sitio con la lista de arriba.</li>
|
|
<li><b>Todo pendiente tiene responsable.</b> Al crearlo en el tablero se elige de esta lista; puede haber más de uno.</li>
|
|
<li>Un miembro <b>sin acceso al sitio</b> también puede ser responsable (Adriana hoy): el pendiente se le asigna igual y alguien con acceso lo mueve por ella.</li>
|
|
<li>Toca un nombre para ver sus pendientes abiertos y abrir el tablero filtrado.</li>
|
|
<li>Los <b>accesos los crea Leo aquí</b> (botón «Nuevo usuario / miembro»), ya no con <code>htpasswd</code> en el servidor. La contraseña inicial se muestra <b>una sola vez</b>: pásala en persona o por mensaje directo; la persona la cambia en <a href="/cuenta.html">Mi cuenta</a>.</li>
|
|
<li><b>Contraseña inicial:</b> mientras alguien entre con la contraseña que le dieron (al darle acceso o al restablecerla), el sitio le muestra arriba un aviso para cambiarla, y aquí el administrador ve la marca «contraseña inicial». La marca se quita sola cuando la persona la cambia en Mi cuenta.</li>
|
|
<li><b>Correo:</b> el de cada quien, para mandar avisos por correo más adelante. Si alguien tiene cuenta en los dos dominios de Helgen, el segundo va en «Otro correo». Lo edita el administrador con «Editar».</li>
|
|
<li>«Desactivar» oculta al miembro de las listas sin borrar nada; «Quitar acceso» le cierra la entrada al sitio y lo desactiva. Nadie se borra: el historial se queda.</li>
|
|
<li>Si no tienes acceso o no apareces, pídeselo a Leo.</li>
|
|
</ol>
|
|
</details>
|
|
|
|
<div class="msg" id="aviso"></div>
|
|
<div class="acciones" style="margin:0 0 12px"><select id="fSitio" aria-label="Sitio"><option value="">Sitio: todos</option></select><span id="acciones" style="display:none"><button id="bNuevo"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Nuevo usuario o miembro</button></span><span class="cnt" id="cnt"></span></div>
|
|
<div id="cont" class="tblwrap"></div>
|
|
<section class="bloque mis-pend" id="detalle" style="display:none"></section>
|
|
|
|
<dialog id="dlg"></dialog>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const el = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a) { if (p === 'html') e.innerHTML = a[p]; else if (p === 'text') e.textContent = a[p]; else e.setAttribute(p, a[p]); } k.forEach(c => c != null && e.append(c)); return e; };
|
|
const esc = s => { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; };
|
|
const fmt = iso => (iso || '').replace('T', ' ').slice(0, 16);
|
|
const $ = id => document.getElementById(id);
|
|
const AREA = { direccion: 'Dirección', operaciones: 'Operaciones', ingenieria: 'Ingeniería', layout: 'Layout', produccion: 'Producción', pruebas: 'Pruebas', fabricacion: 'Fabricación', software: 'Software', campo: 'Campo', asistente: 'Asistente' };
|
|
// Sitio de cada miembro (23-oct): dónde trabaja; vacío = sin sitio fijo
|
|
const SITIO = { icave: 'ICAVE · Veracruz', timsa: 'TIMSA · Manzanillo', eit: 'EIT · Ensenada', lzc: 'LCT · Lázaro Cárdenas', qro: 'Querétaro' };
|
|
const SITIO_C = { icave: 'ICAVE', timsa: 'TIMSA', eit: 'EIT', lzc: 'LCT', qro: 'Querétaro' };
|
|
Object.entries(SITIO).forEach(([k, v]) => document.getElementById('fSitio').append(el('option', { value: k, text: 'Sitio: ' + v })));
|
|
document.getElementById('fSitio').append(el('option', { value: '-', text: 'Sin sitio fijo' }));
|
|
const PISTAS = Kanban.PISTAS, ESTADOS = Object.fromEntries(Kanban.ESTADOS);
|
|
let MIEMBROS = [], ADMIN = false, USER = '', PASS_MIN = 8, AREAS = Object.keys(AREA), HTPASSWD_OK = true;
|
|
const dlg = $('dlg'), aviso = $('aviso');
|
|
function mensaje(t, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.innerHTML = t; clearTimeout(aviso._t); aviso._t = setTimeout(() => aviso.className = 'msg', bien ? 5000 : 10000); }
|
|
async function api(url, opts) { const r = await fetch(url, Object.assign({ cache: 'no-store' }, opts || {})); let j = null; try { j = await r.json(); } catch (e) {} if (!r.ok || !j || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status)); return j; }
|
|
const json = (m, b) => ({ method: m, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(b) });
|
|
function campo(l, i, ayuda) { const d = el('div'); d.append(el('label', { text: l }), i); if (ayuda) d.append(el('div', { style: 'font-size:12.5px;color:var(--tinta2);margin-top:3px', text: ayuda })); return d; }
|
|
|
|
function pintar() {
|
|
const cont = $('cont'); cont.innerHTML = '';
|
|
if (!MIEMBROS.length) { cont.append(el('div', { class: 'vacio', text: 'Todavía no hay miembros. Corre la siembra o crea el primero.' })); return; }
|
|
const fs = $('fSitio').value;
|
|
const vis = MIEMBROS.filter(m => !fs || (fs === '-' ? !m.sitio : m.sitio === fs));
|
|
$('cnt').textContent = fs ? vis.length + ' de ' + MIEMBROS.length : '';
|
|
if (!vis.length) { cont.append(el('div', { class: 'vacio', text: 'Nadie en ese sitio todavía. Edita a un miembro para ponerle su sitio.' })); return; }
|
|
const t = el('table', { class: 't' });
|
|
t.innerHTML = '<tr><th>Nombre</th><th>Sitio</th><th>Rol</th><th>Área</th><th>Correo</th><th>Acceso</th><th class="num">Abiertos</th><th class="num">Vencidos</th>' + (ADMIN ? '<th></th>' : '') + '</tr>';
|
|
vis.forEach(m => {
|
|
const tr = el('tr', { class: m.activo === false ? 'inactivo' : '' });
|
|
const nom = el('span', { class: 'nom' }, Kanban.avatar(m.id), document.createTextNode(m.nombre)); nom.addEventListener('click', () => detalle(m));
|
|
tr.append(el('td', null, nom, el('span', { class: 'id', text: m.id + (m.telefono ? ' · ' + m.telefono : '') })));
|
|
tr.append(el('td', { text: SITIO_C[m.sitio] || '—' }));
|
|
tr.append(el('td', { text: m.rol || '—' }));
|
|
tr.append(el('td', null, el('span', { class: 'area', text: AREA[m.area] || m.area })));
|
|
const co = el('td', { class: 'correo' });
|
|
const partido = c => esc(c).replace('@', '<wbr>@'); // si no cabe, corta antes de la arroba y no a media palabra
|
|
if (m.correo) co.append(el('a', { href: 'mailto:' + m.correo, html: partido(m.correo) })); else co.append(el('span', { class: 'id', text: '—' }));
|
|
if (m.correo_alt) co.append(el('span', { class: 'id', html: partido(m.correo_alt) }));
|
|
tr.append(co);
|
|
tr.append(el('td', { html: (m.activo === false ? '<span class="pill falla">inactivo</span> ' : '') + (m.tiene_login ? '<span class="pill ok">sí</span>' : '<span class="pill pend">no</span>') + (m.es_admin ? ' <span class="pill info">admin</span>' : '')
|
|
+ (ADMIN && m.tiene_login && m.contrasena_inicial ? ' <span class="pill aviso" title="Entra con la contraseña que le dieron; el sitio le pide cambiarla en Mi cuenta">contraseña inicial</span>' : '') }));
|
|
const p = m.pendientes || {};
|
|
tr.append(el('td', { class: 'num', html: p.abiertos ? '<a href="/tableros/pendientes/?responsable=' + encodeURIComponent(m.id) + '"><b>' + p.abiertos + '</b></a>' : '0' }));
|
|
tr.append(el('td', { class: 'num', html: p.vencidas ? '<span style="color:var(--falla);font-weight:700">' + p.vencidas + '</span>' : '0' }));
|
|
if (ADMIN) {
|
|
const acc = el('td', { style: 'white-space:nowrap' });
|
|
const be = el('button', { class: 'sec', text: 'Editar' }); be.addEventListener('click', () => editar(m)); acc.append(be);
|
|
if (m.activo !== false) { const bd = el('button', { class: 'sec', text: 'Desactivar' }); bd.addEventListener('click', () => desactivar(m)); acc.append(bd); }
|
|
else { const ba = el('button', { class: 'sec', text: 'Reactivar' }); ba.addEventListener('click', () => reactivar(m)); acc.append(ba); }
|
|
if (m.tiene_login && m.id.toLowerCase() !== USER.toLowerCase()) { const bq = el('button', { class: 'peligro', text: 'Quitar acceso' }); bq.addEventListener('click', () => quitarAcceso(m)); acc.append(bq); }
|
|
if (!m.tiene_login && m.activo !== false) { const bc = el('button', { class: 'sec', text: 'Crear acceso' }); bc.addEventListener('click', () => nuevo(m)); acc.append(bc); }
|
|
tr.append(acc);
|
|
}
|
|
t.append(tr);
|
|
});
|
|
cont.append(t);
|
|
}
|
|
async function cargar() {
|
|
$('meta').textContent = 'Actualizando…';
|
|
try {
|
|
const j = await api('/api/equipo?todos=1');
|
|
MIEMBROS = j.miembros || []; ADMIN = !!j.admin; USER = j.user || ''; PASS_MIN = j.pass_min || 8; AREAS = j.areas || AREAS; HTPASSWD_OK = j.htpasswd_ok !== false;
|
|
Kanban.usarEquipo(Object.fromEntries(MIEMBROS.map(m => [m.id, { nombre: m.nombre, activo: m.activo !== false, area: m.area }])));
|
|
pintar();
|
|
$('acciones').style.display = ADMIN ? '' : 'none';
|
|
const act = MIEMBROS.filter(m => m.activo !== false), con = act.filter(m => m.tiene_login).length;
|
|
const ini = ADMIN ? act.filter(m => m.tiene_login && m.contrasena_inicial).length : 0;
|
|
$('meta').textContent = act.length + ' miembros activos · ' + con + ' con acceso al sitio' + (ini ? ' · ' + ini + ' con contraseña inicial' : '') + ' · ' + (USER ? USER + (ADMIN ? ' (admin)' : '') : '');
|
|
if (ADMIN && !HTPASSWD_OK) mensaje('El servicio no encuentra el archivo de usuarios (HTPASSWD_FILE): se pueden editar miembros, pero no crear ni quitar accesos. Avisa a Leo.', false);
|
|
if (ADMIN && (j.logins_sin_miembro || []).length) mensaje('Cuentas con acceso que no están en el equipo: <b>' + j.logins_sin_miembro.map(esc).join(', ') + '</b>. Dales de alta como miembro (Nuevo usuario / miembro, sin marcar «crear acceso») para que puedan ser responsables.', false);
|
|
const q = new URLSearchParams(location.search).get('id'); if (q) { const m = MIEMBROS.find(x => x.id === q); if (m) detalle(m); }
|
|
} catch (e) { $('cont').innerHTML = ''; $('cont').append(el('div', { class: 'err', text: 'No se pudo cargar el equipo: ' + e.message })); $('meta').textContent = 'Error'; }
|
|
}
|
|
|
|
// ---------- pendientes de un miembro ----------
|
|
async function detalle(m) {
|
|
const sec = $('detalle'); sec.style.display = ''; sec.innerHTML = '';
|
|
sec.append(el('h2', { text: 'Pendientes de ' + m.nombre }), el('p', { class: 'sub', html: esc(m.rol || '') + ' · ' + esc(AREA[m.area] || m.area) + ' · <a href="/tableros/pendientes/?responsable=' + encodeURIComponent(m.id) + '">Abrir en el tablero →</a>' }));
|
|
const lista = el('div', { text: 'Cargando…' }); sec.append(lista);
|
|
sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
try {
|
|
const j = await api('/api/tareas?responsable=' + encodeURIComponent(m.id));
|
|
const abiertas = (j.tareas || []).filter(t => t.estado !== 'hecho');
|
|
lista.innerHTML = '';
|
|
if (!abiertas.length) { lista.append(el('div', { class: 'vacio', text: 'Sin pendientes abiertos.' })); return; }
|
|
abiertas.forEach(t => {
|
|
const c = el('div', { class: 'card p-' + t.pista + (t.vencida ? ' vencida' : '') });
|
|
c.append(el('div', { class: 'tit', html: '<a href="/tableros/pendientes/?id=' + encodeURIComponent(t.id) + '" style="color:inherit;text-decoration:none">' + esc(t.titulo) + '</a>' }));
|
|
c.append(el('div', { class: 'meta', html: '<span class="chip p-' + t.pista + '">' + esc(PISTAS[t.pista] || t.pista) + '</span><span class="chip">' + esc(ESTADOS[t.estado] || t.estado) + '</span>'
|
|
+ (t.fecha_limite ? '<span class="fecha' + (t.vencida ? ' mal' : '') + '">' + (t.vencida ? 'Venció ' : '') + esc(t.fecha_limite) + '</span>' : '')
|
|
+ ((t.responsable_ids || []).length > 1 ? '<span>Con ' + esc(Kanban.nombresResp(t).join(', ')) + '</span>' : '') }));
|
|
lista.append(c);
|
|
});
|
|
} catch (e) { lista.textContent = 'No se pudieron leer los pendientes: ' + e.message; }
|
|
}
|
|
|
|
// ---------- diálogos (admin) ----------
|
|
function abrirDialogo(titulo, cuerpoFn, botones) {
|
|
dlg.innerHTML = '';
|
|
const cab = el('div', { class: 'dcab' }); cab.append(el('h2', { text: titulo }));
|
|
const x = el('button', { type: 'button', 'aria-label': 'Cerrar', html: Kanban.icono('cerrar') }); x.addEventListener('click', () => dlg.close()); cab.append(x); dlg.append(cab);
|
|
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
|
const m = el('div', { class: 'msg' });
|
|
cuerpoFn(cuerpo, m); cuerpo.append(m);
|
|
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
|
const cancelar = el('button', { type: 'button', class: 'sec', text: botones.length ? 'Cancelar' : 'Cerrar' }); cancelar.addEventListener('click', () => dlg.close()); pie.append(cancelar);
|
|
botones.forEach(([txt, fn]) => { const b = el('button', { type: 'button', text: txt }); b.addEventListener('click', async () => { b.disabled = true; try { await fn(m, b, pie); } catch (e) { m.className = 'msg err'; m.textContent = e.message; } b.disabled = false; }); pie.append(b); });
|
|
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute('open', '');
|
|
}
|
|
function formMiembro(c, m, conAcceso) {
|
|
m = m || {}; const f = {};
|
|
f.id = el('input', { type: 'text', maxlength: '30', value: m.id || '', placeholder: 'adriana (minúsculas, sin acentos; será el usuario del sitio)', autocapitalize: 'none' }); if (m.id) f.id.disabled = true;
|
|
f.nombre = el('input', { type: 'text', maxlength: '80', value: m.nombre || '', placeholder: 'Nombre y apellido' });
|
|
f.rol = el('input', { type: 'text', maxlength: '120', value: m.rol || '', placeholder: 'Técnico de pruebas · Layout de PCB · Operaciones' });
|
|
f.area = el('select'); AREAS.forEach(a => f.area.append(el('option', { value: a, text: AREA[a] || a }))); f.area.value = m.area || 'produccion';
|
|
f.sitio = el('select'); f.sitio.append(el('option', { value: '', text: '— sin sitio fijo —' })); Object.entries(SITIO).forEach(([k, v]) => f.sitio.append(el('option', { value: k, text: v }))); f.sitio.value = m.sitio || '';
|
|
f.correo = el('input', { type: 'email', maxlength: '120', value: m.correo || '', placeholder: 'nombre@helgen.tech' });
|
|
f.correo_alt = el('input', { type: 'email', maxlength: '120', value: m.correo_alt || '', placeholder: 'el del otro dominio, si tiene' });
|
|
f.telefono = el('input', { type: 'tel', maxlength: '40', value: m.telefono || '' });
|
|
const g = el('div', { class: 'dos-col' }); g.append(campo('Usuario (id)', f.id), campo('Nombre', f.nombre), campo('Rol', f.rol), campo('Área', f.area), campo('Sitio', f.sitio, 'Dónde trabaja: el puerto o Querétaro.'), campo('Teléfono', f.telefono), campo('Correo', f.correo, 'Para los avisos por correo.'), campo('Otro correo', f.correo_alt)); c.append(g);
|
|
if (conAcceso) {
|
|
f.acceso = el('input', { type: 'checkbox', style: 'width:auto' }); f.acceso.checked = !m.tiene_login && HTPASSWD_OK; f.acceso.disabled = !HTPASSWD_OK;
|
|
const l = el('label', { class: 'chip-sel', style: 'margin-top:12px' }); l.append(f.acceso, document.createTextNode(' Crear acceso al sitio (usuario y contraseña)')); c.append(l);
|
|
f.pass = el('input', { type: 'text', autocomplete: 'off', placeholder: 'mínimo ' + PASS_MIN + ' caracteres; se muestra una sola vez' });
|
|
const cp = campo('Contraseña inicial', f.pass, 'La persona la cambia en Mi cuenta la primera vez que entre.'); c.append(cp);
|
|
const gen = el('button', { type: 'button', class: 'sec', text: 'Generar una', style: 'padding:6px 10px;font-size:13px' });
|
|
gen.addEventListener('click', () => { const A = 'abcdefghjkmnpqrstuvwxyz23456789'; let p = ''; const r = new Uint32Array(12); crypto.getRandomValues(r); r.forEach(v => p += A[v % A.length]); f.pass.value = p.slice(0, 4) + '-' + p.slice(4, 8) + '-' + p.slice(8, 12); });
|
|
cp.append(gen);
|
|
const sync = () => { cp.style.display = f.acceso.checked ? '' : 'none'; }; f.acceso.addEventListener('change', sync); sync();
|
|
if (!HTPASSWD_OK) c.append(el('div', { class: 'err', text: 'No se pueden crear accesos: el servicio no encuentra el archivo de usuarios.' }));
|
|
}
|
|
return f;
|
|
}
|
|
const leerMiembro = f => ({ nombre: f.nombre.value.trim(), rol: f.rol.value.trim(), area: f.area.value, sitio: f.sitio.value, correo: f.correo.value.trim().toLowerCase(), correo_alt: f.correo_alt.value.trim().toLowerCase(), telefono: f.telefono.value.trim() });
|
|
function nuevo(base) {
|
|
let f;
|
|
abrirDialogo(base ? 'Crear acceso para ' + base.nombre : 'Nuevo usuario / miembro', c => { f = formMiembro(c, base, true); if (base) { f.acceso.checked = true; f.acceso.dispatchEvent(new Event('change')); } },
|
|
[['Guardar', async (m, b, pie) => {
|
|
const id = Kanban.norm(f.id.value); const d = leerMiembro(f);
|
|
if (!id) throw new Error('Escribe el usuario (id).');
|
|
if (!d.nombre) throw new Error('Escribe el nombre.');
|
|
if (f.acceso.checked) {
|
|
if (f.pass.value.length < PASS_MIN) throw new Error('La contraseña inicial debe tener al menos ' + PASS_MIN + ' caracteres.');
|
|
const j = await api('/api/usuarios', json('POST', Object.assign({ usuario: id, password_inicial: f.pass.value }, d)));
|
|
const pw = f.pass.value;
|
|
dlg.querySelector('.dcuerpo').innerHTML = '';
|
|
dlg.querySelector('.dcuerpo').append(el('div', { class: 'pass-una-vez', html: 'Acceso creado para <b>' + esc(j.usuario) + '</b>.<br>Contraseña inicial (no se vuelve a mostrar): <code>' + esc(pw) + '</code><br><small>Pásasela en persona o por mensaje directo y dile que la cambie en Mi cuenta.</small>' }));
|
|
pie.innerHTML = ''; const ok = el('button', { type: 'button', text: 'Listo' }); ok.addEventListener('click', () => dlg.close()); pie.append(ok);
|
|
cargar(); return;
|
|
}
|
|
if (base) throw new Error('Marca «Crear acceso al sitio» para darle acceso.');
|
|
await api('/api/equipo', json('POST', Object.assign({ id }, d)));
|
|
dlg.close(); mensaje('Miembro ' + esc(d.nombre) + ' creado (sin acceso al sitio).', true); cargar();
|
|
}]]);
|
|
}
|
|
function editar(m) {
|
|
let f;
|
|
abrirDialogo('Editar ' + m.nombre, c => {
|
|
f = formMiembro(c, m, false);
|
|
const h = el('div', { class: 'hist' });
|
|
h.append(el('div', { html: '<b>Creado</b> por ' + esc(m.creado_por) + ' el ' + esc(fmt(m.creado_en)) + (m.tiene_login ? ' · tiene acceso al sitio' : ' · sin acceso al sitio')
|
|
+ (m.tiene_login && m.contrasena_inicial ? ' · <b>sigue con la contraseña inicial</b> (desde ' + esc(fmt(m.contrasena_cambiada_en)) + ')' : '') }));
|
|
(m.historial || []).slice().reverse().slice(0, 10).forEach(x => h.append(el('div', { html: esc(fmt(x.cuando)) + ' · <b>' + esc(x.quien) + '</b> · ' + esc(x.cambio) })));
|
|
c.append(h);
|
|
}, [['Guardar', async () => {
|
|
const d = leerMiembro(f); const cambios = {};
|
|
Object.keys(d).forEach(k => { if (d[k] !== (m[k] || '')) cambios[k] = d[k]; });
|
|
if (Object.keys(cambios).length) await api('/api/equipo/miembro?id=' + encodeURIComponent(m.id), json('PATCH', cambios));
|
|
dlg.close(); mensaje('Guardado.', true); cargar();
|
|
}]]);
|
|
}
|
|
async function desactivar(m) {
|
|
if (!confirm('¿Desactivar a ' + m.nombre + '? Deja de aparecer en las listas de responsables; sus pendientes y su historial se quedan.' + (m.tiene_login ? ' Su acceso al sitio NO se quita (usa «Quitar acceso» para eso).' : ''))) return;
|
|
try { await api('/api/equipo/miembro?id=' + encodeURIComponent(m.id), json('PATCH', { activo: false })); mensaje(m.nombre + ' desactivado.', true); cargar(); } catch (e) { mensaje(e.message, false); }
|
|
}
|
|
async function reactivar(m) {
|
|
try { await api('/api/equipo/miembro?id=' + encodeURIComponent(m.id), json('PATCH', { activo: true })); mensaje(m.nombre + ' reactivado.', true); cargar(); } catch (e) { mensaje(e.message, false); }
|
|
}
|
|
async function quitarAcceso(m) {
|
|
if (!confirm('¿Quitar el acceso al sitio a ' + m.nombre + ' (' + m.id + ')? Ya no podrá entrar y quedará como inactivo. Sus actas y pendientes se conservan.')) return;
|
|
try { await api('/api/usuario?u=' + encodeURIComponent(m.id), { method: 'DELETE' }); mensaje('Acceso de ' + m.id + ' retirado.', true); cargar(); } catch (e) { mensaje(e.message, false); }
|
|
}
|
|
$('bNuevo').addEventListener('click', () => nuevo(null));
|
|
$('fSitio').addEventListener('change', pintar);
|
|
cargar();
|
|
})();
|
|
</script>
|