hbox-docs/docs/tableros/instalaciones.js
Leopoldo Rodríguez 02521e14f4 Inventario por sitio: pestañas, envíos en camino y kit por instalación
Una pestaña por sitio (ICAVE, TIMSA, EIT, LCT, Querétaro): lo que hay, lo que va en camino
y lo que falta del kit para instalar. Movimientos con sitio, envío/recepción, origen «fuera».
Lote 2026-10-23_inventario-sitios.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
2026-10-23 13:33:51 -06:00

365 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Tablero de Instalaciones por sitio (22-oct-2026).
Una tarjeta por unidad en cada sitio: Enviada → Recibida en sitio → Instalada → Probada en sitio →
En operación, y aparte el carril «Con falla». Todo sale de las actas (GET /api/instalaciones,
backend/instalaciones.py); aquí no se captura nada. El color, la meta y la nota de cada sitio vienen
de docs/datos/proyecto.json (los mismos de la página de inicio).
La dirección guarda el sitio: ?sitio=eit (sin sitio = resumen de todos; ?sitio=lct también sirve).
Los botones abren la siguiente acta con la unidad, el sitio y el vehículo ya puestos (acta.js, precarga). */
(function () {
const raiz = document.getElementById('ins');
if (!raiz) return;
const $ = id => document.getElementById(id);
const el = (t, a, ...k) => {
const e = document.createElement(t);
if (a) for (const p in a) { if (p === 'text') e.textContent = a[p]; else if (p === 'html') e.innerHTML = a[p]; else if (a[p] != null) e.setAttribute(p, a[p]); }
k.forEach(c => c != null && e.append(typeof c === 'string' ? document.createTextNode(c) : c));
return e;
};
const ETAPAS = [['enviada', 'Enviada'], ['recibida', 'Recibida en sitio'], ['instalada', 'Instalada'], ['probada', 'Probada en sitio'], ['operacion', 'En operación']];
const NOM_ETAPA = Object.fromEntries(ETAPAS);
const CORTO = { enviada: 'Enviada', recibida: 'Recibida', instalada: 'Instalada', probada: 'Probada', operacion: 'Operación', falla: 'Con falla' };
const ORDEN = ['icave', 'timsa', 'eit', 'lzc']; // el orden que pidió Leo
const ALIAS = { lct: 'lzc', lazarocardenas: 'lzc', 'lazaro-cardenas': 'lzc', veracruz: 'icave', manzanillo: 'timsa', ensenada: 'eit' };
const COLOR = { rojo: 'Rojo', amarillo: 'Amarillo', verde: 'Verde' };
const ACTA = { envio: 'Envío', recepcion: 'Recepción', 'instalacion-campo': 'Instalación', 'prueba-sitio': 'Prueba', 'diagnostico-campo': 'Diagnóstico', dano: 'Daño' };
const ACTA_LARGO = { envio: 'Acta de envío', recepcion: 'Acta de recepción', 'instalacion-campo': 'Acta de instalación', 'prueba-sitio': 'Prueba en sitio', 'diagnostico-campo': 'Diagnóstico en campo', dano: 'Acta de daño' };
const FORM = { instalacion: '/formularios/instalacion.html', prueba: '/formularios/prueba-sitio.html', recepcion: '/formularios/recepcion.html', diagnostico: '/formularios/diagnostico.html' };
const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
// Íconos de línea (24×24, trazo redondo), como los del resto del sitio.
const ICONOS = {
documento: 'M14 3.5H7a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8.5zM14 3.5v5h5M9 13h6M9 16.5h4',
prueba: 'M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM8.5 12.2l2.4 2.4 4.6-4.8',
vehiculo: 'M3.5 6.5h10v9.5h-10zM13.5 9.5h3.7l3.3 3.6V16h-7M7.2 18.8a1.7 1.7 0 1 0 0-3.4 1.7 1.7 0 0 0 0 3.4zM16.8 18.8a1.7 1.7 0 1 0 0-3.4 1.7 1.7 0 0 0 0 3.4z',
fecha: 'M6 5h12a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2zM8 3v4M16 3v4M4 10h16',
persona: 'M12 5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7zM5 20c.8-3.8 3.6-6 7-6s6.2 2.2 7 6',
aviso: 'M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM12 8v5M12 16.2v.3',
ok: 'M5 12.5l4.5 4.5L19 7.5',
cruz: 'M7 7l10 10M17 7 7 17',
sitio: 'M12 20.5s-6.5-5.4-6.5-10.3a6.5 6.5 0 0 1 13 0c0 4.9-6.5 10.3-6.5 10.3zM12 7.7a2.4 2.4 0 1 0 0 4.8 2.4 2.4 0 0 0 0-4.8',
columnas: 'M5 4.5h14a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 18V6A1.5 1.5 0 0 1 5 4.5zM9.3 4.5v15M14.7 4.5v15',
flecha: 'M5 12h14M13 6l6 6-6 6',
unidad: 'M5 7.5 12 4l7 3.5v9L12 20l-7-3.5zM5 7.5l7 3.5 7-3.5M12 11v9'
};
const icono = n => '<svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="' + (ICONOS[n] || '') + '"/></svg>';
// Avance de cada columna: un círculo que se llena (como los estados de Pendientes).
const C = 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16z';
const aro = c => '<path d="' + C + '" style="fill:none;stroke:' + c + ';stroke-width:2"/>';
const ETAPA_SVG = {
enviada: '<path d="' + C + '" style="fill:none;stroke:var(--hg-texto-3);stroke-width:2;stroke-dasharray:2.6 2.6"/>',
recibida: aro('var(--hg-curso)') + '<path d="M12 12V4a8 8 0 0 1 8 8z" style="fill:var(--hg-curso)"/>',
instalada: aro('var(--hg-curso)') + '<path d="M12 4a8 8 0 0 1 0 16z" style="fill:var(--hg-curso)"/>',
probada: aro('var(--hg-curso)') + '<path d="M12 12V4a8 8 0 1 1-8 8z" style="fill:var(--hg-curso)"/>',
operacion: '<path d="' + C + '" style="fill:var(--hg-verde)"/><path d="M8.3 12.3l2.5 2.5 4.9-5" style="fill:none;stroke:var(--hg-plano);stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round"/>',
falla: aro('var(--hg-bloqueado)') + '<path d="M12 8v4.6M12 15.8v.2" style="fill:none;stroke:var(--hg-bloqueado);stroke-width:2.2;stroke-linecap:round"/>'
};
const svgEtapa = k => '<svg viewBox="0 0 24 24" aria-hidden="true">' + (ETAPA_SVG[k] || '') + '</svg>';
const plural = (n, uno, varios) => n + ' ' + (n === 1 ? uno : varios);
function fechaC(iso) {
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || ''));
if (!m) return '';
return (+m[3]) + ' ' + MES[+m[2] - 1] + (+m[1] !== new Date().getFullYear() ? ' ' + m[1] : '');
}
const qs = o => Object.keys(o).filter(k => o[k]).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(o[k])).join('&');
const forma = (f, o) => FORM[f] + (o && qs(o) ? '?' + qs(o) : '');
const expediente = (serie, archivo) => '/tableros/unidad/?serie=' + encodeURIComponent(serie)
+ (archivo ? '#a-' + String(archivo).replace(/[^A-Za-z0-9_-]/g, '') : '');
function meta(s) {
const t = String(s.meta || '');
let m = /(\d+)\s*[–—-]\s*(\d+)/.exec(t);
if (m) return [+m[1], Math.max(+m[1], +m[2])];
m = /(\d+)/.exec(t);
return m ? [+m[1], +m[1]] : [1, 2];
}
let DATOS = null, PROY = null, SITIOS = [], vista = 'resumen', cargadoEn = 0;
const cont = $('ins-cont'), tabs = $('ins-tabs');
const sitioDe = id => SITIOS.find(s => s.id === id) || { id, nombre: String(id || '').toUpperCase(), ciudad: '', estado: '', nota: '', meta: '' };
const R = id => (DATOS && DATOS.resumen && DATOS.resumen[id]) || { enviada: 0, recibida: 0, instalada: 0, probada: 0, operacion: 0, falla: 0, unidades: 0, instaladas: 0, probadas: 0, sin_vehiculo: 0, sin_prueba: 0 };
const unidades = id => ((DATOS && DATOS.unidades) || []).filter(u => !id || u.sitio === id);
const conVehiculo = u => !!(u.vehiculo && u.vehiculo.numero);
// ---------------------------------------------------------------- siguiente acta de cada unidad
function siguiente(u) {
const v = u.vehiculo || {};
const base = { serie: u.serie, sitio: u.sitio };
const prueba = Object.assign({}, base, { maquina_id: v.numero, tipo_vehiculo: v.tipo, id_monitor: u.id_monitor });
if (u.falla) {
if (u.falla.tipo === 'dano') return { txt: 'Ver el daño', href: expediente(u.serie, u.falla.archivo), sec: true };
if (u.retirada) return { txt: 'Ver expediente', href: expediente(u.serie), sec: true };
if (u.falla.tipo === 'prueba-sitio') return { txt: 'Repetir la prueba', href: forma('prueba', prueba), ic: 'prueba' };
if (u.falla.tipo === 'recepcion') return { txt: 'Ver la recepción', href: expediente(u.serie, u.falla.archivo), sec: true };
if (u.falla.tipo === 'instalacion-campo' && !u.instalada)
return { txt: 'Acta de instalación', href: forma('instalacion', Object.assign({}, base, { maquina_id: v.numero || u.vehiculo_destino })), ic: 'documento' };
return { txt: 'Diagnóstico en campo', href: forma('diagnostico', Object.assign({}, base, { maquina_id: v.numero })), ic: 'documento' };
}
if (u.etapa === 'enviada') return { txt: 'Registrar recepción', href: forma('recepcion', Object.assign({}, base, { maquina_destino: u.vehiculo_destino })), ic: 'documento' };
if (u.etapa === 'recibida') return { txt: 'Acta de instalación', href: forma('instalacion', Object.assign({}, base, { maquina_id: u.vehiculo_destino })), ic: 'documento' };
if (u.etapa === 'instalada') return { txt: 'Prueba en sitio', href: forma('prueba', prueba), ic: 'prueba' };
return null;
}
// ---------------------------------------------------------------- tarjeta de una unidad
function tarjeta(u, conSitio) {
const c = el('article', { class: 'ins-card' + (u.falla ? ' con-falla' : '') });
const cab = el('div', { class: 'cab' });
cab.append(el('a', { class: 'id', href: expediente(u.serie), title: 'Expediente de ' + u.serie, text: u.id_monitor || u.serie }));
if (conSitio) cab.append(el('span', { class: 'sitio-b', text: sitioDe(u.sitio).nombre }));
c.append(cab);
if (u.id_monitor) c.append(el('div', { class: 'serie', text: u.serie }));
// en qué vehículo está
const v = u.vehiculo || {};
const enSitio = u.instalada || ['instalada', 'probada', 'operacion'].includes(u.etapa);
if (conVehiculo(u)) {
const t = el('span', { class: 'veh' }, el('b', { text: v.numero }), v.tipo ? ' · ' + v.tipo : '');
const f = el('div', { class: 'fila', html: icono('vehiculo') }, t);
if (v.voltaje && /^\d/.test(v.voltaje)) f.append(el('span', { class: 'v-chip', title: 'Sistema eléctrico del vehículo', text: v.voltaje }));
c.append(f);
} else if (enSitio && !u.retirada) {
c.append(el('span', { class: 'sin-veh', html: icono('aviso') }, 'Sin vehículo registrado' + (v.tipo ? ' (' + v.tipo + ')' : '')));
} else if (u.vehiculo_destino) {
c.append(el('div', { class: 'fila', html: icono('vehiculo') }, 'Para ', el('b', { text: u.vehiculo_destino })));
}
// cuándo y en qué quedó
const fe = u.fechas || {};
if (u.falla) {
c.append(el('div', { class: 'fila falla', html: icono('aviso') }, [u.falla.motivo, fechaC(u.falla.fecha)].filter(Boolean).join(' · ')));
if (u.etapa) c.append(el('div', { class: 'fila', html: icono('flecha') }, 'Llegó a: ' + NOM_ETAPA[u.etapa]));
} else {
let txt = '';
if (u.etapa === 'enviada') txt = 'Enviada ' + fechaC(fe.enviada);
else if (u.etapa === 'recibida') txt = 'Recibida ' + fechaC(fe.recibida);
else if (u.etapa === 'instalada') txt = fe.instalada ? 'Instalada ' + fechaC(fe.instalada) : 'Instalada';
else if (u.etapa === 'probada') txt = 'Probada ' + fechaC(fe.probada) + (u.operacion_desde ? ' · pasa a operación el ' + fechaC(u.operacion_desde) + ' si no falla' : '');
else if (u.etapa === 'operacion') txt = 'En operación desde el ' + fechaC(u.operacion_desde);
if (txt) c.append(el('div', { class: 'fila', html: icono('fecha') }, txt));
}
if (u.tecnico) c.append(el('div', { class: 'fila', html: icono('persona') }, u.tecnico));
if (u.sin_acta_instalacion && !u.retirada) c.append(el('div', { class: 'fila aviso', html: icono('aviso') }, 'Falta el acta de instalación'));
// sus actas: la última de cada tipo, liga a esa acta en el expediente
const ult = {};
(u.actas || []).forEach(a => { if (!ult[a.tipo]) ult[a.tipo] = a; });
const chips = el('div', { class: 'actas' });
Object.keys(ACTA).forEach(t => {
const a = ult[t];
if (!a) return;
const mal = a.resultado === 'FALLA' || /^(No se pudo|No aprobada|No entra|Rechazada)/.test(a.accion || '');
chips.append(el('a', { class: mal ? 'falla' : null, href: expediente(u.serie, a.archivo),
title: ACTA_LARGO[t] + (a.fecha ? ' · ' + fechaC(a.fecha) : '') + (a.tecnico ? ' · ' + a.tecnico : '') + (a.accion ? ' · ' + a.accion : ''),
html: icono(mal ? 'cruz' : 'ok') }, ACTA[t]));
});
if (chips.children.length) c.append(chips);
// la siguiente acta, ya con la unidad, el sitio y el vehículo
const acc = el('div', { class: 'acc' });
const s = siguiente(u);
if (s) acc.append(el('a', { class: 'boton' + (s.sec ? ' sec' : ''), href: s.href, html: s.ic ? icono(s.ic) : '' }, s.txt));
const base = { serie: u.serie, sitio: u.sitio };
const probada = u.etapa === 'probada' || u.etapa === 'operacion';
const sinVeh = probada && !conVehiculo(u);
if (!u.falla && !u.retirada && (sinVeh || u.sin_acta_instalacion))
acc.append(el('a', { class: 'boton sec', href: forma('instalacion', Object.assign({}, base, { maquina_id: v.numero, tipo_vehiculo: v.tipo, id_monitor: u.id_monitor })),
html: icono(sinVeh ? 'vehiculo' : 'documento') }, sinVeh ? 'Registrar el vehículo' : 'Acta de instalación'));
if (!u.falla && probada) acc.append(el('a', { class: 'liga', href: forma('diagnostico', Object.assign({}, base, { maquina_id: v.numero })) }, 'Reportar una falla'));
if (acc.children.length) c.append(acc);
return c;
}
// ---------------------------------------------------------------- un sitio: cabecera o tarjeta del resumen
function fichaSitio(s, enResumen) {
const r = R(s.id);
const card = el('section', { class: 'ins-sitio' + (enResumen ? '' : ' cab') + (COLOR[s.estado] ? ' est-' + s.estado : ''), 'aria-label': s.nombre });
const t = el('div', { class: 'ins-sitio-t' }, el('h2', { text: s.nombre }), el('span', { class: 'ciudad', text: s.ciudad || '' }));
if (COLOR[s.estado]) t.append(el('span', { class: 'ins-est', title: 'Semáforo del sitio (página de inicio)', html: '<span class="luz" aria-hidden="true"></span>' }, COLOR[s.estado]));
card.append(t);
if (s.nota) card.append(el('p', { class: 'ins-nota', text: s.nota }));
// la meta cuenta unidades PROBADAS (prueba en sitio aprobada)
const [mmin, mmax] = meta(s);
const m = el('div', { class: 'ins-meta' });
const n = Math.max(mmax, r.instaladas, r.probadas);
const cas = el('span', { class: 'ins-casillas', role: 'img',
'aria-label': plural(r.probadas, 'probada', 'probadas') + ' y ' + plural(r.instaladas, 'instalada', 'instaladas') + ' contra una meta de ' + mmin + ' a ' + mmax });
for (let i = 1; i <= n; i++) cas.append(el('span', { class: i <= r.probadas ? 'prob' : i <= r.instaladas ? 'inst' : null,
title: i <= r.probadas ? 'Probada en sitio' : i <= r.instaladas ? 'Instalada, sin prueba aprobada' : 'Falta' }));
m.append(el('span', null, 'Meta ', el('b', { text: mmin === mmax ? String(mmin) : mmin + '–' + mmax })), cas,
el('span', null, el('b', { text: String(r.probadas) }), ' ' + (r.probadas === 1 ? 'probada' : 'probadas') + ' · ' + plural(r.instaladas, 'instalada', 'instaladas')));
m.append(el('span', { class: 'ins-logro' + (r.probadas >= mmin ? ' si' : ''), text: r.probadas >= mmin ? 'Meta cumplida' : 'Falta la meta' }));
card.append(m);
const emb = el('div', { class: 'ins-embudo' });
ETAPAS.map(e => e[0]).concat('falla').forEach(k => emb.append(el('div', { class: (k === 'falla' ? 'falla' : '') + (r[k] ? '' : ' cero'), title: k === 'falla' ? 'Con falla' : NOM_ETAPA[k] },
el('b', { text: String(r[k] || 0) }), el('span', { text: CORTO[k] }))));
card.append(emb);
if (!r.unidades) {
card.append(el('div', { class: 'ins-vacio' },
el('span', null, 'Sin unidades registradas. ¿Hay unidades instaladas sin acta? Llénala con la ', el('b', { text: 'fecha real' }), ' de la instalación.'),
el('a', { class: 'boton', href: forma('instalacion', { sitio: s.id }), html: icono('documento') }, 'Acta de instalación')));
}
const acc = el('div', { class: 'ins-acc' });
if (enResumen) {
const ver = el('button', { type: 'button', class: 'sec', html: icono('columnas') }, 'Ver ' + s.nombre);
ver.addEventListener('click', () => ir(s.id));
acc.append(ver);
}
acc.append(el('a', { class: 'liga', href: '/tableros/pendientes/?sitio=' + encodeURIComponent(s.id) }, 'Pendientes de ' + s.nombre),
el('a', { class: 'liga', href: '/tableros/sitio/?id=' + encodeURIComponent(s.id), html: icono('sitio') }, 'Página del sitio'));
card.append(acc);
return card;
}
// ---------------------------------------------------------------- vistas
function pintarSitio(id) {
const s = sitioDe(id), us = unidades(id);
cont.innerHTML = '';
cont.append(fichaSitio(s, false));
const fallas = us.filter(u => u.columna === 'falla');
if (fallas.length) {
const car = el('section', { class: 'ins-carril', 'aria-label': 'Con falla' });
car.append(el('h3', { html: svgEtapa('falla') }, 'Con falla ', el('span', { class: 'n', text: String(fallas.length) })));
const g = el('div', { class: 'ins-carril-g' });
fallas.forEach(u => g.append(tarjeta(u)));
car.append(g);
cont.append(car);
}
const kb = el('div', { class: 'ins-kb' });
ETAPAS.forEach(([k, nombre]) => {
const lista = us.filter(u => u.columna === k);
const col = el('section', { class: 'ins-col', 'aria-label': nombre });
col.append(el('h3', { html: svgEtapa(k) }, nombre + ' ', el('span', { class: 'n', text: String(lista.length) })));
if (!lista.length) col.append(el('div', { class: 'hueco', text: '—' }));
lista.forEach(u => col.append(tarjeta(u)));
kb.append(col);
});
cont.append(kb);
}
function pintarResumen() {
cont.innerHTML = '';
const todas = unidades();
const sum = k => SITIOS.reduce((a, s) => a + (R(s.id)[k] || 0), 0);
const conMeta = SITIOS.filter(s => R(s.id).probadas >= meta(s)[0]).length;
const kp = el('div', { class: 'ins-kpis' });
[[sum('instaladas'), 'Instaladas', ''], [sum('probadas'), 'Probadas en sitio (cuentan para la meta)', sum('probadas') ? 'bien' : ''],
[sum('sin_prueba'), 'Instaladas sin prueba', sum('sin_prueba') ? 'aviso' : ''], [sum('falla'), 'Con falla', sum('falla') ? 'mal' : ''],
[conMeta + ' de ' + SITIOS.length, 'Sitios con la meta cumplida', conMeta === SITIOS.length && SITIOS.length ? 'bien' : '']]
.forEach(([n, l, cls]) => kp.append(el('div', { class: 'ins-kpi ' + cls }, el('div', { class: 'n', text: String(n) }), el('div', { class: 'l', text: l }))));
cont.append(kp);
const g = el('div', { class: 'ins-sitios' });
SITIOS.forEach(s => g.append(fichaSitio(s, true)));
cont.append(g);
// qué falta, en todos los sitios
const falta = el('div', { class: 'ins-falta' });
const caja = (titulo, lista, fila, nada) => {
const sec = el('section', { 'aria-label': titulo });
sec.append(el('h3', null, titulo + ' ', el('span', { class: 'n', text: String(lista.length) })));
if (!lista.length) sec.append(el('p', { class: 'nada', text: nada }));
else { const ul = el('ul'); lista.forEach(u => ul.append(fila(u))); sec.append(ul); }
return sec;
};
const quien = u => el('span', { class: 'q' }, el('b', { text: u.id_monitor || u.serie }), ' · ' + sitioDe(u.sitio).nombre
+ (conVehiculo(u) ? ' · ' + u.vehiculo.numero + (u.vehiculo.tipo ? ' (' + u.vehiculo.tipo + ')' : '') : ''));
const boton = (s) => s ? el('a', { class: 'boton' + (s.sec ? ' sec' : ''), href: s.href }, s.txt) : null;
falta.append(caja('Instaladas sin prueba', todas.filter(u => u.columna === 'instalada'),
u => el('li', null, quien(u), boton(siguiente(u))), 'Ninguna: toda unidad instalada ya tiene su prueba en sitio.'));
falta.append(caja('Sin vehículo registrado', todas.filter(u => u.instalada && !conVehiculo(u)),
u => el('li', null, quien(u), el('a', { class: 'boton sec', href: forma('instalacion', { serie: u.serie, sitio: u.sitio, id_monitor: u.id_monitor }) }, 'Registrar el vehículo')),
'Ninguna: cada unidad instalada dice en qué vehículo está.'));
falta.append(caja('Con falla', todas.filter(u => u.columna === 'falla'),
u => el('li', null, el('span', { class: 'q' }, el('b', { text: u.id_monitor || u.serie }), ' · ' + sitioDe(u.sitio).nombre + ' · ' + ((u.falla || {}).motivo || '')), boton(siguiente(u))),
'Ninguna unidad con falla abierta.'));
cont.append(el('h2', { text: 'Qué falta', style: 'margin:6px 0 10px' }), falta);
}
function pintarTabs() {
tabs.innerHTML = '';
const b = (id, nombre, n, estado) => {
const x = el('button', { type: 'button', 'aria-pressed': String(vista === id), class: COLOR[estado] ? 'est-' + estado : null,
title: id === 'resumen' ? 'Todos los sitios' : nombre + (COLOR[estado] ? ' — semáforo ' + COLOR[estado].toLowerCase() : '') });
if (id !== 'resumen') x.append(el('span', { class: 'luz', 'aria-hidden': 'true' }));
x.append(nombre);
if (n != null) x.append(el('span', { class: 'n', text: String(n) }));
x.addEventListener('click', () => ir(id));
return x;
};
tabs.append(b('resumen', 'Resumen'));
SITIOS.forEach(s => tabs.append(b(s.id, s.nombre, R(s.id).unidades, s.estado)));
}
function pintar() {
if (vista !== 'resumen' && !SITIOS.some(s => s.id === vista)) vista = 'resumen';
pintarTabs();
const s = vista === 'resumen' ? null : sitioDe(vista);
const hora = new Date(cargadoEn).toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit', hour12: false });
const us = unidades(s && s.id);
$('ins-meta').textContent = (s ? s.nombre + ' · ' : '') + plural(us.length, 'unidad', 'unidades') + (s ? '' : ' en ' + plural(SITIOS.length, 'sitio', 'sitios')) + ' · al día a las ' + hora;
$('ins-a-inst').href = forma('instalacion', s ? { sitio: s.id } : null);
$('ins-a-prueba').href = forma('prueba', s ? { sitio: s.id } : null);
const mat = $('ins-a-mat'); // inventario del sitio (23-oct)
if (mat) mat.href = '/tableros/inventario/' + (s ? '?sitio=' + encodeURIComponent(s.id) : '');
document.title = 'Instalaciones' + (s ? ' · ' + s.nombre : '') + ' - Drogheda';
if (s) pintarSitio(s.id); else pintarResumen();
}
function ir(id) {
vista = id;
const u = new URL(location.href);
if (id === 'resumen') u.searchParams.delete('sitio'); else u.searchParams.set('sitio', id);
history.replaceState(null, '', u.pathname + u.search + u.hash);
pintar();
if (window.scrollY > tabs.offsetTop) tabs.scrollIntoView({ block: 'start' });
}
function armarSitios() {
const api = (DATOS && DATOS.sitios) || [], proy = (PROY && PROY.sitios) || [];
const hay = id => api.some(s => s.id === id) || proy.some(s => s.id === id);
const ids = [];
ORDEN.filter(hay).concat(api.map(s => s.id), proy.map(s => s.id)).forEach(id => { if (id && id !== 'qro' && !ids.includes(id)) ids.push(id); });
SITIOS = ids.map(id => {
const a = api.find(s => s.id === id) || {}, p = proy.find(s => s.id === id) || {};
return { id, nombre: p.nombre || a.nombre || id.toUpperCase(), ciudad: p.ciudad || a.ubicacion || '', estado: p.estado || '', nota: p.nota || '', meta: p.meta || '' };
});
}
async function cargar() {
const btn = $('ins-actualizar');
if (btn) btn.disabled = true;
try {
const [a, p] = await Promise.all([
fetch('/api/instalaciones', { cache: 'no-store' }).then(async r => {
let j = null;
try { j = await r.json(); } catch (e) {}
if (!r.ok || !j || !j.ok) throw new Error((j && j.error) || ('HTTP ' + r.status));
return j;
}),
fetch('/datos/proyecto.json', { cache: 'no-store' }).then(r => (r.ok ? r.json() : null)).catch(() => null)
]);
DATOS = a; PROY = p; cargadoEn = Date.now();
armarSitios();
pintar();
} catch (e) {
$('ins-meta').textContent = 'Sin datos';
cont.innerHTML = '';
cont.append(el('div', { class: 'err' }, 'No se pudo leer el tablero de instalaciones (' + e.message + '). '
+ (/404/.test(e.message) ? 'Si se acaba de publicar, falta reiniciar el servicio actas-hbox en el servidor. ' : '')
+ 'Las actas siguen en su lugar; avisa a Leo si no se arregla.'));
}
if (btn) btn.disabled = false;
}
const q = (new URLSearchParams(location.search).get('sitio') || '').trim().toLowerCase();
vista = q ? (ALIAS[q] || q) : 'resumen';
const btn = $('ins-actualizar');
if (btn) btn.addEventListener('click', cargar);
document.addEventListener('visibilitychange', () => { if (!document.hidden && cargadoEn && Date.now() - cargadoEn > 60000) cargar(); });
cargar();
})();