Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
342 lines
22 KiB
JavaScript
342 lines
22 KiB
JavaScript
/* Página de inicio de Drogheda (21-oct-2026).
|
|
Llena docs/index.md con:
|
|
- docs/datos/proyecto.json: fase, semáforo, ruta a producción, próximas fechas, sitios con su color (lo actualiza Claude o Leo);
|
|
- /api/hoy: el enfoque de la semana (lo escriben Leo y Adriana con «Editar»);
|
|
- /api/tareas (tablero), /api/actividad, /api/plan + /api/actas (unidades ensambladas),
|
|
/api/equipo y /api/sitio/unidades;
|
|
- /datos/documentos.json: los documentos con fecha (lo genera hooks/documentos.py al construir).
|
|
Si algo del servidor no responde, esa parte lo dice y el resto de la página sigue. */
|
|
(function () {
|
|
'use strict';
|
|
const MES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
|
|
const MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
|
|
const DIAS = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
|
|
const AREAS = ['direccion', 'operaciones', 'layout', 'pruebas', 'fabricacion', 'campo', 'ingenieria', 'software', 'produccion', 'asistente'];
|
|
const BADGE = { 'V5.5': 'v55', 'V5.6': 'v56', 'V5.7': 'v57', 'v6': 'v6' };
|
|
const PLAN_RESPALDO = { pedido_total: 570, versiones: [
|
|
{ version: 'V5.6', objetivo: 113, serie_prefijo: 'V56' }, { version: 'V5.7', objetivo: 327, serie_prefijo: 'V57' }, { version: 'v6', objetivo: 130, serie_prefijo: 'V6' }] };
|
|
const LAPIZ = 'M4 20h4L19 9a2.8 2.8 0 0 0-4-4L4 16zM13.5 6.5l4 4';
|
|
const DOC = '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';
|
|
|
|
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 e.setAttribute(p, a[p]); }
|
|
k.forEach(c => c != null && e.append(typeof c === 'string' ? document.createTextNode(c) : c));
|
|
return e;
|
|
};
|
|
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
const svg = (d, cls) => '<svg class="' + (cls || 'i') + '" viewBox="0 0 24 24" aria-hidden="true"><path d="' + d + '"/></svg>';
|
|
function hoyISO(delta) { const d = new Date(Date.now() + (delta || 0) * 86400000); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); }
|
|
const fechaCorta = f => { const m = String(f || '').match(/^(\d{4})-(\d{2})-(\d{2})/); return m ? (+m[3]) + ' ' + MES[+m[2] - 1] : ''; };
|
|
const plural = (n, uno, varios) => n + ' ' + (n === 1 ? uno : varios);
|
|
const esPrueba = s => String(s || '').toUpperCase().includes('PRUEBA');
|
|
|
|
async function json(url, opts) {
|
|
const r = await fetch(url, Object.assign({ cache: 'no-store', credentials: 'same-origin' }, opts || {}));
|
|
let j = null; try { j = await r.json(); } catch (e) { /* sin JSON */ }
|
|
if (!r.ok || !j || j.ok === false) throw new Error((j && j.error) || ('HTTP ' + r.status));
|
|
return j;
|
|
}
|
|
function falla(cont, que, err) {
|
|
cont.innerHTML = '';
|
|
cont.append(el('p', { class: 'vacio', text: 'No se pudo cargar ' + que + (err && err.message ? ' (' + err.message + ')' : '') + '.' }));
|
|
}
|
|
|
|
// texto del enfoque: **negritas**, `código` y [ligas](/ruta/ o https://…); lo demás se escapa
|
|
function enLinea(s) {
|
|
return esc(s)
|
|
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
|
|
.replace(/\[([^\]]+)\]\(((?:https?:\/\/|\/)[^)\s]*)\)/g, '<a href="$2">$1</a>');
|
|
}
|
|
|
|
const equipo = () => (window.Kanban && window.Kanban.equipo) || {};
|
|
const nombreCorto = id => (window.Kanban && window.Kanban.nombreCorto) ? window.Kanban.nombreCorto(id) : ((equipo()[id] || {}).nombre || id || '').split(' ')[0];
|
|
const lista = xs => xs.length <= 1 ? (xs[0] || '') : xs.slice(0, -1).join(', ') + ' y ' + xs[xs.length - 1];
|
|
const avatares = ids => { const g = el('span', { class: 'avs' }); (ids || []).forEach(id => g.append(window.Kanban.avatar(id))); return g; };
|
|
|
|
// ---------------------------------------------------------------- semáforo del proyecto (22-oct)
|
|
const COLOR = { rojo: 'Rojo', amarillo: 'Amarillo', verde: 'Verde' };
|
|
function pintarSemaforo(P) {
|
|
const S = P.semaforo, sec = $('in-semaforo');
|
|
if (!sec || !S || !COLOR[S.color]) return;
|
|
sec.className = 'in-semaforo sem-' + S.color;
|
|
sec.innerHTML = '';
|
|
const luz = el('div', { class: 'in-sem-luz', role: 'img', 'aria-label': 'Semáforo en ' + COLOR[S.color].toLowerCase() });
|
|
['rojo', 'amarillo', 'verde'].forEach(c => luz.append(el('span', { class: 'l-' + c + (c === S.color ? ' on' : '') })));
|
|
const c = el('div', { class: 'in-sem-c' });
|
|
const cab = el('div', { class: 'in-sem-cab' });
|
|
cab.append(el('span', { text: 'Semáforo del proyecto' }));
|
|
if (S.actualizado) cab.append(el('span', { class: 'in-gris', text: (S.quien ? S.quien + ', ' : '') + fechaLarga(S.actualizado) }));
|
|
c.append(cab, el('h2', { id: 'sem-h', text: S.titulo || COLOR[S.color] }));
|
|
if (S.texto) c.append(el('p', { class: 'in-sem-texto', html: enLinea(S.texto) }));
|
|
if ((S.causas || []).length) { const ul = el('ul', { class: 'in-sem-causas' }); S.causas.forEach(x => ul.append(el('li', { html: enLinea(x) }))); c.append(ul); }
|
|
if (S.destraba) c.append(el('p', { class: 'in-sem-destraba', html: '<b>Qué lo destraba:</b> ' + enLinea(S.destraba) }));
|
|
sec.append(luz, c);
|
|
sec.hidden = false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- arriba: fase, pedido, hito, equipo
|
|
function pintarFase(P) {
|
|
const d = $('in-fase'); d.innerHTML = '';
|
|
d.append(el('span', { class: 'in-chip', text: P.fase.nombre }), ' ', el('span', { class: 'in-gris', text: P.fase.texto }));
|
|
}
|
|
|
|
function pintarPedido(P, plan, actas) {
|
|
const d = $('in-pedido'); d.innerHTML = '';
|
|
const vs = (plan && plan.versiones && plan.versiones.length) ? plan.versiones : PLAN_RESPALDO.versiones;
|
|
const total = (plan && plan.pedido_total) || vs.reduce((n, v) => n + (v.objetivo || 0), 0);
|
|
const ens = (actas || []).filter(a => a.tipo === 'ensamble' && a.resultado !== 'FALLA' && !esPrueba(a.serie));
|
|
const hechas = v => { const pref = String(v.serie_prefijo || '').toUpperCase(); return pref ? ens.filter(a => String((a.componentes || {}).einar || '').toUpperCase().startsWith(pref + '-')).length : 0; };
|
|
const cuenta = vs.reduce((n, v) => n + hechas(v), 0);
|
|
const linea = el('div', { class: 'in-pedido-l' });
|
|
linea.append(el('b', { text: total + ' HBOX' }), ' ',
|
|
el('span', { class: 'in-gris', text: (actas ? plural(cuenta, 'ensamblada', 'ensambladas') + '; ' : '') + (P.pedido_nota || '') }));
|
|
const barra = el('div', { class: 'in-barra', role: 'img', 'aria-label': vs.map(v => v.objetivo + ' ' + v.version).join(', ') });
|
|
const leyenda = el('div', { class: 'in-barra-l' });
|
|
vs.forEach(v => {
|
|
const cls = BADGE[v.version] || 'v4';
|
|
const seg = el('span', { class: 'in-seg s-' + cls, style: 'flex:' + (v.objetivo || 1) });
|
|
const pct = v.objetivo ? Math.min(100, Math.round(100 * hechas(v) / v.objetivo)) : 0;
|
|
seg.append(el('span', { style: 'width:' + pct + '%' }));
|
|
barra.append(seg);
|
|
leyenda.append(el('span', { class: 's-' + cls, style: 'flex:' + (v.objetivo || 1), text: v.objetivo + ' ' + v.version }));
|
|
});
|
|
d.append(linea, barra, leyenda);
|
|
d.append(el('a', { class: 'in-mini', href: '/tableros/produccion/', text: 'Avance contra el plan' }));
|
|
}
|
|
|
|
function hitosVigentes(P) {
|
|
const hoy = hoyISO();
|
|
return (P.hitos || []).filter(h => !h.fecha || (h.fin || h.fecha) >= hoy)
|
|
.sort((a, b) => ((a.fecha ? 1 : 0) - (b.fecha ? 1 : 0)) || String(a.fecha).localeCompare(String(b.fecha)));
|
|
}
|
|
function pintarHito(P) {
|
|
const d = $('in-hito'); d.innerHTML = '';
|
|
const h = hitosVigentes(P)[0];
|
|
if (!h) { d.append(el('span', { class: 'vacio', text: 'Sin hitos por delante.' })); return; }
|
|
d.append(el('span', { text: h.texto }), ' ', el('span', { class: 'in-gris', text: h.cuando }));
|
|
}
|
|
|
|
function pintarEquipo(P) {
|
|
const d = $('in-equipo'); d.innerHTML = '';
|
|
const activos = Object.entries(equipo()).filter(([, m]) => m.activo !== false && m.nombre)
|
|
.sort((a, b) => ((AREAS.indexOf(a[1].area) + 1 || 99) - (AREAS.indexOf(b[1].area) + 1 || 99)) || a[1].nombre.localeCompare(b[1].nombre));
|
|
const caras = el('a', { class: 'avs in-caras', href: '/tableros/equipo/', title: 'Equipo: ' + activos.map(([, m]) => m.nombre).join(', ') });
|
|
activos.slice(0, 6).forEach(([id]) => caras.append(window.Kanban.avatar(id)));
|
|
if (activos.length > 6) caras.append(el('span', { class: 'av libre', text: '+' + (activos.length - 6) }));
|
|
d.append(caras);
|
|
if (P.gestion && P.gestion.texto) d.append(' ', el('span', { class: 'in-gris', text: P.gestion.texto }));
|
|
}
|
|
|
|
// ---------------------------------------------------------------- enfoque de la semana
|
|
const ORDEN_EDITORES = ['leo', 'adriana'];
|
|
function fechaLarga(iso) {
|
|
const f = String(iso || '').slice(0, 10); if (!f) return '';
|
|
const d = new Date(f + 'T12:00:00');
|
|
return DIAS[d.getDay()] + ' ' + d.getDate() + ' de ' + MESES[d.getMonth()];
|
|
}
|
|
function cuandoEditado(iso) {
|
|
const f = String(iso || '').slice(0, 10), h = String(iso || '').slice(11, 16);
|
|
if (f === hoyISO()) return 'hoy a las ' + h;
|
|
if (f === hoyISO(-1)) return 'ayer a las ' + h;
|
|
return 'el ' + fechaLarga(iso) + (h ? ' a las ' + h : '');
|
|
}
|
|
|
|
function pintarEnfoque(P, r) {
|
|
const c = document.querySelector('#in-enfoque .in-enfoque-c'); c.innerHTML = '';
|
|
const h = r && r.enfoque, e = h || P.enfoque_inicial || { titulo: 'Sin enfoque escrito todavía.', puntos: [] };
|
|
const editores = ((r && r.editores) || ORDEN_EDITORES).slice().sort((a, b) => ((ORDEN_EDITORES.indexOf(a) + 1 || 9) - (ORDEN_EDITORES.indexOf(b) + 1 || 9)));
|
|
const cab = el('div', { class: 'in-enfoque-cab' });
|
|
cab.append(el('span', { class: 'in-enfoque-cuando', text: 'Enfoque de la semana' + (h ? '. Escrito el ' + fechaLarga(h.editado_en) : '') }));
|
|
if (r && r.puede_editar) {
|
|
const b = el('button', { type: 'button', class: 'sec in-editar', html: svg(LAPIZ) + 'Editar' });
|
|
b.addEventListener('click', () => editarEnfoque(P, r, e));
|
|
cab.append(b);
|
|
}
|
|
c.append(cab, el('h2', { id: 'enfoque-h', text: e.titulo }));
|
|
if ((e.puntos || []).length) { const ul = el('ul'); e.puntos.forEach(p => ul.append(el('li', { html: enLinea(p) }))); c.append(ul); }
|
|
const pie = el('p', { class: 'in-enfoque-pie' });
|
|
if (h) pie.textContent = 'Lo escriben ' + lista(editores.map(nombreCorto)) + '. Editado ' + cuandoEditado(h.editado_en) + ' por ' + nombreCorto(h.editado_por) + '.';
|
|
else if (P.enfoque_inicial && P.enfoque_inicial.nota) pie.innerHTML = enLinea(P.enfoque_inicial.nota);
|
|
if (!r) pie.append(el('span', { class: 'in-gris', text: ' (El servidor no respondió: este es el texto inicial.)' }));
|
|
c.append(pie);
|
|
}
|
|
|
|
function editarEnfoque(P, r, e) {
|
|
const c = document.querySelector('#in-enfoque .in-enfoque-c'); c.innerHTML = '';
|
|
const f = el('form', { class: 'in-enfoque-ed' });
|
|
const tit = el('input', { type: 'text', maxlength: '160', id: 'enf-tit', required: '' }); tit.value = e.titulo || '';
|
|
const pts = el('textarea', { rows: '6', id: 'enf-pts' }); pts.value = (e.puntos || []).join('\n');
|
|
const msg = el('div', { class: 'msg', role: 'status' });
|
|
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' });
|
|
const guardar = el('button', { type: 'submit', text: 'Guardar' });
|
|
f.append(el('div', { class: 'in-enfoque-cab' }, el('span', { class: 'in-enfoque-cuando', text: 'Editar el enfoque de la semana' })),
|
|
el('label', { for: 'enf-tit', text: 'Frase de la semana (una línea)' }), tit,
|
|
el('label', { for: 'enf-pts', text: 'Puntos: uno por línea, hasta 8' }), pts,
|
|
el('p', { class: 'ayuda', html: 'Se puede usar <code>**negritas**</code>, <code>`código`</code> y <code>[ligas](/hardware/…/)</code>. Se guarda con tu nombre y la hora; lo anterior queda en el historial del servidor.' }),
|
|
msg, el('div', { class: 'in-enfoque-botones' }, cancelar, guardar));
|
|
c.append(f);
|
|
tit.focus();
|
|
cancelar.addEventListener('click', () => pintarEnfoque(P, r));
|
|
f.addEventListener('submit', async ev => {
|
|
ev.preventDefault();
|
|
const d = { titulo: tit.value.trim(), puntos: pts.value.split('\n').map(x => x.trim()).filter(Boolean) };
|
|
if (!d.titulo) { msg.className = 'msg err'; msg.textContent = 'Escribe la frase de la semana.'; return; }
|
|
if (d.puntos.length > 8) { msg.className = 'msg err'; msg.textContent = 'Son ' + d.puntos.length + ' puntos; deja 8 o menos.'; return; }
|
|
guardar.disabled = true;
|
|
try {
|
|
const j = await json('/api/hoy', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(d) });
|
|
r = Object.assign({}, r, { enfoque: j.enfoque });
|
|
pintarEnfoque(P, r);
|
|
} catch (err) {
|
|
msg.className = 'msg err'; msg.textContent = 'No se guardó: ' + err.message;
|
|
guardar.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------- próximas fechas
|
|
function pintarFechas(P, tareas) {
|
|
const cont = $('in-hitos'); cont.innerHTML = '';
|
|
const hoy = hoyISO(), tope = hoyISO(10);
|
|
const filas = hitosVigentes(P).map(h => ({ fecha: h.fecha, cuando: h.cuando, texto: h.texto, quien: h.quien || [], hito: true }));
|
|
(tareas || []).forEach(t => {
|
|
if (t.estado === 'hecho' || !t.fecha_limite || t.fecha_limite < hoy || t.fecha_limite > tope) return;
|
|
filas.push({ fecha: t.fecha_limite, cuando: t.fecha_limite === hoy ? 'Hoy' : fechaCorta(t.fecha_limite), texto: t.titulo, quien: window.Kanban.idsResp(t), id: t.id });
|
|
});
|
|
filas.sort((a, b) => ((a.fecha ? 1 : 0) - (b.fecha ? 1 : 0)) || String(a.fecha).localeCompare(String(b.fecha)));
|
|
if (!filas.length) { cont.append(el('p', { class: 'vacio', text: 'Nada con fecha en los próximos 10 días.' })); return; }
|
|
const ol = el('ol', { class: 'in-fechas' });
|
|
filas.slice(0, 7).forEach(x => {
|
|
const li = el('li', { class: x.hito ? 'es-hito' : 'es-pendiente' });
|
|
li.append(el('span', { class: 'in-f' + (x.cuando === 'Hoy' ? ' hoy' : '') + (x.fecha ? '' : ' sin'), text: x.cuando }));
|
|
li.append(x.id ? el('a', { class: 'in-t', href: '/tableros/pendientes/?id=' + encodeURIComponent(x.id), text: x.texto }) : el('span', { class: 'in-t', text: x.texto }));
|
|
li.append(avatares(x.quien));
|
|
ol.append(li);
|
|
});
|
|
cont.append(ol);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- documentos recientes
|
|
async function pintarDocs() {
|
|
const cont = $('in-docs');
|
|
try {
|
|
const j = await json('/datos/documentos.json');
|
|
cont.innerHTML = '';
|
|
const docs = (j.documentos || []).slice(0, 6);
|
|
if (!docs.length) { cont.append(el('p', { class: 'vacio', text: 'Sin documentos con fecha.' })); return; }
|
|
const ol = el('ol', { class: 'in-docs' });
|
|
docs.forEach(d => ol.append(el('li', null, el('a', { href: d.url, html: svg(DOC) + '<span>' + esc(d.titulo) + '</span>' }), el('span', { class: 'in-f', text: fechaCorta(d.fecha) }))));
|
|
cont.append(ol);
|
|
} catch (err) { falla(cont, 'la lista de documentos', err); }
|
|
}
|
|
|
|
// ---------------------------------------------------------------- dónde estamos
|
|
async function pintarMapa(P) {
|
|
const cont = $('in-mapa'), ul = $('in-sitios');
|
|
try {
|
|
const r = await fetch('/img/mexico.svg', { cache: 'force-cache' });
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
const doc = new DOMParser().parseFromString(await r.text(), 'image/svg+xml');
|
|
const mapa = doc.documentElement;
|
|
if (mapa.nodeName.toLowerCase() !== 'svg') throw new Error('mapa inválido');
|
|
mapa.removeAttribute('width'); mapa.removeAttribute('height');
|
|
mapa.setAttribute('role', 'img'); mapa.setAttribute('aria-label', 'Mapa de México con los puertos y la oficina de Querétaro');
|
|
cont.innerHTML = '';
|
|
cont.append(document.importNode(mapa, true));
|
|
} catch (err) { cont.classList.add('sin-mapa'); }
|
|
// cada sitio con su color (rojo / amarillo / verde) y liga a sus pendientes (22-oct)
|
|
const aPend = id => '/tableros/pendientes/?sitio=' + encodeURIComponent(id);
|
|
const est = s => COLOR[s.estado] ? ' est-' + s.estado : '';
|
|
const pin = (s, oficina) => {
|
|
const p = el(s.id ? 'a' : 'span', { class: 'in-pin' + (oficina ? ' oficina' : '') + est(s) + ' et-' + (s.etiqueta || 'der'), style: 'left:' + s.x + '%;top:' + s.y + '%' });
|
|
if (s.id) { p.setAttribute('href', aPend(s.id)); p.setAttribute('title', s.nombre + ' · ' + s.ciudad + (COLOR[s.estado] ? ' — ' + COLOR[s.estado].toLowerCase() + (s.nota ? ': ' + s.nota : '') : '') + '. Ver sus pendientes'); }
|
|
p.append(el('span', { class: 'in-pin-p', 'aria-hidden': 'true' }), el('span', { class: 'in-pin-t', text: s.nombre }));
|
|
return p;
|
|
};
|
|
(P.sitios || []).forEach(s => cont.append(pin(s)));
|
|
if (P.oficina) cont.append(pin(Object.assign({ etiqueta: 'arriba' }, P.oficina), true));
|
|
ul.innerHTML = '';
|
|
const fila = (s, oficina) => {
|
|
const li = el('li', { class: est(s).trim() });
|
|
const a = el('a', { href: s.id ? aPend(s.id) : '#', title: 'Pendientes de ' + s.nombre });
|
|
a.append(el('span', { class: 'in-sit-luz', 'aria-hidden': 'true' }), el('b', { text: oficina ? s.ciudad : s.nombre }), ' ' + (oficina ? 'fabricación e ingeniería' : s.ciudad));
|
|
if (COLOR[s.estado]) a.append(el('span', { class: 'vh', text: ' (' + COLOR[s.estado].toLowerCase() + ')' }));
|
|
const estado = el('span', { class: 'in-gris', text: s.nota || '' });
|
|
li.append(a, estado);
|
|
// Debajo del sitio: su responsable de campo (proyecto.json, junta del 22-oct) y la meta, que lleva a sus instalaciones.
|
|
if (s.meta || s.responsable) {
|
|
const m = el('span', { class: 'in-sit-meta' });
|
|
if (s.responsable) m.append('Responsable: ' + s.responsable + (s.meta ? ' · ' : ''));
|
|
if (s.meta) m.append(el('a', { href: '/tableros/instalaciones/?sitio=' + encodeURIComponent(s.id), title: 'Instalaciones de ' + s.nombre, text: 'Meta: ' + s.meta + ' · ver instalaciones' }));
|
|
li.append(m);
|
|
}
|
|
ul.append(li);
|
|
return estado;
|
|
};
|
|
// Conteos por sitio: los mismos del tablero de Instalaciones (solo las probadas cuentan para la meta).
|
|
// Si el servicio todavía no tiene /api/instalaciones (falta reiniciar actas-hbox), se usa el conteo anterior.
|
|
const porSitio = json('/api/instalaciones').then(j => j.resumen || {}).catch(() => null);
|
|
const conteoAnterior = (s, estado) => json('/api/sitio/unidades?id=' + encodeURIComponent(s.id)).then(j => {
|
|
const n = j.total_unidades || 0, inst = (j.unidades || []).filter(u => u.instalada).length;
|
|
const partes = [];
|
|
if (s.nota) partes.push(s.nota);
|
|
partes.push(n ? plural(n, 'unidad con actas', 'unidades con actas') + ', ' + (inst ? plural(inst, 'instalada', 'instaladas') : 'ninguna instalada') : 'sin unidades registradas');
|
|
estado.textContent = partes.join(' · ');
|
|
});
|
|
(P.sitios || []).forEach(s => {
|
|
const estado = fila(s);
|
|
porSitio.then(R => {
|
|
if (!R) return conteoAnterior(s, estado);
|
|
const r = R[s.id] || {};
|
|
const partes = [];
|
|
if (s.nota) partes.push(s.nota);
|
|
if (r.unidades) {
|
|
let t = plural(r.instaladas || 0, 'instalada', 'instaladas') + ', ' + plural(r.probadas || 0, 'probada', 'probadas');
|
|
if (r.falla) t += ', ' + plural(r.falla, 'con falla', 'con falla');
|
|
partes.push(t);
|
|
} else partes.push('sin unidades registradas');
|
|
estado.textContent = partes.join(' · ');
|
|
}).catch(() => { estado.textContent = s.nota || ''; });
|
|
});
|
|
if (P.oficina && P.oficina.id) fila(P.oficina, true);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- arranque
|
|
async function iniciar() {
|
|
if (!$('inicio') || !window.Kanban) return;
|
|
const K = window.Kanban;
|
|
let P;
|
|
try { P = await json('/datos/proyecto.json'); }
|
|
catch (err) { falla($('in-ruta'), 'el estado del proyecto', err); return; }
|
|
|
|
pintarSemaforo(P);
|
|
pintarFase(P);
|
|
pintarHito(P);
|
|
pintarFechas(P, []);
|
|
window.Ruta.montar($('in-ruta'), P, { enlace: t => '/tableros/ingenieria/#' + t.clave });
|
|
|
|
// tablero de pendientes (extracto) y lo que depende de él
|
|
K.montar($('in-kb'), {
|
|
filtros: 'min', vistas: true, compacto: true, limite: { otros: 4, hecho: 3 }, limitePersona: 2,
|
|
enlaceMas: '/tableros/pendientes/', nuevoPorColumna: false, barraEn: $('in-pend-acc'),
|
|
onCargado: tareas => pintarFechas(P, tareas),
|
|
onPintado: (vis, info) => {
|
|
const ab = vis.filter(t => t.estado !== 'hecho').length, ve = vis.filter(t => t.vencida).length;
|
|
$('in-resumen').textContent = plural(ab, 'abierto', 'abiertos') + (ve ? ', ' + plural(ve, 'vencido', 'vencidos') : '') + (info.filtro.mios ? ', solo los tuyos' : '');
|
|
}
|
|
});
|
|
K.cargarEquipo().then(() => pintarEquipo(P));
|
|
// el enfoque espera al equipo para escribir los nombres («Lo escriben Leo y Adriana»)
|
|
Promise.all([json('/api/hoy').catch(() => null), K.cargarEquipo().catch(() => null)]).then(([r]) => pintarEnfoque(P, r));
|
|
Promise.all([json('/api/plan').catch(() => null), json('/api/actas').catch(() => null)])
|
|
.then(([p, a]) => pintarPedido(P, p && p.plan, a ? (a.actas || []) : null));
|
|
window.Actividad.montar($('in-actividad'), { dias: 7, limite: 6, corto: 46, mas: '/bitacora/#actividad' });
|
|
pintarDocs();
|
|
pintarMapa(P);
|
|
}
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', iniciar); else iniciar();
|
|
})();
|