hbox-docs/docs/formularios/acta.js
Leopoldo Rodríguez 57f67332eb actas: formularios de prueba V5/V4 + checklist de ensamble + backend
- backend Flask (backend/): recibe formularios firmados, guarda JSON por
  numero de serie en /srv/projects/drogheda/actas, indice CSV, GET /api/actas
  + systemd unit + README de instalacion (nginx /api proxy)
- formularios (docs/formularios/): acta.css/acta.js (motor con autoguardado
  y envio), V5 fiel al procedimiento verificado, V4 borrador adaptado,
  checklist de ensamble borrador; firma = nombre + conformidad
- docs/actas/index.md + guia-ensamble-qc.md; nav actualizada
- probado end-to-end (headless): envio + validacion + guardado por serie

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ4eAB6zjQT49RVwpgfKdt
2026-09-16 18:05:16 +00:00

157 lines
8.2 KiB
JavaScript

/* Motor de actas HBOX — renderiza el formulario desde una config y lo envía al servidor.
Uso: <script src="acta.js"></script> luego Acta.render(CONFIG)
Tipos de ítem: check | medida | texto | radio | nota
Autoguardado en localStorage por (tipo + serie). Envío: POST /api/actas */
(function () {
const $ = (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=>e.append(c)); return e; };
function render(cfg) {
const root = document.getElementById('acta');
const key = () => 'acta:' + cfg.tipo + ':' + (serieSel.value || '');
const wrap = $('div', { class: 'wrap' });
const head = $('header', { class: 'top' });
head.append($('h1', { text: cfg.titulo }));
head.append($('p', { text: cfg.subtitulo || 'Helgen Technologies · llena mientras pruebas la tarjeta' }));
wrap.append(head);
wrap.append($('a', { class: 'volver', href: '../actas/' , text: '← Volver a Actas' }));
if (cfg.borrador)
wrap.append($('div', { class: 'borrador', html: '⚠ <b>BORRADOR</b> — este formulario aún debe revisarse con ingeniería antes de usarlo como registro oficial.' }));
// datos de cabecera
const datos = $('div', { class: 'datos' });
const serieSel = $('select', { id: 'f_serie' });
serieSel.append($('option', { value: '', text: '— elegir —' }));
for (let i = 1; i <= (cfg.series || 15); i++) {
const s = cfg.prefijo + '-' + String(i).padStart(2, '0');
serieSel.append($('option', { value: s, text: s }));
}
const tecnico = $('input', { type: 'text', id: 'f_tecnico', placeholder: 'Nombre y apellido' });
const dS = $('div'); dS.append($('label', { text: 'Número de serie de la tarjeta' }), serieSel);
const dT = $('div'); dT.append($('label', { text: 'Técnico' }), tecnico);
datos.append(dS, dT);
const cab = $('section', { class: 'paso' }); cab.append(datos);
wrap.append(cab);
// secciones del procedimiento
cfg.secciones.forEach((sec, si) => {
const s = $('section', { class: 'paso' });
s.append($('h2', { text: sec.titulo }));
if (sec.sub) s.append($('p', { class: 'sub', text: sec.sub }));
(sec.items || []).forEach((it, ii) => {
const id = `s${si}_${ii}`;
const box = $('div', { class: 'item' });
if (it.aviso) box.append($('div', { class: 'aviso', html: it.aviso }));
if (it.tipo === 'check') {
const lab = $('label', { class: 'chk' });
lab.append($('input', { type: 'checkbox', 'data-id': id, 'data-lbl': it.txt }));
lab.append($('span', { class: 'txt', html: it.txt }));
box.append(lab);
} else if (it.tipo === 'medida') {
box.classList.add('medida');
box.append($('span', { class: 'txt', text: it.txt }));
if (it.esperado) box.append($('span', { class: 'esp', html: 'Esperado: ' + it.esperado }));
box.append($('input', { type: 'text', inputmode: 'decimal', 'data-id': id, 'data-lbl': it.txt, placeholder: 'medido' }));
if (it.u) box.append($('span', { class: 'u', text: it.u }));
} else if (it.tipo === 'texto') {
box.append($('label', { text: it.txt }));
box.append($('textarea', { rows: '2', 'data-id': id, 'data-lbl': it.txt, placeholder: it.ph || '' }));
} else if (it.tipo === 'nota') {
box.append($('div', { class: 'nota-item', html: it.txt }));
}
s.append(box);
});
wrap.append(s);
});
// firma / conformidad
const firma = $('section', { class: 'paso firma' });
firma.append($('h2', { text: 'Cierre y firma' }));
firma.append($('p', { class: 'sub', text: 'Al enviar, quedas registrado como responsable de esta prueba.' }));
const res = $('div', { class: 'resultado' });
const rOK = $('label', { class: 'ok' }); rOK.append($('input', { type: 'radio', name: 'resultado', value: 'OK' }), document.createTextNode('APROBADA'));
const rNO = $('label', { class: 'falla' }); rNO.append($('input', { type: 'radio', name: 'resultado', value: 'FALLA' }), document.createTextNode('FALLA'));
res.append(rOK, rNO);
firma.append($('label', { text: 'Resultado final' }), res);
const conf = $('label', { class: 'chk', style: 'margin-top:14px' });
conf.append($('input', { type: 'checkbox', id: 'f_conforme' }));
conf.append($('span', { class: 'txt', html: 'Confirmo que <b>seguí el procedimiento completo</b> y que los datos anotados son verídicos.' }));
firma.append(conf);
wrap.append(firma);
const msg = $('div', { class: 'msg', id: 'f_msg' });
wrap.append(msg);
root.append(wrap);
// barra fija de envío
const barra = $('div', { class: 'barra' });
const estado = $('span', { class: 'estado', id: 'f_estado', text: 'Sin guardar' });
const btn = $('button', { class: 'enviar', id: 'f_enviar', text: 'Enviar acta firmada' });
barra.append(estado, btn);
document.body.append(barra);
// ---- autoguardado ----
function snapshot() {
const r = {};
wrap.querySelectorAll('[data-id]').forEach(el => {
r[el.dataset.id] = { lbl: el.dataset.lbl, val: el.type === 'checkbox' ? el.checked : el.value };
});
const rr = wrap.querySelector('input[name=resultado]:checked');
return { tipo: cfg.tipo, serie: serieSel.value, tecnico: tecnico.value,
conforme: document.getElementById('f_conforme').checked,
resultado: rr ? rr.value : '', respuestas: r };
}
let t;
function guardar() {
if (!serieSel.value) { estado.textContent = 'Elige el número de serie'; return; }
clearTimeout(t);
t = setTimeout(() => {
try { localStorage.setItem(key(), JSON.stringify(snapshot())); } catch (e) {}
estado.textContent = 'Guardado local ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' });
}, 400);
}
function cargar() {
let d; try { d = JSON.parse(localStorage.getItem(key()) || 'null'); } catch (e) {}
if (!d) { estado.textContent = 'Nueva acta'; return; }
tecnico.value = d.tecnico || '';
document.getElementById('f_conforme').checked = !!d.conforme;
if (d.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${d.resultado}"]`); if (el) el.checked = true; }
for (const k in (d.respuestas || {})) {
const el = wrap.querySelector(`[data-id="${k}"]`); if (!el) continue;
if (el.type === 'checkbox') el.checked = !!d.respuestas[k].val; else el.value = d.respuestas[k].val || '';
}
estado.textContent = 'Borrador recuperado';
}
wrap.addEventListener('input', guardar);
serieSel.addEventListener('change', cargar);
// ---- envío ----
btn.addEventListener('click', async () => {
const d = snapshot();
if (!d.serie) return fail('Elige el número de serie de la tarjeta.');
if (!d.tecnico.trim()) return fail('Escribe tu nombre.');
if (!d.resultado) return fail('Marca el resultado final (Aprobada o Falla).');
if (!d.conforme) return fail('Debes confirmar que seguiste el procedimiento.');
btn.disabled = true; btn.textContent = 'Enviando…';
try {
const r = await fetch('/api/actas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(d) });
const j = await r.json();
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
msg.className = 'msg ok';
msg.innerHTML = '✓ Acta enviada y registrada como <b>' + d.serie + '</b>. Puedes cerrar esta página.';
try { localStorage.removeItem(key()); } catch (e) {}
wrap.classList.add('enviada'); estado.textContent = 'Enviada';
btn.textContent = 'Enviada ✓'; window.scrollTo(0, document.body.scrollHeight);
} catch (e) {
fail('No se pudo enviar: ' + e.message + '. Revisa tu conexión e inténtalo otra vez (tu borrador está guardado).');
btn.disabled = false; btn.textContent = 'Enviar acta firmada';
}
});
function fail(m) { msg.className = 'msg err'; msg.textContent = m; window.scrollTo(0, document.body.scrollHeight); }
cargar();
}
window.Acta = { render };
})();