hbox-docs/docs/tableros/standup-tablero.js

141 lines
11 KiB
JavaScript

/* Standup diario (27-oct-2026). Servidor: backend/standup.py.
Mi standup: GET /api/standup/hoy → instrucciones del bot + preguntas por pendiente; POST /api/standup/respuesta.
Si la persona lo deja marcado, mueve sus pendientes con PATCH /api/tarea?id= (queda a su nombre en el historial).
Equipo (Leo, Adriana, el bot): GET /api/standup?fecha= . La dirección guarda la vista: ?vista=equipo&fecha=AAAA-MM-DD. */
(function () {
const $ = id => document.getElementById(id);
const esc = s => { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; };
const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
const DIA = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
const largo = f => { const d = new Date(f + 'T12:00:00'); return isNaN(d) ? f : DIA[d.getDay()] + ' ' + d.getDate() + ' ' + MES[d.getMonth()]; };
const hora = iso => (iso || '').slice(11, 16);
const ESTADO_T = { 'por-hacer': 'Por hacer', 'en-curso': 'En curso', bloqueado: 'Bloqueado', hecho: 'Hecho' };
const OPC = [['hecho', 'Hecho'], ['sigo', 'Sigo con él'], ['bloqueado', 'Bloqueado'], ['no-empece', 'No empecé']];
const A_TABLERO = { hecho: 'hecho', sigo: 'en-curso', bloqueado: 'bloqueado' };
const params = new URLSearchParams(location.search);
let vista = params.get('vista') === 'equipo' ? 'equipo' : 'mio', puedeEquipo = false;
async function api(url, op) {
const r = await fetch(url, Object.assign({ credentials: 'same-origin', cache: 'no-store' }, op || {}));
const j = await r.json().catch(() => ({ ok: false, error: 'HTTP ' + r.status }));
if (!r.ok || !j.ok) { const e = new Error(j.error || ('HTTP ' + r.status)); e.status = r.status; throw e; }
return j;
}
const json = (method, body) => ({ method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
function aviso(t, bien) { const a = $('aviso'); a.className = 'msg ' + (bien ? 'ok' : 'err'); a.innerHTML = t; a.scrollIntoView({ block: 'nearest' }); }
function botones() {
$('vistas').innerHTML = '<button class="' + (vista === 'mio' ? '' : 'sec') + '" data-v="mio">Mi standup</button>'
+ (puedeEquipo ? '<button class="' + (vista === 'equipo' ? '' : 'sec') + '" data-v="equipo">Equipo</button>' : '');
$('vistas').querySelectorAll('button').forEach(b => b.addEventListener('click', () => {
vista = b.dataset.v; const p = new URLSearchParams(location.search); p.set('vista', vista); history.replaceState(null, '', '?' + p);
botones(); (vista === 'equipo' ? equipo : mio)();
}));
}
async function mio() {
const C = $('cont'); C.innerHTML = '<p class="vacio">Cargando…</p>';
let j;
try { j = await api('/api/standup/hoy'); } catch (e) { C.innerHTML = '<div class="err">' + esc(e.message) + '</div>'; return; }
const s = j.standup;
$('meta').textContent = largo(s.fecha) + (s.fecha !== j.hoy ? ' (pendiente de contestar)' : '') + ' · ' + (s.nombre || s.usuario);
let h = '';
if (s.respuesta) {
h += '<div class="su-hecho"><b>Ya contestaste</b> a las ' + esc(hora(s.respuesta.enviada_en)) + '. Gracias. Puedes corregir abajo y volver a enviar.</div>';
}
if ((s.instrucciones || []).length || s.nota_bot) {
h += '<section class="su-bot"><h2>Tus instrucciones de hoy</h2><ol>' + (s.instrucciones || []).map(i =>
'<li>' + esc(i.texto) + (i.liga ? ' — <a href="' + esc(i.liga) + '">abrir</a>' : '')
+ (i.tarea_id ? ' <span class="pill pend">' + esc(i.tarea_id) + '</span>' : '') + '</li>').join('') + '</ol>'
+ (s.nota_bot ? '<p class="nota">' + esc(s.nota_bot) + '</p>' : '')
+ '<p class="nota">Las preparó ' + esc(s.preparado_por || 'el bot') + ' el ' + esc(largo((s.preparado_en || '').slice(0, 10))) + ' a las ' + esc(hora(s.preparado_en)) + ', con el tablero de ese momento.</p></section>';
} else {
h += '<p class="vacio">Hoy el bot no dejó instrucciones; contesta sobre tus pendientes abiertos.</p>';
}
const previas = {};
((s.respuesta || {}).por_tarea || []).forEach(p => { previas[p.tarea_id] = p; });
const preg = s.preguntas || [];
h += '<section class="bloque"><h2>Tus pendientes</h2>';
if (!preg.length) h += '<p class="vacio">No tienes pendientes abiertos a tu nombre. Si estás trabajando en algo que no está en el tablero, cuéntalo abajo.</p>';
preg.forEach((p, i) => {
const prev = previas[p.tarea_id] || {};
h += '<div class="su-p" data-id="' + esc(p.tarea_id) + '"><div class="su-p__t"><a href="/tableros/pendientes/?id=' + encodeURIComponent(p.tarea_id) + '">' + esc(p.titulo) + '</a></div>'
+ '<div class="su-p__m">' + esc(p.tarea_id) + ' · ' + esc(ESTADO_T[p.estado_tablero] || p.estado_tablero || '') + (p.vence ? ' · vence ' + esc(p.vence) : '') + '</div>'
+ (p.pregunta ? '<div class="su-p__q">' + esc(p.pregunta) + '</div>' : '')
+ '<div class="su-op">' + OPC.map(o => '<label class="' + (o[0] === 'bloqueado' ? 'b' : '') + '"><input type="radio" name="p' + i + '" value="' + o[0] + '"' + (prev.estado === o[0] ? ' checked' : '') + '> ' + o[1] + '</label>').join('') + '</div>'
+ '<input type="text" class="nota" maxlength="600" placeholder="Una línea: qué avanzaste o qué te falta" value="' + esc(prev.nota || '') + '"></div>';
});
h += '</section>';
const r = s.respuesta || {};
h += '<section class="bloque su-q"><h2>Las tres preguntas</h2>'
+ '<label for="qHecho">¿Qué terminaste desde el último standup?</label><textarea id="qHecho" maxlength="3000">' + esc(r.hecho || '') + '</textarea>'
+ '<label for="qFalta">¿Qué te falta? ¿Qué sigue hoy?</label><textarea id="qFalta" maxlength="3000">' + esc(r.falta || '') + '</textarea>'
+ '<label for="qBloq">¿Qué te bloquea o qué necesitas de alguien?</label><textarea id="qBloq" maxlength="3000" placeholder="Nada, si no hay. Si es de alguien, di de quién.">' + esc(r.bloqueos || '') + '</textarea>'
+ '<label for="qCom">Algo más (opcional)</label><textarea id="qCom" maxlength="3000">' + esc(r.comentario || '') + '</textarea>'
+ '<div class="su-fin"><button id="bEnviar">' + (s.respuesta ? 'Volver a enviar' : 'Enviar mi standup') + '</button>'
+ '<label><input type="checkbox" id="mover" checked> Actualizar mis pendientes en el tablero</label></div></section>';
C.innerHTML = h;
$('bEnviar').addEventListener('click', () => enviar(s, preg));
}
async function enviar(s, preg) {
const b = $('bEnviar'); b.disabled = true; b.textContent = 'Enviando…';
const por_tarea = [];
document.querySelectorAll('.su-p').forEach((d, i) => {
const o = d.querySelector('input[type=radio]:checked');
if (o) por_tarea.push({ tarea_id: d.dataset.id, estado: o.value, nota: d.querySelector('input.nota').value.trim() });
});
try {
await api('/api/standup/respuesta', json('POST', { fecha: s.fecha, por_tarea, hecho: $('qHecho').value.trim(), falta: $('qFalta').value.trim(),
bloqueos: $('qBloq').value.trim(), comentario: $('qCom').value.trim() }));
let movidos = 0, fallas = [];
if ($('mover').checked) {
for (const p of por_tarea) {
const q = preg.find(x => x.tarea_id === p.tarea_id) || {};
const destino = A_TABLERO[p.estado];
if (!destino || destino === q.estado_tablero) continue;
try {
await api('/api/tarea?id=' + encodeURIComponent(p.tarea_id), json('PATCH', { estado: destino, _nota: 'desde el standup' + (p.nota ? ': ' + p.nota : '') }));
movidos++;
} catch (e) { fallas.push(p.tarea_id + ' (' + e.message + ')'); }
}
}
aviso('<b>Listo, gracias.</b> ' + (movidos ? movidos + ' pendiente' + (movidos > 1 ? 's' : '') + ' movido' + (movidos > 1 ? 's' : '') + ' en el tablero. ' : '')
+ (fallas.length ? 'No pude mover: ' + esc(fallas.join(', ')) + ' — muévelos a mano en Pendientes.' : ''), !fallas.length);
document.querySelectorAll('.hg-standup').forEach(x => x.remove());
await mio();
} catch (e) { aviso(esc(e.message)); b.disabled = false; b.textContent = 'Enviar mi standup'; }
}
async function equipo(fecha) {
const C = $('cont'); C.innerHTML = '<p class="vacio">Cargando…</p>';
fecha = fecha || params.get('fecha') || '';
let j;
try { j = await api('/api/standup' + (fecha ? '?fecha=' + encodeURIComponent(fecha) : '')); } catch (e) { C.innerHTML = '<div class="err">' + esc(e.message) + '</div>'; return; }
$('meta').textContent = largo(j.fecha) + ' · contestaron ' + j.contestaron + ' de ' + j.total;
let h = '<div class="acciones" style="display:flex;gap:8px;align-items:center;margin:0 0 12px"><select id="fFecha">'
+ (j.fechas.length ? j.fechas : [j.fecha]).map(f => '<option value="' + f + '"' + (f === j.fecha ? ' selected' : '') + '>' + largo(f) + '</option>').join('') + '</select></div>';
if (!j.standups.length) h += '<p class="vacio">El bot no preparó standup ese día.</p>';
else {
const bloqueos = j.standups.filter(s => s.respuesta && (s.respuesta.bloqueos || (s.respuesta.por_tarea || []).some(p => p.estado === 'bloqueado')));
if (bloqueos.length) h += '<section class="bloque"><h2>Bloqueos</h2><ul>' + bloqueos.map(s => '<li><b>' + esc(s.nombre) + ':</b> <span class="bloq">'
+ esc(s.respuesta.bloqueos || (s.respuesta.por_tarea || []).filter(p => p.estado === 'bloqueado').map(p => p.tarea_id + (p.nota ? ' — ' + p.nota : '')).join('; ')) + '</span></li>').join('') + '</ul></section>';
h += '<div class="tblwrap"><table class="t su-eq"><thead><tr><th>Persona</th><th>Contestó</th><th>Terminó</th><th>Le falta</th><th>Pendientes</th></tr></thead><tbody>'
+ j.standups.sort((a, b) => (b.respuesta ? 1 : 0) - (a.respuesta ? 1 : 0) || String(a.nombre).localeCompare(b.nombre)).map(s => {
const r = s.respuesta;
return '<tr class="' + (r ? '' : 'sin') + '"><td><b>' + esc(s.nombre) + '</b></td><td>' + (r ? esc(hora(r.enviada_en)) : '—') + '</td><td>' + esc(r ? r.hecho : '') + '</td><td>' + esc(r ? r.falta : '') + '</td><td>'
+ (r ? (r.por_tarea || []).map(p => esc(p.tarea_id) + ': ' + esc(p.estado) + (p.nota ? ' — ' + esc(p.nota) : '')).join('<br>') : esc((s.preguntas || []).length + ' preguntas')) + '</td></tr>';
}).join('') + '</tbody></table></div>';
}
C.innerHTML = h;
$('fFecha').addEventListener('change', e => { const p = new URLSearchParams(location.search); p.set('fecha', e.target.value); history.replaceState(null, '', '?' + p); equipo(e.target.value); });
}
(async () => {
try { const w = await api('/api/whoami'); puedeEquipo = !!w.admin || ['leo', 'adriana', 'claude', 'gibranlp'].includes((w.user || '').toLowerCase()); } catch (e) {}
if (vista === 'equipo' && !puedeEquipo) vista = 'mio';
botones();
(vista === 'equipo' ? equipo : mio)();
})();
})();