Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
363 lines
24 KiB
JavaScript
363 lines
24 KiB
JavaScript
/* 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);
|
||
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();
|
||
})();
|