/* 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 }; })();