hbox-docs/docs/diagnostico/tablero-campo.html

201 lines
10 KiB
HTML

<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tablero de campo — diagnósticos HBOX</title>
<style>
:root{
--naranja:#e8590c; --tinta:#1d1c19; --tinta2:#5a564d; --linea:#e6e1d6;
--papel:#faf8f4; --tarjeta:#fff; --superf:#fcfcfb;
--ok:#2f9e44; --ok-bg:#ebfbee; --falla:#e03131; --falla-bg:#fff5f5;
--pend:#adb5bd; --pend-bg:#f4f2ee; --info:#1971c2; --info-bg:#e7f1fb;
}
@media(prefers-color-scheme:dark){:root:not([data-theme=light]){
--papel:#191b1e;--tinta:#e6e1d6;--tinta2:#a09a8d;--linea:#33363b;--tarjeta:#1f2226;--superf:#1b1d20;
--ok:#69db7c;--ok-bg:#1b2a1f;--falla:#ff8787;--falla-bg:#2c1d1d;--pend:#5c636a;--pend-bg:#26282c;--info:#74c0fc;--info-bg:#182734;}}
*{box-sizing:border-box}
body{margin:0;background:var(--papel);color:var(--tinta);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
.wrap{max-width:1020px;margin:0 auto;padding:18px 16px 70px}
header.top{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;
background:#2C3C5B;color:#fff;padding:16px;border-radius:12px;margin-bottom:16px}
header.top h1{margin:0;font-size:20px}
header.top .meta{color:#d7d2c8;font-size:13px}
a.volver{color:#fff;font-size:13px;text-decoration:none;opacity:.85}
button{background:var(--naranja);color:#fff;border:0;border-radius:9px;padding:9px 16px;font-size:14px;font-weight:600;cursor:pointer}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:18px}
.kpi{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:14px 16px}
.kpi .n{font-size:30px;font-weight:700;line-height:1}
.kpi .l{color:var(--tinta2);font-size:13px;margin-top:4px}
section.bloque{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:16px;margin:14px 0}
section.bloque h2{margin:0 0 2px;font-size:17px}
.resumen{color:var(--tinta2);font-size:13.5px;margin:0 0 12px}
.unidad{border:1px solid var(--linea);border-radius:11px;padding:12px 14px;margin:10px 0;background:var(--superf)}
.unidad .cab{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline;justify-content:space-between}
.unidad .s{font-weight:700;font-size:15px}
.unidad .m{color:var(--tinta2);font-size:13px}
.pill{font-size:11.5px;font-weight:700;padding:3px 10px;border-radius:99px;white-space:nowrap}
.pill.ok{background:var(--ok-bg);color:var(--ok)}
.pill.falla{background:var(--falla-bg);color:var(--falla)}
.pill.pend{background:var(--pend-bg);color:var(--tinta2)}
.pill.info{background:var(--info-bg);color:var(--info)}
.visita{border-top:1px solid var(--linea);margin-top:10px;padding-top:10px;font-size:13.5px}
.visita .f{color:var(--tinta2);font-size:12.5px}
.visita .cl{font-weight:700}
.fotos{display:flex;flex-wrap:wrap;gap:7px;margin-top:8px}
.logs{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
.logs a{font-size:12.5px;font-weight:600;color:var(--info);text-decoration:none;
border:1px solid var(--linea);border-radius:7px;padding:4px 9px;background:var(--superf)}
.fotos a{display:block;width:74px;height:74px;border-radius:8px;overflow:hidden;border:1px solid var(--linea)}
.fotos img{width:100%;height:100%;object-fit:cover;display:block;background:var(--pend-bg)}
details{margin-top:8px}
summary{cursor:pointer;color:var(--naranja);font-size:13px;font-weight:600}
table.det{border-collapse:collapse;width:100%;font-size:13px;margin-top:8px}
table.det td{padding:5px 8px 5px 0;border-bottom:1px solid var(--linea);vertical-align:top}
table.det td.k{color:var(--tinta2);width:45%}
.vacio{color:var(--tinta2);font-size:13.5px;font-style:italic}
.err{background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla);padding:10px 14px;border-radius:10px}
</style>
</head>
<body>
<div class="wrap">
<header class="top">
<div><h1>Tablero de campo — diagnósticos HBOX</h1><div class="meta" id="meta">Cargando…</div></div>
<div style="display:flex;gap:12px;align-items:center">
<a class="volver" href="../">← Documentación</a>
<a class="volver" href="../formularios/diagnostico.html">Nueva acta de diagnóstico</a>
<button id="refrescar">Actualizar</button>
</div>
</header>
<div id="cont"></div>
</div>
<script src="../formularios/flota.js"></script>
<script>
const TIPOS = ['diagnostico-campo','diagnostico-banco'];
const TIPO = 'diagnostico-campo';
const cont = document.getElementById('cont');
const el = (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;};
const fecha = s => (s||'').replace('T',' ').slice(0,16);
function pill(v){
const t = String(v.accion||'');
if (/^Instalada y reportando/.test(t)) return ['ok','Operando'];
if (/^Instalada/.test(t)) return ['falla','Instalada sin resolver'];
if (/Querétaro/.test(t)) return ['info','En camino a QRO'];
if (/taller/.test(t)) return ['info','En taller'];
if (/Reemplazada/.test(t)) return ['ok','Reemplazada'];
return ['pend', v.resultado || '—'];
}
async function detalle(tipo, serie, archivo, caja){
caja.textContent = 'Cargando…';
try{
const r = await fetch(`/api/acta?tipo=${encodeURIComponent(tipo)}&serie=${encodeURIComponent(serie)}&archivo=${encodeURIComponent(archivo)}`);
const j = await r.json();
if(!j.exists) throw new Error('no encontrada');
const t = el('table',{class:'det'});
const res = j.acta.respuestas || {};
Object.keys(res).forEach(k=>{
const v = res[k]; if(v.val===''||v.val===false||v.val==null) return;
t.append(el('tr',null, el('td',{class:'k',text:(v.lbl||k).replace(/<[^>]+>/g,'')}),
el('td',{text: v.val===true ? '✓' : String(v.val)})));
});
caja.innerHTML=''; caja.append(t);
}catch(e){ caja.textContent = 'No se pudo cargar el detalle.'; }
}
function render(actas){
cont.innerHTML='';
const porUnidad = {};
actas.filter(a=>TIPOS.includes(a.tipo)).forEach(a=>{ (porUnidad[a.serie]=porUnidad[a.serie]||[]).push(a); });
const flota = (window.FLOTA||[]).slice();
Object.keys(porUnidad).forEach(s=>{ if(!flota.some(u=>u.serie===s)) flota.push({serie:s, etiqueta:s, maquina:'(fuera del registro de flota)', terminal:'Sin clasificar'}); });
const visitas = actas.filter(a=>TIPOS.includes(a.tipo));
const conVisita = Object.keys(porUnidad).length;
const operando = flota.filter(u=>{const v=(porUnidad[u.serie]||[])[0]; return v && pill(v)[0]==='ok';}).length;
const kp = el('div',{class:'kpis'});
[[flota.length,'Unidades en el registro'],[visitas.length,'Diagnósticos recibidos'],
[conVisita,'Unidades ya diagnosticadas'],[operando,'Reportadas como operando']]
.forEach(([n,l])=>kp.append(el('div',{class:'kpi'}, el('div',{class:'n',text:String(n)}), el('div',{class:'l',text:l}))));
cont.append(kp);
const grupos = {};
flota.forEach(u=>{ (grupos[u.terminal||'Sin clasificar']=grupos[u.terminal||'Sin clasificar']||[]).push(u); });
Object.keys(grupos).sort().forEach(g=>{
const b = el('section',{class:'bloque'});
b.append(el('h2',{text:g}));
b.append(el('p',{class:'resumen',text:grupos[g].length+' unidad(es) registradas en esta terminal.'}));
grupos[g].forEach(u=>{
const vs = (porUnidad[u.serie]||[]);
const box = el('div',{class:'unidad'});
const cab = el('div',{class:'cab'});
cab.append(el('div',null,
el('div',{class:'s'}, el('a',{href:'../unidad.html?serie='+encodeURIComponent(u.serie),
style:'color:inherit;text-decoration:none;border-bottom:1px dotted currentColor',
title:'Ver el expediente completo',text:u.serie})),
el('div',{class:'m',text:(u.maquina||'')+(u.nota?' · '+u.nota:'')})));
const [cls,txt] = vs.length ? pill(vs[0]) : ['pend','Sin diagnóstico'];
if(DANOS[u.serie]) cab.append(el('span',{class:'pill falla',text:DANOS[u.serie].marca==='BAJA'?'⛔ BAJA':'⚠ DAÑADA'}));
cab.append(el('span',{class:'pill '+cls,text:txt}));
box.append(cab);
if(!vs.length) box.append(el('div',{class:'visita vacio',text:'Todavía no hay ningún diagnóstico registrado para esta unidad.'}));
vs.forEach(v=>{
const d = el('div',{class:'visita'});
d.append(el('div',{class:'f',text:(v.tipo==='diagnostico-banco'?'🔬 banco · ':'🚜 campo · ')+fecha(v.recibido)+' · '+(v.tecnico||'—')+(v.motivo?' · '+v.motivo:'')}));
if(v.clasificacion) d.append(el('div',{class:'cl',text:v.clasificacion}));
if(v.accion) d.append(el('div',{text:'→ '+v.accion}));
if(v.fotos && v.fotos.length){
const f = el('div',{class:'fotos'});
v.fotos.forEach(r=>{
const a = el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(r),target:'_blank'});
a.append(el('img',{src:'/api/adjunto?ruta='+encodeURIComponent(r),loading:'lazy'}));
f.append(a);
});
d.append(f);
}
if(v.archivos && v.archivos.length){
const g = el('div',{class:'logs'});
v.archivos.forEach(a=>{
const x = el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(a.ruta),target:'_blank',
text:'📄 '+(a.nombre||'captura')});
g.append(x);
});
d.append(g);
}
const det = el('details');
det.append(el('summary',{text:'Ver todas las respuestas'}));
const caja = el('div'); det.append(caja);
det.addEventListener('toggle',()=>{ if(det.open && !caja.dataset.listo){ caja.dataset.listo='1'; detalle(v.tipo, v.serie, v.archivo_n, caja); } });
d.append(det);
box.append(d);
});
b.append(box);
});
cont.append(b);
});
}
let DANOS = {};
async function cargar(){
document.getElementById('meta').textContent = 'Cargando…';
try{
try{ const d=await (await fetch('/api/danos',{cache:'no-store'})).json(); DANOS={}; (d.series||[]).forEach(x=>DANOS[x.serie]=x); }catch(e){}
const r = await fetch('/api/actas',{cache:'no-store'});
const j = await r.json();
render(j.actas||[]);
document.getElementById('meta').textContent = 'Actualizado ' + new Date().toLocaleString('es-MX',{dateStyle:'short',timeStyle:'short'});
}catch(e){
cont.innerHTML=''; cont.append(el('div',{class:'err',text:'No se pudo leer el servidor de actas: '+e.message}));
document.getElementById('meta').textContent = 'Error';
}
}
document.getElementById('refrescar').addEventListener('click',cargar);
cargar();
</script>
</body>
</html>