247 lines
13 KiB
JavaScript
247 lines
13 KiB
JavaScript
/* Motor de actas HBOX — renderiza el formulario, sincroniza con el servidor y firma.
|
|
- Un acta real se envía UNA vez: al elegir una serie ya enviada, se carga llena y bloqueada.
|
|
- Solo el administrador (leo) ve el botón "Borrar acta (reabrir)".
|
|
- La serie PRUEBA no se bloquea (para practicar).
|
|
Tipos de ítem: check | medida | texto | radio | nota · 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 || '');
|
|
let admin = false;
|
|
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);
|
|
|
|
const nav = $('div', { style: 'display:flex;justify-content:space-between;margin:2px 0 8px' });
|
|
nav.append($('a', { class: 'volver', href: '../actas/', text: '← Volver a Actas' }));
|
|
nav.append($('a', { class: 'volver', href: '../salir.html', text: 'Cerrar sesión' }));
|
|
wrap.append(nav);
|
|
|
|
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.' }));
|
|
|
|
const datos = $('div', { class: 'datos' });
|
|
const serieSel = $('select', { id: 'f_serie' });
|
|
serieSel.append($('option', { value: '', text: '— elegir —' }));
|
|
serieSel.append($('option', { value: cfg.prefijo + '-PRUEBA', text: '⚙ PRUEBA (no cuenta)' }));
|
|
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);
|
|
|
|
const banner = $('div', { class: 'msg', id: 'f_banner' });
|
|
wrap.append(banner);
|
|
|
|
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);
|
|
});
|
|
|
|
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);
|
|
|
|
const barra = $('div', { class: 'barra' });
|
|
const estado = $('span', { class: 'estado', id: 'f_estado', text: 'Sin guardar' });
|
|
const limpiar = $('button', { class: 'limpiar', id: 'f_limpiar', text: 'Limpiar' });
|
|
const borrar = $('button', { class: 'limpiar', id: 'f_borrar', text: 'Borrar acta (reabrir)', style: 'display:none;border-color:var(--falla);color:var(--falla)' });
|
|
const btn = $('button', { class: 'enviar', id: 'f_enviar', text: 'Enviar acta firmada' });
|
|
const grp = $('div', { style: 'display:flex;gap:10px' }); grp.append(limpiar, borrar, btn);
|
|
barra.append(estado, grp);
|
|
document.body.append(barra);
|
|
|
|
// ---- helpers de estado ----
|
|
const conforme = () => document.getElementById('f_conforme');
|
|
function fields() { return wrap.querySelectorAll('[data-id]'); }
|
|
function setEnabled(on) {
|
|
fields().forEach(el => el.disabled = !on);
|
|
tecnico.disabled = !on; conforme().disabled = !on;
|
|
wrap.querySelectorAll('input[name=resultado]').forEach(el => el.disabled = !on);
|
|
}
|
|
function blankFields() {
|
|
fields().forEach(el => { if (el.type === 'checkbox') el.checked = false; else el.value = ''; });
|
|
const rr = wrap.querySelector('input[name=resultado]:checked'); if (rr) rr.checked = false;
|
|
conforme().checked = false; tecnico.value = '';
|
|
}
|
|
function fillFrom(a) {
|
|
tecnico.value = a.tecnico || '';
|
|
conforme().checked = !!a.conforme;
|
|
if (a.resultado) { const el = wrap.querySelector(`input[name=resultado][value="${a.resultado}"]`); if (el) el.checked = true; }
|
|
for (const k in (a.respuestas || {})) {
|
|
const el = wrap.querySelector(`[data-id="${k}"]`); if (!el) continue;
|
|
if (el.type === 'checkbox') el.checked = !!a.respuestas[k].val; else el.value = a.respuestas[k].val || '';
|
|
}
|
|
}
|
|
function showEditable() {
|
|
setEnabled(true);
|
|
limpiar.style.display = ''; btn.style.display = ''; borrar.style.display = 'none';
|
|
wrap.classList.remove('enviada'); banner.className = 'msg';
|
|
}
|
|
function showLocked(a) {
|
|
setEnabled(false);
|
|
limpiar.style.display = 'none'; btn.style.display = 'none';
|
|
borrar.style.display = admin ? '' : 'none';
|
|
banner.className = 'msg ok';
|
|
const f = (a._recibido || '').replace('T', ' ').slice(0, 16);
|
|
banner.innerHTML = '✓ <b>Acta ya enviada</b> por <b>' + (a.tecnico || '—') + '</b>'
|
|
+ (f ? ' el ' + f : '') + '. Registro final'
|
|
+ (a.resultado ? ' — resultado <b>' + a.resultado + '</b>' : '') + '.'
|
|
+ (admin ? '' : ' Para rehacerla, pide a Leo que la borre.');
|
|
estado.textContent = 'Bloqueada';
|
|
}
|
|
|
|
// ---- autoguardado local (solo para actas nuevas, no enviadas) ----
|
|
let t, locked = false;
|
|
function snapshot() {
|
|
const r = {};
|
|
fields().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: conforme().checked, resultado: rr ? rr.value : '', respuestas: r };
|
|
}
|
|
function guardarLocal() {
|
|
if (locked || !serieSel.value) 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 cargarLocal() {
|
|
let d; try { d = JSON.parse(localStorage.getItem(key()) || 'null'); } catch (e) {}
|
|
if (d) { fillFrom(d); estado.textContent = 'Borrador recuperado'; }
|
|
else estado.textContent = 'Nueva acta';
|
|
}
|
|
|
|
// ---- cargar una serie: consulta el servidor y bloquea si ya existe ----
|
|
async function loadSerie() {
|
|
msg.className = 'msg'; locked = false;
|
|
const s = serieSel.value;
|
|
blankFields(); showEditable();
|
|
if (!s) { estado.textContent = 'Elige el número de serie'; return; }
|
|
if (s.toUpperCase().indexOf('PRUEBA') >= 0) { cargarLocal(); estado.textContent = 'Modo prueba (no cuenta)'; return; }
|
|
estado.textContent = 'Consultando…';
|
|
let j = null;
|
|
try { const r = await fetch(`/api/acta?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(s)}`); j = await r.json(); } catch (e) {}
|
|
if (j && j.exists) { locked = true; fillFrom(j.acta); showLocked(j.acta); }
|
|
else { cargarLocal(); }
|
|
}
|
|
wrap.addEventListener('input', guardarLocal);
|
|
serieSel.addEventListener('change', loadSerie);
|
|
|
|
// ---- limpiar (solo borrador local; no aplica a enviadas) ----
|
|
limpiar.addEventListener('click', () => {
|
|
if (locked) return;
|
|
if (!confirm('¿Limpiar este formulario y empezar de nuevo?')) return;
|
|
try { localStorage.removeItem(key()); } catch (e) {}
|
|
blankFields(); msg.className = 'msg'; estado.textContent = 'Formulario limpio'; window.scrollTo(0, 0);
|
|
});
|
|
|
|
// ---- borrar / reabrir (solo admin) ----
|
|
borrar.addEventListener('click', async () => {
|
|
if (!admin) return;
|
|
if (!confirm('¿Borrar el acta de ' + serieSel.value + '? Quedará disponible para rehacerse.')) return;
|
|
borrar.disabled = true;
|
|
try {
|
|
const r = await fetch(`/api/acta?tipo=${encodeURIComponent(cfg.tipo)}&serie=${encodeURIComponent(serieSel.value)}`, { method: 'DELETE' });
|
|
const j = await r.json();
|
|
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
|
locked = false; blankFields(); showEditable();
|
|
msg.className = 'msg ok'; msg.textContent = 'Acta borrada. Ya puede rehacerse.';
|
|
estado.textContent = 'Reabierta';
|
|
} catch (e) { msg.className = 'msg err'; msg.textContent = 'No se pudo borrar: ' + e.message; }
|
|
borrar.disabled = false;
|
|
});
|
|
|
|
// ---- envío ----
|
|
btn.addEventListener('click', async () => {
|
|
if (locked) return;
|
|
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.status === 409) { // ya existía: recargar bloqueada
|
|
try { localStorage.removeItem(key()); } catch (e) {}
|
|
await loadSerie();
|
|
msg.className = 'msg err'; msg.textContent = j.error || 'Ya existe un acta para esta serie.';
|
|
btn.disabled = false; btn.textContent = 'Enviar acta firmada';
|
|
return;
|
|
}
|
|
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
|
try { localStorage.removeItem(key()); } catch (e) {}
|
|
msg.className = 'msg ok';
|
|
msg.innerHTML = '✓ Acta enviada y registrada como <b>' + d.serie + '</b>.';
|
|
if (!j.prueba) { locked = true; showLocked({ tecnico: d.tecnico, resultado: d.resultado, _recibido: new Date().toISOString() }); }
|
|
else { btn.disabled = false; btn.textContent = 'Enviar acta firmada'; estado.textContent = 'Prueba enviada'; }
|
|
window.scrollTo(0, document.body.scrollHeight);
|
|
} catch (e) {
|
|
fail('No se pudo enviar: ' + e.message + '. Revisa tu conexión (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); }
|
|
|
|
// ---- init: saber si soy admin, luego cargar la serie elegida ----
|
|
(async () => {
|
|
try { const r = await fetch('/api/whoami'); const j = await r.json(); admin = !!j.admin; } catch (e) {}
|
|
loadSerie();
|
|
})();
|
|
}
|
|
window.Acta = { render };
|
|
})();
|