hbox-docs/docs/tableros/actividad.js

160 lines
8.8 KiB
JavaScript

/* Actividad: lo que pasó en el tablero de pendientes y en las actas (GET /api/actividad).
No hay bitácora aparte que mantener: el servidor junta el historial de cada pendiente y las
actas recibidas (las series PRUEBA no cuentan). Lo usan la página de inicio y la Bitácora.
Uso: Actividad.montar(contenedor, { dias: 7, limite: 6, porDia: false });
Necesita tableros/kanban.js (avatares y nombres del equipo) y tableros/tablero.css. */
(function () {
const ESTADOS = { 'por-hacer': 'Por hacer', 'en-curso': 'En curso', 'bloqueado': 'Bloqueado', 'hecho': 'Hecho' };
const ACTAS = {
'prueba-v5': 'tarjeta V5', 'prueba-v4': 'tarjeta V4', 'modulo-ard': 'ArduSimple', 'modulo-4g': 'módem 4G',
'integracion': 'integración', 'ensamble': 'ensamble', 'reflasheo': 'reflasheo', 'prueba-ethernet-serie': 'Ethernet y serie',
'envio': 'envío', 'recepcion': 'recepción', 'instalacion-campo': 'instalación', 'diagnostico-campo': 'diagnóstico en campo',
'diagnostico-banco': 'diagnóstico en banco', 'dano': 'daño', 'prueba-bateria-modem': 'prueba de batería del módem', 'prueba-sitio': 'prueba en sitio'
};
const CAMPOS = { titulo: 'el título', descripcion: 'la descripción', pista: 'la pista', responsable: 'el responsable',
fecha_limite: 'la fecha', version_tarjeta: 'la tarjeta', enlaces: 'las ligas', etiquetas: 'las etiquetas',
bloqueada_por: 'de qué depende' };
const DIAS = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
const MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
const el = (t, a, ...k) => {
const e = document.createElement(t);
if (a) for (const p in a) { if (p === 'text') e.textContent = a[p]; else if (p === 'html') e.innerHTML = a[p]; else e.setAttribute(p, a[p]); }
k.forEach(c => c != null && e.append(typeof c === 'string' ? document.createTextNode(c) : c));
return e;
};
const corto = (s, n) => { s = String(s || ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
const dia = iso => (iso || '').slice(0, 10);
function hoyISO(delta) { const d = new Date(Date.now() + (delta || 0) * 86400000); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); }
// «Hoy, 10:52» · «Ayer, 19:30» · «lun 19, 17:10» · «3 oct, 09:30»
function cuando(iso) {
if (!iso) return '';
const f = dia(iso), h = iso.slice(11, 16);
if (f === hoyISO()) return 'Hoy' + (h ? ', ' + h : '');
if (f === hoyISO(-1)) return 'Ayer' + (h ? ', ' + h : '');
const d = new Date(f + 'T12:00:00');
const hace = (Date.now() - d.getTime()) / 86400000;
const base = hace < 6.5 ? DIAS[d.getDay()].slice(0, 3) + ' ' + d.getDate() : d.getDate() + ' ' + MES[d.getMonth()];
return base + (h ? ', ' + h : '');
}
// encabezado de día en la Bitácora: «Hoy» · «Ayer» · «Lunes 19 de octubre»
function tituloDia(f) {
if (f === hoyISO()) return 'Hoy';
if (f === hoyISO(-1)) return 'Ayer';
const d = new Date(f + 'T12:00:00');
const s = DIAS[d.getDay()] + ' ' + d.getDate() + ' de ' + MESES[d.getMonth()];
return s.charAt(0).toUpperCase() + s.slice(1);
}
// Lo que dice el historial («estado: por-hacer → en-curso; fecha_limite: … → …») en una frase.
function frasePendiente(e) {
const c = String(e.cambio || '');
if (/^borró el pendiente/i.test(c)) return { verbo: 'borró', fin: '' };
const m = c.match(/^(?:creado en|creó el pendiente)\s*([a-z-]*)/i);
if (m) return { verbo: 'creó', fin: m[1] && m[1] !== 'por-hacer' && ESTADOS[m[1]] ? ' en ' + ESTADOS[m[1]] : '' };
const partes = c.split(/;\s*/).map(p => p.match(/^([a-z_]+):\s*(.*?)\s*→\s*(.*)$/)).filter(Boolean);
if (!partes.length) return { verbo: 'comentó en', nota: c };
const est = partes.find(p => p[1] === 'estado');
if (est) {
const a = (est[3] || '').split(' — ')[0].trim();
if (a === 'hecho') return { verbo: 'terminó', fin: '' };
return { verbo: 'movió', fin: ' a ' + (ESTADOS[a] || a) };
}
const campos = partes.map(p => p[1]);
if (campos.length === 1 && campos[0] === 'fecha_limite') {
const f = (partes[0][3] || '').trim();
const fm = f.match(/^(\d{4})-(\d{2})-(\d{2})/);
return { verbo: 'cambió la fecha de', fin: fm ? ' al ' + (+fm[3]) + ' ' + MES[+fm[2] - 1] : ' (sin fecha)' };
}
if (campos.length === 1 && CAMPOS[campos[0]]) return { verbo: 'cambió ' + CAMPOS[campos[0]] + ' de', fin: '' };
return { verbo: 'editó', fin: '' };
}
function avatar(id, nombreLibre) {
if (window.Kanban) return id ? window.Kanban.avatar(id) : window.Kanban.avatar(null, nombreLibre || '?');
return el('span', { class: 'av', text: String(id || nombreLibre || '?').slice(0, 2).toUpperCase() });
}
function quienEs(login, equipo) {
const id = String(login || '').toLowerCase().trim();
const eq = (window.Kanban && window.Kanban.equipo) || {};
if (id && eq[id] && window.Kanban.nombreCorto) return { id, nombre: window.Kanban.nombreCorto(id) };
if (id && (eq[id] || equipo[id])) return { id, nombre: ((eq[id] || {}).nombre || equipo[id] || id).split(' ')[0] };
return { id: null, nombre: login || 'Alguien' };
}
function evento(e, equipo, opts) {
const li = el('li', { class: 'act act-' + e.tipo });
const q = e.tipo === 'acta' ? quienEs(e.quien, equipo) : quienEs(e.quien, equipo);
if (e.tipo === 'acta' && !q.id && e.tecnico) q.nombre = e.tecnico;
li.append(q.id ? avatar(q.id) : avatar(null, q.nombre));
const t = el('div', { class: 'act-t' });
const quien = el('b', { text: q.nombre });
if (e.tipo === 'pendiente') {
const f = frasePendiente(e);
// un pendiente borrado ya no está en el tablero: su título va sin liga
const a = e.borrado ? el('span', { class: 'act-borrado', title: 'Pendiente borrado', text: '«' + corto(e.titulo, opts.corto || 70) + '»' })
: el('a', { href: '/tableros/pendientes/?id=' + encodeURIComponent(e.id || ''), text: '«' + corto(e.titulo, opts.corto || 70) + '»' });
t.append(quien, ' ' + f.verbo + ' ', a);
if (f.fin) t.append(f.fin);
if (f.nota) t.append(el('span', { class: 'act-nota', text: corto(f.nota, 160) }));
} else {
const a = el('a', { href: '/tableros/unidad/?serie=' + encodeURIComponent(e.serie || ''), text: e.serie || 'sin serie' });
t.append(quien, ' registró el acta de ' + (ACTAS[e.acta] || e.acta) + ' de ', a);
if (e.resultado) t.append(' ', el('span', { class: 'act-res' + (/FALLA|BAJA|DA[ÑN]/i.test(e.resultado) ? ' mal' : ''), text: e.resultado }));
}
// en la Bitácora el día ya va en el encabezado: solo la hora
li.append(t, el('time', { class: 'act-f', datetime: e.cuando || '', text: opts.porDia ? String(e.cuando || '').slice(11, 16) : cuando(e.cuando) }));
return li;
}
async function montar(cont, opts) {
opts = opts || {};
const dias = opts.dias || 7, limite = opts.limite || 30;
cont.classList.add('act-caja');
cont.innerHTML = '';
cont.append(el('p', { class: 'vacio', text: 'Cargando…' }));
let j;
try {
const r = await fetch('/api/actividad?dias=' + dias + '&limite=' + limite, { cache: 'no-store' });
j = await r.json();
if (!r.ok || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status));
} catch (err) {
cont.innerHTML = '';
cont.append(el('p', { class: 'vacio', text: 'No se pudo cargar la actividad (' + err.message + ').' }));
return null;
}
if (window.Kanban && window.Kanban.cargarEquipo) { try { await window.Kanban.cargarEquipo(); } catch (e) { /* sin equipo: iniciales del login */ } }
const equipo = j.equipo || {};
const eventos = j.eventos || [];
cont.innerHTML = '';
if (!eventos.length) {
cont.append(el('p', { class: 'vacio', text: 'Sin movimientos en los últimos ' + dias + ' días.' }));
return j;
}
if (opts.porDia) {
let actual = null, lista = null;
eventos.forEach(e => {
const f = dia(e.cuando);
if (f !== actual) {
actual = f;
cont.append(el('h3', { class: 'act-dia', text: tituloDia(f) }));
lista = el('ol', { class: 'act-lista' });
cont.append(lista);
}
lista.append(evento(e, equipo, opts));
});
} else {
const lista = el('ol', { class: 'act-lista' });
eventos.forEach(e => lista.append(evento(e, equipo, opts)));
cont.append(lista);
}
if (j.total > eventos.length && opts.mas) cont.append(el('a', { class: 'act-mas', href: opts.mas, text: (j.total - eventos.length) + ' movimientos más en la Bitácora' }));
return j;
}
window.Actividad = { montar, cuando, frasePendiente };
})();