/* 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 => '';
// 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 => '';
const ETAPA_SVG = {
enviada: '',
recibida: aro('var(--hg-curso)') + '',
instalada: aro('var(--hg-curso)') + '',
probada: aro('var(--hg-curso)') + '',
operacion: '',
falla: aro('var(--hg-bloqueado)') + ''
};
const svgEtapa = k => '';
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: '' }, 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();
})();