Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
180 lines
16 KiB
Markdown
180 lines
16 KiB
Markdown
---
|
|
title: "Sitios"
|
|
description: "Todo lo que pasó en un sitio: unidades instaladas, envíos, diagnósticos, daños y contactos."
|
|
template: tablero.html
|
|
hide:
|
|
- toc
|
|
---
|
|
|
|
<div class="tb tb-sitio">
|
|
<header class="tb-cab">
|
|
<div class="tb-cab__t"><h1 id="titulo">Sitio</h1><span class="meta" id="meta">Cargando…</span></div>
|
|
<div class="tb-cab__acc"><a class="boton sec" href="/formularios/instalacion.html"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="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"/></svg>Nueva instalación</a><a class="boton sec" href="/formularios/diagnostico.html"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="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"/></svg>Diagnóstico</a><a class="boton sec" href="/tableros/pendientes/?pista=campo"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="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"/></svg>Pendientes de campo</a></div>
|
|
</header>
|
|
|
|
<details class="info tb-como">
|
|
<summary>Cómo se usa</summary>
|
|
<ol>
|
|
<li>Esta página junta todo lo que pasó en un sitio: unidades instaladas, envíos, recepciones, diagnósticos y daños. Sale de las actas; aquí no se captura nada de campo.</li>
|
|
<li>Para que una unidad aparezca, el acta debe traer el <b>sitio</b> elegido en el formulario (instalación, envío, recepción, diagnóstico, daño). Las actas viejas sin sitio se buscan por el nombre del puerto y se marcan «por texto».</li>
|
|
<li>Toca una serie para abrir su expediente completo. Rojo = unidad con marca de daño o baja.</li>
|
|
<li>Contactos y ubicación los edita solo el administrador (botón «Editar sitio»). Si falta un dato, díselo a Leo.</li>
|
|
<li>Para dar de alta otro sitio: <a href="/sitios/">lista de sitios</a> → «Nuevo sitio» (administrador).</li>
|
|
</ol>
|
|
</details>
|
|
|
|
<div class="sitio-sel"><label for="sel">Sitio</label><select id="sel"></select><button id="bEditar" class="sec" style="display:none">Editar sitio</button></div>
|
|
<div class="msg" id="aviso"></div>
|
|
<div id="cont"></div>
|
|
|
|
<dialog id="dlg"></dialog>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const el = (t, a, ...k) => { const e = document.createElement(t); if (a) for (const p in a) { if (p === 'html') e.innerHTML = a[p]; else if (p === 'text') e.textContent = a[p]; else e.setAttribute(p, a[p]); } k.forEach(c => c != null && e.append(c)); return e; };
|
|
const esc = s => { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; };
|
|
const fmt = iso => (iso || '').replace('T', ' ').slice(0, 16);
|
|
const $ = id => document.getElementById(id);
|
|
const NOMBRE = { 'instalacion-campo': 'Instalación', 'prueba-sitio': 'Prueba en sitio', 'diagnostico-campo': 'Diagnóstico en campo', 'envio': 'Envío', 'recepcion': 'Recepción', 'dano': 'Daño o incidente' };
|
|
let SITIOS = [], ADMIN = false, ACTUAL = null, DATOS = null;
|
|
const dlg = $('dlg'), aviso = $('aviso');
|
|
function mensaje(t, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.textContent = t; clearTimeout(aviso._t); aviso._t = setTimeout(() => aviso.className = 'msg', bien ? 4000 : 9000); }
|
|
async function api(url, opts) { const r = await fetch(url, Object.assign({ cache: 'no-store' }, opts || {})); 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; }
|
|
const json = (m, b) => ({ method: m, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(b) });
|
|
function campo(l, i) { const d = el('div'); d.append(el('label', { text: l }), i); return d; }
|
|
|
|
function render(j) {
|
|
const s = j.sitio, cont = $('cont'); cont.innerHTML = '';
|
|
$('titulo').textContent = s.nombre + (s.cliente ? ' — ' + s.cliente : '');
|
|
$('meta').textContent = (s.ubicacion || 'ubicación por confirmar') + ' · ' + j.total_unidades + ' unidad(es) · ' + j.total_actas + ' acta(s)' + (s.activo === false ? ' · INACTIVO' : '');
|
|
document.title = s.nombre + ' - Drogheda';
|
|
const ficha = el('div', { class: 'ficha' });
|
|
const c = (l, v) => ficha.append(el('div', { class: 'c' }, el('div', { class: 'l', text: l }), el('div', { class: 'v', html: v })));
|
|
c('Cliente', esc(s.cliente || '—')); c('Ubicación', esc(s.ubicacion || 'por confirmar'));
|
|
c('Unidades instaladas', String(j.unidades.filter(u => u.instalada).length) + ' <span style="font-weight:400;color:var(--tinta2);font-size:12.5px">con acta de instalación OK</span>');
|
|
c('Con daño o baja', '<span style="color:' + (j.danos.length ? 'var(--falla)' : 'inherit') + '">' + j.danos.length + '</span>');
|
|
cont.append(ficha);
|
|
cont.append(el('p', { class: 'sub', style: 'margin:-6px 0 12px', html: '<a href="/tableros/instalaciones/?sitio=' + encodeURIComponent(s.id) + '">Instalaciones de ' + esc(s.nombre) + '</a>: en qué vehículo está cada unidad, su prueba en sitio y la meta del sitio.' }));
|
|
if (s.notas) cont.append(el('p', { class: 'sub', style: 'margin:-6px 0 12px', text: s.notas }));
|
|
|
|
// contactos
|
|
const b0 = el('section', { class: 'bloque' });
|
|
b0.append(el('h2', { text: 'Contactos' }));
|
|
if (!(s.contactos || []).length) b0.append(el('div', { class: 'vacio', text: 'Sin contactos registrados. Pide a Leo que los agregue.' }));
|
|
(s.contactos || []).forEach(k => b0.append(el('div', { class: 'contacto', html: '<b>' + esc(k.nombre) + '</b>' + (k.rol ? ' <span class="r">· ' + esc(k.rol) + '</span>' : '') + (k.telefono ? ' · <a href="tel:' + esc(k.telefono) + '">' + esc(k.telefono) + '</a>' : '') + (k.correo ? ' · <a href="mailto:' + esc(k.correo) + '">' + esc(k.correo) + '</a>' : '') })));
|
|
cont.append(b0);
|
|
|
|
// unidades
|
|
const b1 = el('section', { class: 'bloque' });
|
|
b1.append(el('h2', { text: 'Unidades en este sitio' }), el('p', { class: 'sub', text: 'Toda serie con alguna acta que apunte a este sitio. «Instalada» = fecha de la última acta de instalación aprobada.' }));
|
|
if (!j.unidades.length) b1.append(el('div', { class: 'vacio', text: 'Todavía no hay unidades con actas en este sitio. Llena un acta de instalación o de envío con este sitio.' }));
|
|
else {
|
|
const t = el('table', { class: 't' }); t.innerHTML = '<tr><th>Unidad</th><th>Máquina</th><th>Instalada</th><th>Última acta</th><th>Actas</th><th>Estado</th></tr>';
|
|
j.unidades.forEach(u => {
|
|
const tr = el('tr');
|
|
tr.innerHTML = '<td><a href="/tableros/unidad/?serie=' + encodeURIComponent(u.serie) + '"><b>' + esc(u.serie) + '</b></a>' + (u.coincidencia === 'texto' ? ' <span class="por-texto" title="el acta no trae el sitio; coincide por el nombre del puerto">por texto</span>' : '') + '</td>'
|
|
+ '<td>' + esc(u.maquina || '—') + '</td><td>' + (u.instalada ? '<span class="pill ok">' + esc(u.instalada) + '</span>' : '<span class="pill pend">sin acta</span>') + '</td>'
|
|
+ '<td>' + esc(NOMBRE[(u.ultima || {}).tipo] || (u.ultima || {}).tipo || '—') + (u.ultima && u.ultima.fecha ? ' · ' + esc(u.ultima.fecha) : '') + (u.ultima && u.ultima.accion ? '<div style="font-size:12px;color:var(--tinta2)">' + esc(u.ultima.accion) + '</div>' : '') + '</td>'
|
|
+ '<td>' + u.actas + '</td><td>' + (u.marca ? '<span class="pill ' + (u.marca === 'BAJA' ? 'falla' : 'aviso') + '">' + (u.marca === 'BAJA' ? 'BAJA' : 'DAÑADA') + '</span>' : '<span class="pill ok">sin marca</span>') + '</td>';
|
|
t.append(tr);
|
|
});
|
|
b1.append(el('div', { class: 'tblwrap' }, t));
|
|
}
|
|
cont.append(b1);
|
|
|
|
// daños abiertos
|
|
if (j.danos.length) {
|
|
const b2 = el('section', { class: 'bloque' });
|
|
b2.append(el('h2', { text: 'Daños abiertos' }), el('p', { class: 'sub', text: 'Unidades de este sitio con marca vigente. Se levanta con un acta de daño «Reparada».' }));
|
|
j.danos.forEach(u => { const a = u.marca_acta || {}; b2.append(el('div', { class: 'ev dano', style: 'padding-left:0', html: '<b><a href="/tableros/unidad/?serie=' + encodeURIComponent(u.serie) + '">' + esc(u.serie) + '</a></b> · ' + esc(a.clasificacion || '') + (a.resumen ? ' — ' + esc(a.resumen) : '') + '<div class="f">' + esc(a.fecha || fmt(a.recibido)) + ' · ' + esc(a.tecnico || '') + (a.accion ? ' · ' + esc(a.accion) : '') + '</div>' })); });
|
|
cont.append(b2);
|
|
}
|
|
|
|
// actas
|
|
const b3 = el('section', { class: 'bloque' });
|
|
b3.append(el('h2', { text: 'Actas de instalación y campo' }), el('p', { class: 'sub', text: 'De la más reciente a la más vieja: instalaciones, diagnósticos, envíos, recepciones y daños.' }));
|
|
if (!j.actas.length) b3.append(el('div', { class: 'vacio', text: 'Sin actas.' }));
|
|
else {
|
|
const lt = el('div', { class: 'linea' });
|
|
j.actas.forEach(a => {
|
|
const r = String(a.resultado || '');
|
|
lt.append(el('div', { class: 'ev ' + a.tipo, html: '<b>' + esc(NOMBRE[a.tipo] || a.tipo) + '</b> — <a href="/tableros/unidad/?serie=' + encodeURIComponent(a.serie) + '">' + esc(a.serie) + '</a>'
|
|
+ (a.maquina ? ' · ' + esc(a.maquina) : '') + ' <span class="pill ' + (r === 'FALLA' ? 'falla' : r ? 'ok' : 'pend') + '">' + esc(r || '—') + '</span>'
|
|
+ (a.coincidencia === 'texto' ? ' <span class="por-texto">por texto</span>' : '')
|
|
+ '<div class="f">' + esc(a.fecha || '') + ' · recibida ' + esc(fmt(a.recibido)) + ' · ' + esc(a.tecnico || '—') + (a.fotos ? ' · ' + a.fotos + ' foto(s)' : '') + '</div>'
|
|
+ (a.accion ? '<div>→ ' + esc(a.accion) + '</div>' : '') + (a.motivo ? '<div>Motivo: ' + esc(a.motivo) + '</div>' : '') + (a.resumen ? '<div>' + esc(a.resumen) + '</div>' : '') }));
|
|
});
|
|
b3.append(lt);
|
|
}
|
|
cont.append(b3);
|
|
}
|
|
|
|
async function cargar(id) {
|
|
$('cont').innerHTML = '<p class="vacio">Cargando…</p>';
|
|
try { DATOS = await api('/api/sitio/unidades?id=' + encodeURIComponent(id)); ACTUAL = DATOS.sitio; render(DATOS); $('bEditar').style.display = ADMIN ? '' : 'none'; }
|
|
catch (e) { $('cont').innerHTML = ''; $('cont').append(el('div', { class: 'err', text: 'No se pudo leer el sitio: ' + e.message })); $('meta').textContent = 'Error'; }
|
|
}
|
|
|
|
// ---------- alta / edición (admin) ----------
|
|
function formSitio(c, s) {
|
|
s = s || {}; const f = {};
|
|
f.id = el('input', { type: 'text', maxlength: '30', value: s.id || '', placeholder: 'icave (minúsculas, sin espacios)' }); if (s.id) f.id.disabled = true;
|
|
f.nombre = el('input', { type: 'text', maxlength: '80', value: s.nombre || '', placeholder: 'ICAVE' });
|
|
f.cliente = el('input', { type: 'text', maxlength: '120', value: s.cliente || '', placeholder: 'Hutchison Ports' });
|
|
f.ubicacion = el('input', { type: 'text', maxlength: '200', value: s.ubicacion || '', placeholder: 'Veracruz, Ver.' });
|
|
f.alias = el('input', { type: 'text', value: (s.alias || []).join(', '), placeholder: 'LCT, Lázaro Cárdenas (nombres con los que aparece en actas viejas)' });
|
|
f.notas = el('textarea', { rows: '2' }); f.notas.value = s.notas || '';
|
|
f.activo = el('select'); [['true', 'Activo'], ['false', 'Inactivo (ya no se muestra en los formularios)']].forEach(([v, t]) => f.activo.append(el('option', { value: v, text: t }))); f.activo.value = s.activo === false ? 'false' : 'true';
|
|
const g = el('div', { class: 'dos-col' }); g.append(campo('Id (slug)', f.id), campo('Nombre', f.nombre), campo('Cliente', f.cliente), campo('Ubicación', f.ubicacion)); c.append(g);
|
|
c.append(campo('Alias (separados por coma)', f.alias));
|
|
const ced = el('div', { class: 'contactos-ed' });
|
|
function fila(k) { const r = el('div', { class: 'fila' }); r.append(el('input', { type: 'text', value: k.nombre || '', placeholder: 'Nombre' }), el('input', { type: 'text', value: k.rol || '', placeholder: 'Rol' }), el('input', { type: 'text', value: k.telefono || '', placeholder: 'Teléfono' }), el('input', { type: 'text', value: k.correo || '', placeholder: 'Correo' })); const q = el('button', { type: 'button', class: 'sec', 'aria-label': 'Quitar el contacto', html: Kanban.icono('cerrar') }); q.addEventListener('click', () => r.remove()); r.append(q); ced.append(r); }
|
|
(s.contactos || []).forEach(fila);
|
|
const add = el('button', { type: 'button', class: 'sec', html: Kanban.icono('mas') + 'Contacto', style: 'padding:6px 10px;font-size:13px' }); add.addEventListener('click', () => fila({}));
|
|
c.append(campo('Contactos', ced), add, campo('Notas', f.notas), campo('Estado', f.activo));
|
|
return () => ({ id: f.id.value.trim().toLowerCase(), nombre: f.nombre.value.trim(), cliente: f.cliente.value.trim(), ubicacion: f.ubicacion.value.trim(),
|
|
alias: f.alias.value.split(',').map(x => x.trim()).filter(Boolean), notas: f.notas.value.trim(), activo: f.activo.value === 'true',
|
|
contactos: [...ced.querySelectorAll('.fila')].map(r => ({ nombre: r.children[0].value.trim(), rol: r.children[1].value.trim(), telefono: r.children[2].value.trim(), correo: r.children[3].value.trim() })).filter(k => k.nombre) });
|
|
}
|
|
function dialogo(titulo, s) {
|
|
dlg.innerHTML = '';
|
|
const cab = el('div', { class: 'dcab' }); cab.append(el('h2', { text: titulo })); const x = el('button', { type: 'button', 'aria-label': 'Cerrar', html: Kanban.icono('cerrar') }); x.addEventListener('click', () => dlg.close()); cab.append(x); dlg.append(cab);
|
|
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
|
const leer = formSitio(cuerpo, s);
|
|
const m = el('div', { class: 'msg' }); cuerpo.append(m);
|
|
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
|
const cancelar = el('button', { type: 'button', class: 'sec', text: 'Cancelar' }); cancelar.addEventListener('click', () => dlg.close());
|
|
const guardar = el('button', { type: 'button', text: s ? 'Guardar' : 'Crear sitio' });
|
|
guardar.addEventListener('click', async () => {
|
|
const d = leer();
|
|
if (!d.nombre) { m.className = 'msg err'; m.textContent = 'Escribe el nombre.'; return; }
|
|
guardar.disabled = true;
|
|
try {
|
|
if (s) { delete d.id; await api('/api/sitio?id=' + encodeURIComponent(s.id), json('PATCH', d)); dlg.close(); mensaje('Sitio guardado.', true); await cargarLista(); cargar(s.id); }
|
|
else { const j = await api('/api/sitios', json('POST', d)); dlg.close(); mensaje('Sitio ' + j.sitio.nombre + ' creado.', true); await cargarLista(); history.replaceState(null, '', '?id=' + encodeURIComponent(j.sitio.id)); $('sel').value = j.sitio.id; cargar(j.sitio.id); }
|
|
} catch (e) { m.className = 'msg err'; m.textContent = e.message; }
|
|
guardar.disabled = false;
|
|
});
|
|
pie.append(cancelar, guardar);
|
|
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute('open', '');
|
|
}
|
|
$('bEditar').addEventListener('click', () => ACTUAL && dialogo('Editar sitio ' + ACTUAL.nombre, ACTUAL));
|
|
|
|
async function cargarLista() {
|
|
const j = await api('/api/sitios?todos=1'); SITIOS = j.sitios || []; ADMIN = !!j.admin;
|
|
const sel = $('sel'); sel.innerHTML = ''; sel.append(el('option', { value: '', text: '— elegir sitio —' }));
|
|
SITIOS.forEach(s => sel.append(el('option', { value: s.id, text: s.nombre + (s.activo === false ? ' (inactivo)' : '') })));
|
|
if (ADMIN) sel.append(el('option', { value: '__nuevo__', text: '+ Nuevo sitio…' }));
|
|
}
|
|
$('sel').addEventListener('change', () => { const v = $('sel').value; if (v === '__nuevo__') { $('sel').value = ACTUAL ? ACTUAL.id : ''; dialogo('Nuevo sitio', null); } else if (v) { history.replaceState(null, '', '?id=' + encodeURIComponent(v)); cargar(v); } });
|
|
(async () => {
|
|
try { await cargarLista(); } catch (e) { $('cont').innerHTML = ''; $('cont').append(el('div', { class: 'err', text: 'No se pudo leer la lista de sitios: ' + e.message })); return; }
|
|
const q = new URLSearchParams(location.search);
|
|
if (q.get('nuevo') === '1') { if (ADMIN) dialogo('Nuevo sitio', null); else mensaje('Solo el administrador da de alta sitios. Pide a Leo que lo cree.', false); }
|
|
const id = q.get('id') || (SITIOS[0] || {}).id;
|
|
if (id) { $('sel').value = id; cargar(id); } else { $('meta').textContent = 'No hay sitios dados de alta.'; $('cont').innerHTML = '<p class="vacio">Pide a Leo que cree el primer sitio.</p>'; }
|
|
})();
|
|
})();
|
|
</script>
|