198 lines
10 KiB
HTML
198 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:var(--tinta);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'];
|
|
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);
|
|
});
|
|
}
|
|
|
|
async function cargar(){
|
|
document.getElementById('meta').textContent = 'Cargando…';
|
|
try{
|
|
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>
|