Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
157 lines
8.4 KiB
JavaScript
157 lines
8.4 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'
|
|
};
|
|
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 || '');
|
|
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);
|
|
const a = 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 };
|
|
})();
|