/* 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); 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(); })();