hbox-docs/docs/inicio/inicio.js

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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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();
})();