auto-sync 2026-10-20 18:00
@ -42,7 +42,7 @@ El acta de ensamble no deja elegir un módulo sin aprobar, sin integración o ya
|
|||||||
| Acta | Qué registra | Procedimiento |
|
| Acta | Qué registra | Procedimiento |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| [Instalación en campo](../formularios/instalacion.html) | Una por unidad y vehículo: sitio, equipo, alimentación 24 V, antenas, RTK FIX, señal 4G, fotos | [Checklist de puesta en marcha](../procedimientos/v5-checklist-puesta-en-marcha.md) · [Sitios](../sitios/index.md) |
|
| [Instalación en campo](../formularios/instalacion.html) | Una por unidad y vehículo: sitio, equipo, alimentación 24 V, antenas, RTK FIX, señal 4G, fotos | [Checklist de puesta en marcha](../procedimientos/v5-checklist-puesta-en-marcha.md) · [Sitios](../sitios/index.md) |
|
||||||
| [Diagnóstico en campo](../formularios/diagnostico.html) | Una por visita; muestra las visitas anteriores y admite fotos desde el teléfono | [Diagnóstico en campo](../diagnostico/procedimiento-diagnostico-campo.md) · [Tablero de campo](../diagnostico/tablero-campo.html) |
|
| [Diagnóstico en campo](../formularios/diagnostico.html) | Una por visita; muestra las visitas anteriores y admite fotos desde el teléfono | [Diagnóstico en campo](../diagnostico/procedimiento-diagnostico-campo.md) · [Tablero de campo](../tableros/campo.md) |
|
||||||
| [Envío](../formularios/envio.html) · [Recepción](../formularios/recepcion.html) | El tramo entre el banco y el puerto | [Ciclo de vida](../procedimientos/ciclo-de-vida.md) |
|
| [Envío](../formularios/envio.html) · [Recepción](../formularios/recepcion.html) | El tramo entre el banco y el puerto | [Ciclo de vida](../procedimientos/ciclo-de-vida.md) |
|
||||||
| [Daño o incidente](../formularios/dano.html) | Cualquier serie; la marca en todos los formularios. «Reparada» la levanta, «Baja» la hace permanente | [Ciclo de vida](../procedimientos/ciclo-de-vida.md#dano-o-incidente) |
|
| [Daño o incidente](../formularios/dano.html) | Cualquier serie; la marca en todos los formularios. «Reparada» la levanta, «Baja» la hace permanente | [Ciclo de vida](../procedimientos/ciclo-de-vida.md#dano-o-incidente) |
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ El acta de ensamble no deja elegir un módulo sin aprobar, sin integración o ya
|
|||||||
1. Abre el formulario y elige la serie (o los módulos, en integración y ensamble).
|
1. Abre el formulario y elige la serie (o los módulos, en integración y ensamble).
|
||||||
2. Llena cada sección conforme avanzas. Se guarda solo en el navegador: al reabrir con la misma serie recuperas lo escrito.
|
2. Llena cada sección conforme avanzas. Se guarda solo en el navegador: al reabrir con la misma serie recuperas lo escrito.
|
||||||
3. Marca el resultado (Aprobada / Falla), escribe tu nombre y confirma que seguiste el procedimiento.
|
3. Marca el resultado (Aprobada / Falla), escribe tu nombre y confirma que seguiste el procedimiento.
|
||||||
4. Pulsa Enviar acta firmada. Queda en el servidor y en el [tablero](../tablero.html).
|
4. Pulsa Enviar acta firmada. Queda en el servidor y en el [tablero](../tableros/actas.md).
|
||||||
|
|
||||||
La serie PRUEBA sirve para practicar: no cuenta y se puede reenviar.
|
La serie PRUEBA sirve para practicar: no cuenta y se puede reenviar.
|
||||||
|
|
||||||
|
|||||||
@ -4,21 +4,22 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Administración — HBOX</title>
|
<title>Administración — HBOX</title>
|
||||||
|
<script src="js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="formularios/acta.css">
|
<link rel="stylesheet" href="formularios/acta.css">
|
||||||
<style>
|
<style>
|
||||||
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:6px}
|
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:6px}
|
||||||
th{text-align:left;color:var(--tinta2);font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;padding:6px 8px 6px 0;border-bottom:1px solid var(--tinta2)}
|
th{text-align:left;color:var(--tinta2);font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;padding:6px 8px 6px 0;border-bottom:1px solid var(--tinta2)}
|
||||||
td{padding:7px 8px 7px 0;border-bottom:1px solid var(--linea)}
|
td{padding:7px 8px 7px 0;border-bottom:1px solid var(--linea)}
|
||||||
.del{background:#fff;color:var(--falla);border:1px solid var(--falla);border-radius:8px;padding:5px 10px;font-size:13px;cursor:pointer}
|
.del{background:transparent;color:var(--falla);border:1px solid var(--falla);border-radius:8px;padding:5px 10px;font-size:13px;cursor:pointer}
|
||||||
.bulk{background:var(--falla);color:#fff;border:0;border-radius:9px;padding:9px 14px;font-size:13.5px;cursor:pointer;margin:4px 8px 4px 0}
|
.bulk{background:var(--falla);color:#fff;border:0;border-radius:9px;padding:9px 14px;font-size:13.5px;cursor:pointer;margin:4px 8px 4px 0}
|
||||||
.ver{color:var(--plane);text-decoration:none;font-weight:600}
|
.ver{color:var(--plane);text-decoration:none;font-weight:600}
|
||||||
.aviso{background:var(--crit-soft);border-left:3px solid var(--crit);padding:10px 14px;border-radius:0 8px 8px 0;margin:10px 0}
|
.aviso{background:var(--crit-soft);border:1px solid var(--crit);padding:10px 14px;border-radius:8px;margin:10px 0}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<header class="top"><h1>Administración de actas</h1><p id="sub">Solo para el administrador.</p></header>
|
<header class="top"><h1>Administración de actas</h1><p id="sub">Solo para el administrador.</p></header>
|
||||||
<div style="margin:2px 0 10px;display:flex;gap:16px;flex-wrap:wrap"><a class="volver" href="./tablero.html">← Tablero</a><a class="volver" href="tableros/equipo.html">Usuarios y equipo →</a></div>
|
<div style="margin:2px 0 10px;display:flex;gap:16px;flex-wrap:wrap"><a class="volver" href="tableros/actas/">← Estado de actas</a><a class="volver" href="tableros/equipo/">Usuarios y equipo →</a></div>
|
||||||
<div id="cont"><p>Comprobando permisos…</p></div>
|
<div id="cont"><p>Comprobando permisos…</p></div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
65
docs/bitacora.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: "Bitácora"
|
||||||
|
description: Los documentos del proyecto por fecha, del más nuevo al más viejo.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bitácora
|
||||||
|
|
||||||
|
Los documentos con fecha, del más nuevo al más viejo. El menú lateral muestra solo lo vigente; aquí queda todo, incluido lo superado. Lo que pasa cada día en el tablero (quién movió qué) se verá aquí cuando el servidor lo publique; por ahora vive en el historial de cada [pendiente](tableros/pendientes.md).
|
||||||
|
|
||||||
|
!!! info "Cómo se agrega un documento"
|
||||||
|
Arriba, con su fecha, una línea que diga qué decide o qué cambia, y la liga. En `mkdocs.yml` va en *Tarjetas y hardware → Auditorías y pasadas*; cuando otro lo supere, pasa a *Historia → Tarjetas superadas* y aquí se marca «superado».
|
||||||
|
|
||||||
|
## Octubre de 2026
|
||||||
|
|
||||||
|
**19 oct (tarde)**
|
||||||
|
: [V5.6: qué es crítico y qué se puede quedar](hardware/v56-clasificacion-19oct.md). Un solo crítico, nuevo: C129 en VSYS de U6. L7 y lo heredado de la V5.5 se quedan para el piloto y se miden en el banco.
|
||||||
|
|
||||||
|
**19 oct**
|
||||||
|
: [Re-auditoría de V5.6, V5.7 y v6](hardware/re-auditoria-19oct.md), sobre la pasada 3 de Michelle. V5.6 casi lista; V5.7 con un bloqueante (`/SYS5V` por In2); v6 sin avance en sus 8 bloqueantes.
|
||||||
|
: [Pasada 4: cierre de la V5.6 y la V5.7](hardware/v56-v57-layout-pasada-4.md). Instrucciones para Michelle; parte A (V5.6) y parte B (V5.7).
|
||||||
|
: [Plan de fines de semana de la v6, rev. 1.1](hardware/v6-plan-fines-de-semana.md). Recorrido una semana: 24–25 oct y 31 oct–1 nov; piloto de 20 el lunes 2 de noviembre.
|
||||||
|
|
||||||
|
**16 oct**
|
||||||
|
: [Auditoría de producción de la v6](hardware/v6-auditoria-produccion-16oct.md). No lista: potencia por capas internas, RF del módem por In1, sin barrenos.
|
||||||
|
: [Auditoría de producción de V5.6, V5.7 y v6](hardware/auditoria-produccion-16oct.md). Requisitos uno por uno y la prueba de RS1 con TP1.
|
||||||
|
: [Pasada 3 de V5.6 y V5.7](hardware/v56-v57-layout-pasada-3.md) (superada por la pasada 4).
|
||||||
|
|
||||||
|
**14 oct**
|
||||||
|
: [V5.7: colocación y ruteo](hardware/v57-colocacion-ruteo.md) (superado por la pasada 3).
|
||||||
|
|
||||||
|
**13 oct**
|
||||||
|
: [ECO V5.7: fuente de 24 V a 5 V a bordo](hardware/v57-eco-fuente.md), v0.2. Entrada de 9 a 32 V sin la SD-15B.
|
||||||
|
: [Pasada 2 de la V5.6](hardware/v56-layout-pasada-2.md) (superada por la pasada 3).
|
||||||
|
: [Auditoría del layout de la V5.6](hardware/v56-auditoria-layout-13oct.md), en inglés (superada por la del 16 oct).
|
||||||
|
: [Guía de layout de la fuente de la V5.7](hardware/v57-layout-guia.md) (superada: la fuente ya está colocada).
|
||||||
|
|
||||||
|
**12 oct**
|
||||||
|
: [Pasada 1 de la V5.6](hardware/v56-layout-pasada-1.md) (superada por la pasada 2).
|
||||||
|
|
||||||
|
**7 oct**
|
||||||
|
: [V4 sin NMEA: riel SW_5V y arranque del F9P](procedimientos/v4-prueba-riel-sw5v-f9p.md). Procedimiento de banco con el mapa de puntos de medición.
|
||||||
|
|
||||||
|
**1 oct**
|
||||||
|
: [Hoja de datos de la V5.6](hardware/v56-datasheet.md).
|
||||||
|
|
||||||
|
## Septiembre de 2026
|
||||||
|
|
||||||
|
**29 sep**
|
||||||
|
: [Pinout de la V5.6](hardware/v56-pinout.md) y [reglas de layout, del esquemático a la PCB](hardware/v56-layout-instrucciones.md).
|
||||||
|
: [v6: próximos pasos para ordenar](hardware/v6-proximos-pasos.md) (superado el 16 oct).
|
||||||
|
: [Guía del ingeniero](guia-ingeniero.md): reglas del sitio y cómo se trabaja.
|
||||||
|
|
||||||
|
**28 sep**
|
||||||
|
: [Auditoría completa de la v6](hardware/v6-auditoria-completa.md) ([en inglés](hardware/v6-full-audit.md)), con su [plan de corrección](hardware/v6-plan-correccion.md) y el [mapa de pines del MCU](hardware/v6-pinmap.md).
|
||||||
|
: Pasadas de la v6: [1, cambios al esquemático](hardware/v6-cambios-esquematico.md); [2, puertos y sin nueva versión](hardware/v6-puertos-sin-nueva-version.md); [3, revisión final](hardware/v6-revision-final.md); [resumen de las tres](hardware/v6-cambios-resumen.md).
|
||||||
|
: [Cambios al esquemático de la V5.5 a la V5.6](hardware/v56-cambios-esquematico.md).
|
||||||
|
: [v6 contra las lecciones de la V5.5](hardware/v6-revision-lecciones-v55.md) (superada).
|
||||||
|
|
||||||
|
**27 sep**
|
||||||
|
: [ECO V5.6: requisitos R1 a R5](hardware/v56-eco.md).
|
||||||
|
|
||||||
|
**25 sep**
|
||||||
|
: [Auditoría pre-fabricación de la v6](hardware/v6-auditoria-prefab.md) ([en inglés](hardware/v6-prefab-audit.md)) (superada por la del 28 sep).
|
||||||
|
|
||||||
|
Lo anterior a septiembre está en la [historia del proyecto](historia/index.md).
|
||||||
@ -10,10 +10,10 @@ Unidad **en la máquina o en camino**: instalación en el puerto, falla en sitio
|
|||||||
|
|
||||||
| Sitio | Puerto | Página |
|
| Sitio | Puerto | Página |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| ICAVE | Veracruz | [Sitio ICAVE](../sitios/sitio.html?id=icave) |
|
| ICAVE | Veracruz | [Sitio ICAVE](../tableros/sitio.md?id=icave) |
|
||||||
| LZC | Lázaro Cárdenas | [Sitio LZC](../sitios/sitio.html?id=lzc) |
|
| LZC | Lázaro Cárdenas | [Sitio LZC](../tableros/sitio.md?id=lzc) |
|
||||||
| EIT | Ensenada | [Sitio EIT](../sitios/sitio.html?id=eit) |
|
| EIT | Ensenada | [Sitio EIT](../tableros/sitio.md?id=eit) |
|
||||||
| TIMSA | Manzanillo | [Sitio TIMSA](../sitios/sitio.html?id=timsa) |
|
| TIMSA | Manzanillo | [Sitio TIMSA](../tableros/sitio.md?id=timsa) |
|
||||||
|
|
||||||
Cada sitio muestra sus unidades instaladas y sus actas de instalación y diagnóstico. Dar de alta un sitio nuevo: [Sitios](../sitios/index.md) (solo administradores). Las páginas de sitio las publica el otro agente (WP5); si una liga no abre todavía, usa el [índice de sitios](../sitios/index.md).
|
Cada sitio muestra sus unidades instaladas y sus actas de instalación y diagnóstico. Dar de alta un sitio nuevo: [Sitios](../sitios/index.md) (solo administradores). Las páginas de sitio las publica el otro agente (WP5); si una liga no abre todavía, usa el [índice de sitios](../sitios/index.md).
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ Cada sitio muestra sus unidades instaladas y sus actas de instalación y diagnó
|
|||||||
| Llega una unidad al puerto o al taller | [Ciclo de vida §7](../procedimientos/ciclo-de-vida.md#7-recepcion-e-inspeccion) | [Recepción e inspección](../formularios/recepcion.html) |
|
| Llega una unidad al puerto o al taller | [Ciclo de vida §7](../procedimientos/ciclo-de-vida.md#7-recepcion-e-inspeccion) | [Recepción e inspección](../formularios/recepcion.html) |
|
||||||
| Algo se dañó (cualquier serie) | [Ciclo de vida: daño](../procedimientos/ciclo-de-vida.md#dano-o-incidente) | [Daño o incidente](../formularios/dano.html) |
|
| Algo se dañó (cualquier serie) | [Ciclo de vida: daño](../procedimientos/ciclo-de-vida.md#dano-o-incidente) | [Daño o incidente](../formularios/dano.html) |
|
||||||
|
|
||||||
El checklist impreso se sube al [expediente](../unidad.html) junto con el acta de instalación. El acta de instalación es nueva (oct-2026): mientras no esté publicada, el checklist impreso sigue siendo el registro.
|
El checklist impreso se sube al [expediente](../tableros/unidad.md) junto con el acta de instalación. El acta de instalación es nueva (oct-2026): mientras no esté publicada, el checklist impreso sigue siendo el registro.
|
||||||
|
|
||||||
## Lo que hay que saber en campo
|
## Lo que hay que saber en campo
|
||||||
|
|
||||||
@ -38,8 +38,8 @@ El checklist impreso se sube al [expediente](../unidad.html) junto con el acta d
|
|||||||
|
|
||||||
## Tableros y expediente
|
## Tableros y expediente
|
||||||
|
|
||||||
- [Expediente de una unidad](../unidad.html): cada acta, foto y log por serie, incluida la cadena de campo (envío → instalación → diagnóstico).
|
- [Expediente de una unidad](../tableros/unidad.md): cada acta, foto y log por serie, incluida la cadena de campo (envío → instalación → diagnóstico).
|
||||||
- [Tablero de campo](../diagnostico/tablero-campo.html): todas las visitas de diagnóstico.
|
- [Tablero de campo](../tableros/campo.md): todas las visitas de diagnóstico.
|
||||||
- [Sitios](../sitios/index.md): unidades por puerto.
|
- [Sitios](../sitios/index.md): unidades por puerto.
|
||||||
- [Índice de actas](../actas/index.md): cómo se llenan.
|
- [Índice de actas](../actas/index.md): cómo se llenan.
|
||||||
- Para el cliente: [manual del cliente](../manuales/manual-cliente.md) · [PDF de instalación y operación](../manuales/Drogheda-Manual-Instalacion-Operacion_v2.0.pdf)
|
- Para el cliente: [manual del cliente](../manuales/manual-cliente.md) · [PDF de instalación y operación](../manuales/Drogheda-Manual-Instalacion-Operacion_v2.0.pdf)
|
||||||
|
|||||||
@ -4,16 +4,17 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Mi cuenta — HBOX</title>
|
<title>Mi cuenta — HBOX</title>
|
||||||
|
<script src="js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="formularios/acta.css">
|
<link rel="stylesheet" href="formularios/acta.css">
|
||||||
<style>
|
<style>
|
||||||
.campo{margin:12px 0}
|
.campo{margin:12px 0}
|
||||||
.quien{background:var(--naranja-suave);border:1px solid #ffd8a8;border-radius:10px;
|
.quien{background:var(--hg-plano);border:1px solid var(--linea);border-radius:10px;
|
||||||
padding:12px 14px;margin-bottom:14px;font-size:15px}
|
padding:12px 14px;margin-bottom:14px;font-size:15px}
|
||||||
.quien b{font-size:17px}
|
.quien b{font-size:17px}
|
||||||
.fuerza{height:6px;border-radius:3px;background:var(--linea);margin-top:6px;overflow:hidden}
|
.fuerza{height:6px;border-radius:3px;background:var(--linea);margin-top:6px;overflow:hidden}
|
||||||
.fuerza i{display:block;height:100%;width:0;transition:width .2s,background .2s}
|
.fuerza i{display:block;height:100%;width:0;transition:width .2s,background .2s}
|
||||||
.pista{font-size:12.5px;color:var(--tinta2);margin-top:5px}
|
.pista{font-size:12.5px;color:var(--tinta2);margin-top:5px}
|
||||||
.admin{border:1px solid var(--linea);border-left:4px solid var(--naranja)}
|
.admin{border:1px solid var(--linea)}
|
||||||
button.enviar{width:100%;margin-top:6px}
|
button.enviar{width:100%;margin-top:6px}
|
||||||
.ver{font-size:13px;color:var(--naranja);cursor:pointer;user-select:none;margin-top:6px;display:inline-block}
|
.ver{font-size:13px;color:var(--naranja);cursor:pointer;user-select:none;margin-top:6px;display:inline-block}
|
||||||
</style>
|
</style>
|
||||||
@ -47,7 +48,7 @@
|
|||||||
<div class="campo">
|
<div class="campo">
|
||||||
<label for="confirma">Repite la contraseña nueva</label>
|
<label for="confirma">Repite la contraseña nueva</label>
|
||||||
<input type="password" id="confirma" autocomplete="new-password">
|
<input type="password" id="confirma" autocomplete="new-password">
|
||||||
<span class="ver" id="ver">👁 Ver lo que escribí</span>
|
<span class="ver" id="ver">Ver lo que escribí</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="enviar" id="guardar">Guardar contraseña nueva</button>
|
<button class="enviar" id="guardar">Guardar contraseña nueva</button>
|
||||||
<div class="msg" id="msg"></div>
|
<div class="msg" id="msg"></div>
|
||||||
@ -72,7 +73,7 @@
|
|||||||
<h2>Si olvidaste tu contraseña</h2>
|
<h2>Si olvidaste tu contraseña</h2>
|
||||||
<p class="sub" style="margin:0">
|
<p class="sub" style="margin:0">
|
||||||
No hay recuperación por correo: pídele a Leo que te la restablezca desde esta misma página.
|
No hay recuperación por correo: pídele a Leo que te la restablezca desde esta misma página.
|
||||||
Si todavía no tienes acceso, pídeselo a Leo (Administración → <a href="tableros/equipo.html">Usuarios y equipo</a>).
|
Si todavía no tienes acceso, pídeselo a Leo (Administración → <a href="tableros/equipo/">Usuarios y equipo</a>).
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -94,7 +95,7 @@ function aviso(el, texto, bien) {
|
|||||||
yo = j.user || '';
|
yo = j.user || '';
|
||||||
$('quien').innerHTML = yo
|
$('quien').innerHTML = yo
|
||||||
? 'Entraste como <b>' + yo + '</b>' + (j.admin ? ' · administrador' : '')
|
? 'Entraste como <b>' + yo + '</b>' + (j.admin ? ' · administrador' : '')
|
||||||
: '⚠ No puedo identificarte. Vuelve a entrar al sitio y recarga esta página.';
|
: 'No puedo identificarte. Vuelve a entrar al sitio y recarga esta página.';
|
||||||
if (j.admin) {
|
if (j.admin) {
|
||||||
$('bloqueAdmin').style.display = '';
|
$('bloqueAdmin').style.display = '';
|
||||||
try {
|
try {
|
||||||
@ -130,7 +131,7 @@ $('nueva').addEventListener('input', () => {
|
|||||||
$('ver').addEventListener('click', () => {
|
$('ver').addEventListener('click', () => {
|
||||||
const t = $('nueva').type === 'password' ? 'text' : 'password';
|
const t = $('nueva').type === 'password' ? 'text' : 'password';
|
||||||
$('nueva').type = t; $('confirma').type = t; $('actual').type = t;
|
$('nueva').type = t; $('confirma').type = t; $('actual').type = t;
|
||||||
$('ver').textContent = t === 'text' ? '🙈 Ocultar' : '👁 Ver lo que escribí';
|
$('ver').textContent = t === 'text' ? 'Ocultar' : 'Ver lo que escribí';
|
||||||
});
|
});
|
||||||
|
|
||||||
const AVISO_SESION =
|
const AVISO_SESION =
|
||||||
@ -148,7 +149,7 @@ $('guardar').addEventListener('click', async () => {
|
|||||||
});
|
});
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||||
aviso($('msg'), '✓ <b>Contraseña cambiada.</b>' + AVISO_SESION, true);
|
aviso($('msg'), '<b>Contraseña cambiada.</b>' + AVISO_SESION, true);
|
||||||
$('actual').value = $('nueva').value = $('confirma').value = '';
|
$('actual').value = $('nueva').value = $('confirma').value = '';
|
||||||
$('barra').style.width = '0';
|
$('barra').style.width = '0';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -169,7 +170,7 @@ $('resetear').addEventListener('click', async () => {
|
|||||||
});
|
});
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||||
aviso($('msgR'), '✓ Contraseña de <b>' + u + '</b> restablecida. Pásasela en persona o por '
|
aviso($('msgR'), 'Contraseña de <b>' + u + '</b> restablecida. Pásasela en persona o por '
|
||||||
+ 'mensaje directo, y dile que la cambie aquí mismo cuando entre.'
|
+ 'mensaje directo, y dile que la cambie aquí mismo cuando entre.'
|
||||||
+ (u === yo ? AVISO_SESION : ''), true);
|
+ (u === yo ? AVISO_SESION : ''), true);
|
||||||
$('nuevaR').value = '';
|
$('nuevaR').value = '';
|
||||||
|
|||||||
@ -15,7 +15,7 @@ version: "2.3"
|
|||||||
| Módem 4G NTRIP Master | Parámetros `$$$AS+…` | Terminal serie por el USB XBEE | [Módem 4G](#parametros-del-modem-4g-ntrip-master) |
|
| Módem 4G NTRIP Master | Parámetros `$$$AS+…` | Terminal serie por el USB XBEE | [Módem 4G](#parametros-del-modem-4g-ntrip-master) |
|
||||||
| simpleRTK2B Pro (ZED-F9P) | Configuración u-center y firmware u-blox | u-center por el USB POWER+GPS | [ZED-F9P](#firmware-y-configuracion-del-simplertk2b-pro-zed-f9p) |
|
| simpleRTK2B Pro (ZED-F9P) | Configuración u-center y firmware u-blox | u-center por el USB POWER+GPS | [ZED-F9P](#firmware-y-configuracion-del-simplertk2b-pro-zed-f9p) |
|
||||||
|
|
||||||
Cada carga se registra en el [acta de reflasheo](../formularios/reflasheo.html) y queda en el [expediente](../unidad.html) con las capturas de antes y después.
|
Cada carga se registra en el [acta de reflasheo](../formularios/reflasheo.html) y queda en el [expediente](../tableros/unidad.md) con las capturas de antes y después.
|
||||||
|
|
||||||
## Verifica el SHA-256
|
## Verifica el SHA-256
|
||||||
|
|
||||||
|
|||||||
@ -152,7 +152,7 @@ Elige lo que observaste, no lo que supones. "No está claro" es una respuesta v
|
|||||||
|
|
||||||
## 8. Después de enviar el acta
|
## 8. Después de enviar el acta
|
||||||
|
|
||||||
- Queda en el servidor con tu nombre, hora y fotos, y aparece en el [tablero de campo](tablero-campo.html).
|
- Queda en el servidor con tu nombre, hora y fotos, y aparece en el [tablero de campo](../tableros/campo.md).
|
||||||
- Quien abra el acta de esa unidad verá tu visita antes de empezar.
|
- Quien abra el acta de esa unidad verá tu visita antes de empezar.
|
||||||
- Sin señal, el acta se guarda sola en el teléfono; fotos y envío necesitan datos. Si falla, reintenta desde el mismo formulario.
|
- Sin señal, el acta se guarda sola en el teléfono; fotos y envío necesitan datos. Si falla, reintenta desde el mismo formulario.
|
||||||
- Unidad desmontada: sigue en [diagnóstico en banco](../procedimientos/diagnostico-banco.md). Tarjeta: [manual V3](../manuales/manual-tarjeta-V3.md).
|
- Unidad desmontada: sigue en [diagnóstico en banco](../procedimientos/diagnostico-banco.md). Tarjeta: [manual V3](../manuales/manual-tarjeta-V3.md).
|
||||||
|
|||||||
@ -1,200 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
<!-- Desde el 20-oct-2026 este tablero vive dentro del sitio: docs/tableros/campo.md → /tableros/campo/
|
||||||
|
Este archivo solo redirige (conserva ?serie=, ?id=, ?pista=… y #…) para que las ligas viejas
|
||||||
|
guardadas en pendientes y actas sigan sirviendo. No se edita: los cambios van en tableros/campo.md. -->
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Tablero de campo — diagnósticos HBOX</title>
|
<meta name="robots" content="noindex">
|
||||||
<style>
|
<title>Visitas de campo - Drogheda</title>
|
||||||
:root{
|
<script>location.replace('/tableros/campo/' + location.search + location.hash);</script>
|
||||||
--naranja:#e8590c; --tinta:#1d1c19; --tinta2:#5a564d; --linea:#e6e1d6;
|
<meta http-equiv="refresh" content="0; url=/tableros/campo/">
|
||||||
--papel:#faf8f4; --tarjeta:#fff; --superf:#fcfcfb;
|
<style>body{margin:0;padding:24px;background:#0E1524;color:#E7ECF5;font:16px/1.5 system-ui,sans-serif}a{color:#7FE9D8}</style>
|
||||||
--ok:#2f9e44; --ok-bg:#ebfbee; --falla:#e03131; --falla-bg:#fff5f5;
|
|
||||||
--pend:#adb5bd; --pend-bg:#f4f2ee; --info:#1971c2; --info-bg:#e7f1fb;
|
|
||||||
}
|
|
||||||
@media(prefers-color-scheme:dark){:root:not([data-theme=light]){
|
|
||||||
--papel:#191b1e;--tinta:#e6e1d6;--tinta2:#a09a8d;--linea:#33363b;--tarjeta:#1f2226;--superf:#1b1d20;
|
|
||||||
--ok:#69db7c;--ok-bg:#1b2a1f;--falla:#ff8787;--falla-bg:#2c1d1d;--pend:#5c636a;--pend-bg:#26282c;--info:#74c0fc;--info-bg:#182734;}}
|
|
||||||
*{box-sizing:border-box}
|
|
||||||
body{margin:0;background:var(--papel);color:var(--tinta);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
||||||
.wrap{max-width:1020px;margin:0 auto;padding:18px 16px 70px}
|
|
||||||
header.top{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;
|
|
||||||
background:#2C3C5B;color:#fff;padding:16px;border-radius:12px;margin-bottom:16px}
|
|
||||||
header.top h1{margin:0;font-size:20px}
|
|
||||||
header.top .meta{color:#d7d2c8;font-size:13px}
|
|
||||||
a.volver{color:#fff;font-size:13px;text-decoration:none;opacity:.85}
|
|
||||||
button{background:var(--naranja);color:#fff;border:0;border-radius:9px;padding:9px 16px;font-size:14px;font-weight:600;cursor:pointer}
|
|
||||||
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:18px}
|
|
||||||
.kpi{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:14px 16px}
|
|
||||||
.kpi .n{font-size:30px;font-weight:700;line-height:1}
|
|
||||||
.kpi .l{color:var(--tinta2);font-size:13px;margin-top:4px}
|
|
||||||
section.bloque{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:16px;margin:14px 0}
|
|
||||||
section.bloque h2{margin:0 0 2px;font-size:17px}
|
|
||||||
.resumen{color:var(--tinta2);font-size:13.5px;margin:0 0 12px}
|
|
||||||
.unidad{border:1px solid var(--linea);border-radius:11px;padding:12px 14px;margin:10px 0;background:var(--superf)}
|
|
||||||
.unidad .cab{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline;justify-content:space-between}
|
|
||||||
.unidad .s{font-weight:700;font-size:15px}
|
|
||||||
.unidad .m{color:var(--tinta2);font-size:13px}
|
|
||||||
.pill{font-size:11.5px;font-weight:700;padding:3px 10px;border-radius:99px;white-space:nowrap}
|
|
||||||
.pill.ok{background:var(--ok-bg);color:var(--ok)}
|
|
||||||
.pill.falla{background:var(--falla-bg);color:var(--falla)}
|
|
||||||
.pill.pend{background:var(--pend-bg);color:var(--tinta2)}
|
|
||||||
.pill.info{background:var(--info-bg);color:var(--info)}
|
|
||||||
.visita{border-top:1px solid var(--linea);margin-top:10px;padding-top:10px;font-size:13.5px}
|
|
||||||
.visita .f{color:var(--tinta2);font-size:12.5px}
|
|
||||||
.visita .cl{font-weight:700}
|
|
||||||
.fotos{display:flex;flex-wrap:wrap;gap:7px;margin-top:8px}
|
|
||||||
.logs{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
|
|
||||||
.logs a{font-size:12.5px;font-weight:600;color:var(--info);text-decoration:none;
|
|
||||||
border:1px solid var(--linea);border-radius:7px;padding:4px 9px;background:var(--superf)}
|
|
||||||
.fotos a{display:block;width:74px;height:74px;border-radius:8px;overflow:hidden;border:1px solid var(--linea)}
|
|
||||||
.fotos img{width:100%;height:100%;object-fit:cover;display:block;background:var(--pend-bg)}
|
|
||||||
details{margin-top:8px}
|
|
||||||
summary{cursor:pointer;color:var(--naranja);font-size:13px;font-weight:600}
|
|
||||||
table.det{border-collapse:collapse;width:100%;font-size:13px;margin-top:8px}
|
|
||||||
table.det td{padding:5px 8px 5px 0;border-bottom:1px solid var(--linea);vertical-align:top}
|
|
||||||
table.det td.k{color:var(--tinta2);width:45%}
|
|
||||||
.vacio{color:var(--tinta2);font-size:13.5px;font-style:italic}
|
|
||||||
.err{background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla);padding:10px 14px;border-radius:10px}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<p>Este tablero ahora está dentro del sitio: <a href="/tableros/campo/">Visitas de campo</a>.</p>
|
||||||
<header class="top">
|
|
||||||
<div><h1>Tablero de campo — diagnósticos HBOX</h1><div class="meta" id="meta">Cargando…</div></div>
|
|
||||||
<div style="display:flex;gap:12px;align-items:center">
|
|
||||||
<a class="volver" href="../">← Documentación</a>
|
|
||||||
<a class="volver" href="../formularios/diagnostico.html">Nueva acta de diagnóstico</a>
|
|
||||||
<button id="refrescar">Actualizar</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div id="cont"></div>
|
|
||||||
</div>
|
|
||||||
<script src="../formularios/flota.js"></script>
|
|
||||||
<script>
|
|
||||||
const TIPOS = ['diagnostico-campo','diagnostico-banco'];
|
|
||||||
const TIPO = 'diagnostico-campo';
|
|
||||||
const cont = document.getElementById('cont');
|
|
||||||
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=>e.append(c)); return e;};
|
|
||||||
const fecha = s => (s||'').replace('T',' ').slice(0,16);
|
|
||||||
|
|
||||||
function pill(v){
|
|
||||||
const t = String(v.accion||'');
|
|
||||||
if (/^Instalada y reportando/.test(t)) return ['ok','Operando'];
|
|
||||||
if (/^Instalada/.test(t)) return ['falla','Instalada sin resolver'];
|
|
||||||
if (/Querétaro/.test(t)) return ['info','En camino a QRO'];
|
|
||||||
if (/taller/.test(t)) return ['info','En taller'];
|
|
||||||
if (/Reemplazada/.test(t)) return ['ok','Reemplazada'];
|
|
||||||
return ['pend', v.resultado || '—'];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function detalle(tipo, serie, archivo, caja){
|
|
||||||
caja.textContent = 'Cargando…';
|
|
||||||
try{
|
|
||||||
const r = await fetch(`/api/acta?tipo=${encodeURIComponent(tipo)}&serie=${encodeURIComponent(serie)}&archivo=${encodeURIComponent(archivo)}`);
|
|
||||||
const j = await r.json();
|
|
||||||
if(!j.exists) throw new Error('no encontrada');
|
|
||||||
const t = el('table',{class:'det'});
|
|
||||||
const res = j.acta.respuestas || {};
|
|
||||||
Object.keys(res).forEach(k=>{
|
|
||||||
const v = res[k]; if(v.val===''||v.val===false||v.val==null) return;
|
|
||||||
t.append(el('tr',null, el('td',{class:'k',text:(v.lbl||k).replace(/<[^>]+>/g,'')}),
|
|
||||||
el('td',{text: v.val===true ? '✓' : String(v.val)})));
|
|
||||||
});
|
|
||||||
caja.innerHTML=''; caja.append(t);
|
|
||||||
}catch(e){ caja.textContent = 'No se pudo cargar el detalle.'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(actas){
|
|
||||||
cont.innerHTML='';
|
|
||||||
const porUnidad = {};
|
|
||||||
actas.filter(a=>TIPOS.includes(a.tipo)).forEach(a=>{ (porUnidad[a.serie]=porUnidad[a.serie]||[]).push(a); });
|
|
||||||
|
|
||||||
const flota = (window.FLOTA||[]).slice();
|
|
||||||
Object.keys(porUnidad).forEach(s=>{ if(!flota.some(u=>u.serie===s)) flota.push({serie:s, etiqueta:s, maquina:'(fuera del registro de flota)', terminal:'Sin clasificar'}); });
|
|
||||||
|
|
||||||
const visitas = actas.filter(a=>TIPOS.includes(a.tipo));
|
|
||||||
const conVisita = Object.keys(porUnidad).length;
|
|
||||||
const operando = flota.filter(u=>{const v=(porUnidad[u.serie]||[])[0]; return v && pill(v)[0]==='ok';}).length;
|
|
||||||
const kp = el('div',{class:'kpis'});
|
|
||||||
[[flota.length,'Unidades en el registro'],[visitas.length,'Diagnósticos recibidos'],
|
|
||||||
[conVisita,'Unidades ya diagnosticadas'],[operando,'Reportadas como operando']]
|
|
||||||
.forEach(([n,l])=>kp.append(el('div',{class:'kpi'}, el('div',{class:'n',text:String(n)}), el('div',{class:'l',text:l}))));
|
|
||||||
cont.append(kp);
|
|
||||||
|
|
||||||
const grupos = {};
|
|
||||||
flota.forEach(u=>{ (grupos[u.terminal||'Sin clasificar']=grupos[u.terminal||'Sin clasificar']||[]).push(u); });
|
|
||||||
|
|
||||||
Object.keys(grupos).sort().forEach(g=>{
|
|
||||||
const b = el('section',{class:'bloque'});
|
|
||||||
b.append(el('h2',{text:g}));
|
|
||||||
b.append(el('p',{class:'resumen',text:grupos[g].length+' unidad(es) registradas en esta terminal.'}));
|
|
||||||
grupos[g].forEach(u=>{
|
|
||||||
const vs = (porUnidad[u.serie]||[]);
|
|
||||||
const box = el('div',{class:'unidad'});
|
|
||||||
const cab = el('div',{class:'cab'});
|
|
||||||
cab.append(el('div',null,
|
|
||||||
el('div',{class:'s'}, el('a',{href:'../unidad.html?serie='+encodeURIComponent(u.serie),
|
|
||||||
style:'color:inherit;text-decoration:none;border-bottom:1px dotted currentColor',
|
|
||||||
title:'Ver el expediente completo',text:u.serie})),
|
|
||||||
el('div',{class:'m',text:(u.maquina||'')+(u.nota?' · '+u.nota:'')})));
|
|
||||||
const [cls,txt] = vs.length ? pill(vs[0]) : ['pend','Sin diagnóstico'];
|
|
||||||
if(DANOS[u.serie]) cab.append(el('span',{class:'pill falla',text:DANOS[u.serie].marca==='BAJA'?'⛔ BAJA':'⚠ DAÑADA'}));
|
|
||||||
cab.append(el('span',{class:'pill '+cls,text:txt}));
|
|
||||||
box.append(cab);
|
|
||||||
|
|
||||||
if(!vs.length) box.append(el('div',{class:'visita vacio',text:'Todavía no hay ningún diagnóstico registrado para esta unidad.'}));
|
|
||||||
vs.forEach(v=>{
|
|
||||||
const d = el('div',{class:'visita'});
|
|
||||||
d.append(el('div',{class:'f',text:(v.tipo==='diagnostico-banco'?'🔬 banco · ':'🚜 campo · ')+fecha(v.recibido)+' · '+(v.tecnico||'—')+(v.motivo?' · '+v.motivo:'')}));
|
|
||||||
if(v.clasificacion) d.append(el('div',{class:'cl',text:v.clasificacion}));
|
|
||||||
if(v.accion) d.append(el('div',{text:'→ '+v.accion}));
|
|
||||||
if(v.fotos && v.fotos.length){
|
|
||||||
const f = el('div',{class:'fotos'});
|
|
||||||
v.fotos.forEach(r=>{
|
|
||||||
const a = el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(r),target:'_blank'});
|
|
||||||
a.append(el('img',{src:'/api/adjunto?ruta='+encodeURIComponent(r),loading:'lazy'}));
|
|
||||||
f.append(a);
|
|
||||||
});
|
|
||||||
d.append(f);
|
|
||||||
}
|
|
||||||
if(v.archivos && v.archivos.length){
|
|
||||||
const g = el('div',{class:'logs'});
|
|
||||||
v.archivos.forEach(a=>{
|
|
||||||
const x = el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(a.ruta),target:'_blank',
|
|
||||||
text:'📄 '+(a.nombre||'captura')});
|
|
||||||
g.append(x);
|
|
||||||
});
|
|
||||||
d.append(g);
|
|
||||||
}
|
|
||||||
const det = el('details');
|
|
||||||
det.append(el('summary',{text:'Ver todas las respuestas'}));
|
|
||||||
const caja = el('div'); det.append(caja);
|
|
||||||
det.addEventListener('toggle',()=>{ if(det.open && !caja.dataset.listo){ caja.dataset.listo='1'; detalle(v.tipo, v.serie, v.archivo_n, caja); } });
|
|
||||||
d.append(det);
|
|
||||||
box.append(d);
|
|
||||||
});
|
|
||||||
b.append(box);
|
|
||||||
});
|
|
||||||
cont.append(b);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let DANOS = {};
|
|
||||||
async function cargar(){
|
|
||||||
document.getElementById('meta').textContent = 'Cargando…';
|
|
||||||
try{
|
|
||||||
try{ const d=await (await fetch('/api/danos',{cache:'no-store'})).json(); DANOS={}; (d.series||[]).forEach(x=>DANOS[x.serie]=x); }catch(e){}
|
|
||||||
const r = await fetch('/api/actas',{cache:'no-store'});
|
|
||||||
const j = await r.json();
|
|
||||||
render(j.actas||[]);
|
|
||||||
document.getElementById('meta').textContent = 'Actualizado ' + new Date().toLocaleString('es-MX',{dateStyle:'short',timeStyle:'short'});
|
|
||||||
}catch(e){
|
|
||||||
cont.innerHTML=''; cont.append(el('div',{class:'err',text:'No se pudo leer el servidor de actas: '+e.message}));
|
|
||||||
document.getElementById('meta').textContent = 'Error';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.getElementById('refrescar').addEventListener('click',cargar);
|
|
||||||
cargar();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
93
docs/fonts/OFL-Atkinson-Hyperlegible-Mono.txt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020-2024 The Atkinson Hyperlegible Mono Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next-mono) AtkinsonHyperlegibleMono-Italic[wght].ttf: Copyright 2020-2024 The Atkinson Hyperlegible Mono Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next-mono)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
docs/fonts/OFL-Atkinson-Hyperlegible-Next.txt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next) AtkinsonHyperlegibleNext-Italic[wght].ttf: Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors (https://github.com/googlefonts/atkinson-hyperlegible-next)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
93
docs/fonts/OFL-League-Spartan.txt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The League Spartan Project Authors (https://github.com/theleagueof/league-spartan)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
BIN
docs/fonts/atkinson-hyperlegible-mono-latin.woff2
Normal file
BIN
docs/fonts/atkinson-hyperlegible-next-latin.woff2
Normal file
BIN
docs/fonts/league-spartan-latin.woff2
Normal file
@ -1,75 +1,112 @@
|
|||||||
/* Estilo compartido de las actas HBOX — pensado para tablet/laptop en el banco */
|
/* Estilo compartido de las actas HBOX — pensado para tablet/laptop en el banco y en el patio.
|
||||||
:root{
|
Colores y letras: stylesheets/helgen.css (oscuro por omisión; claro si la persona lo eligió:
|
||||||
--naranja:#e8590c; --naranja-suave:#fff1e8; --tinta:#1d1c19; --tinta2:#5a564d;
|
js/tema.js pone <html data-tema="claro"> y agrega el botón de modo al encabezado). */
|
||||||
--linea:#e2ddd2; --papel:#faf8f4; --tarjeta:#ffffff; --ok:#2f9e44; --falla:#e03131;
|
@import url("../stylesheets/helgen.css");
|
||||||
--ok-bg:#ebfbee; --falla-bg:#fff5f5; --aviso:#f08c00; --aviso-bg:#fff9db;
|
|
||||||
|
:root {
|
||||||
|
/* nombres de siempre, ahora sobre los tokens de Helgen (los usan acta.js y las páginas) */
|
||||||
|
--naranja: var(--hg-liga);
|
||||||
|
--naranja-suave: var(--hg-acento-suave);
|
||||||
|
--tinta: var(--hg-texto);
|
||||||
|
--tinta2: var(--hg-texto-2);
|
||||||
|
--linea: var(--hg-linea);
|
||||||
|
--papel: var(--hg-fondo);
|
||||||
|
--tarjeta: var(--hg-plano);
|
||||||
|
--ok: var(--hg-hecho);
|
||||||
|
--falla: var(--hg-bloqueado);
|
||||||
|
--ok-bg: var(--hg-hecho-bg);
|
||||||
|
--falla-bg: var(--hg-bloqueado-bg);
|
||||||
|
--aviso: var(--hg-aviso);
|
||||||
|
--aviso-bg: var(--hg-aviso-bg);
|
||||||
|
--plane: var(--hg-liga);
|
||||||
|
--crit: var(--hg-bloqueado);
|
||||||
|
--crit-soft: var(--hg-bloqueado-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
body{margin:0;background:var(--papel);color:var(--tinta);
|
body{margin:0;background:var(--hg-fondo);color:var(--hg-texto);
|
||||||
font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-text-size-adjust:100%}
|
font:16px/1.5 var(--hg-fuente);-webkit-text-size-adjust:100%;
|
||||||
|
-webkit-font-smoothing:antialiased}
|
||||||
|
a{color:var(--hg-liga)}
|
||||||
|
:focus-visible{outline:2px solid var(--hg-foco);outline-offset:2px}
|
||||||
|
code{font-family:var(--hg-fuente-mono);font-size:.9em;background:var(--hg-elevado);border-radius:4px;padding:0 .3em}
|
||||||
.wrap{max-width:820px;margin:0 auto;padding:16px 16px 120px}
|
.wrap{max-width:820px;margin:0 auto;padding:16px 16px 120px}
|
||||||
header.top{background:#2C3C5B;color:#fff;padding:16px;border-radius:12px;margin-bottom:16px}
|
header.top{position:relative;background:var(--hg-barra);color:var(--hg-texto);border:1px solid var(--hg-linea);
|
||||||
header.top h1{margin:0;font-size:20px}
|
padding:16px 58px 16px 16px;border-radius:12px;margin-bottom:16px}
|
||||||
header.top p{margin:4px 0 0;color:#d7d2c8;font-size:14px}
|
html[data-tema="claro"] header.top{background:var(--hg-indigo);border-color:var(--hg-indigo);color:#fff}
|
||||||
.borrador{background:var(--aviso-bg);border:1px solid var(--aviso);color:#a5620a;
|
header.top h1{margin:0;font-family:var(--hg-fuente-titulo);font-weight:600;font-size:23px;line-height:1.15;letter-spacing:0}
|
||||||
|
header.top p{margin:6px 0 0;color:var(--hg-texto-2);font-size:14px}
|
||||||
|
html[data-tema="claro"] header.top p{color:rgba(255,255,255,.78)}
|
||||||
|
/* botón de modo claro/oscuro (js/tema.js) */
|
||||||
|
.hg-modo{position:absolute;top:12px;right:12px;width:34px;height:34px;display:grid;place-items:center;
|
||||||
|
border-radius:8px;border:1px solid var(--hg-linea-2);background:transparent;color:var(--hg-texto-2);cursor:pointer;padding:0}
|
||||||
|
.hg-modo:hover{color:var(--hg-texto-fuerte);background:var(--hg-elevado)}
|
||||||
|
html[data-tema="claro"] .hg-modo{border-color:rgba(255,255,255,.35);color:rgba(255,255,255,.85)}
|
||||||
|
html[data-tema="claro"] .hg-modo:hover{background:rgba(255,255,255,.12);color:#fff}
|
||||||
|
|
||||||
|
.borrador{background:var(--aviso-bg);border:1px solid color-mix(in srgb,var(--aviso) 45%,transparent);color:var(--aviso);
|
||||||
padding:10px 14px;border-radius:10px;margin-bottom:16px;font-size:14px}
|
padding:10px 14px;border-radius:10px;margin-bottom:16px;font-size:14px}
|
||||||
.datos{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:8px}
|
.datos{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:8px}
|
||||||
@media(max-width:560px){.datos{grid-template-columns:1fr}}
|
@media(max-width:560px){.datos{grid-template-columns:1fr}}
|
||||||
label{display:block;font-weight:600;font-size:14px;margin-bottom:4px}
|
label{display:block;font-weight:600;font-size:14px;margin-bottom:5px;color:var(--hg-texto-2)}
|
||||||
input[type=text],input[type=number],select,textarea{
|
input[type=text],input[type=number],input[type=password],input[type=email],input[type=tel],select,textarea{
|
||||||
width:100%;padding:11px 12px;border:1px solid var(--linea);border-radius:9px;
|
width:100%;padding:11px 12px;border:1px solid var(--hg-linea-2);border-radius:9px;
|
||||||
font-size:16px;background:#fff;color:var(--tinta)}
|
font:inherit;font-size:16px;background:var(--hg-plano);color:var(--hg-texto)}
|
||||||
input:focus,select:focus,textarea:focus{outline:2px solid var(--naranja);border-color:var(--naranja)}
|
input::placeholder,textarea::placeholder{color:var(--hg-texto-3)}
|
||||||
section.paso{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;
|
input:focus,select:focus,textarea:focus{outline:2px solid var(--hg-foco);outline-offset:1px;border-color:transparent}
|
||||||
|
section.paso{background:var(--hg-plano-2);border:1px solid var(--hg-linea);border-radius:12px;
|
||||||
padding:14px 16px;margin:14px 0}
|
padding:14px 16px;margin:14px 0}
|
||||||
section.paso h2{margin:0 0 4px;font-size:17px;color:var(--naranja)}
|
section.paso h2{margin:0 0 4px;font-family:var(--hg-fuente-titulo);font-weight:600;font-size:19px;line-height:1.2;color:var(--hg-texto-fuerte)}
|
||||||
section.paso .sub{color:var(--tinta2);font-size:13.5px;margin:0 0 10px}
|
section.paso .sub{color:var(--hg-texto-2);font-size:14px;margin:0 0 10px}
|
||||||
.item{padding:10px 0;border-top:1px solid var(--linea)}
|
.item{padding:10px 0;border-top:1px solid var(--hg-linea)}
|
||||||
.item:first-of-type{border-top:0}
|
.item:first-of-type{border-top:0}
|
||||||
.chk{display:flex;gap:12px;align-items:flex-start;cursor:pointer}
|
.chk{display:flex;gap:12px;align-items:flex-start;cursor:pointer}
|
||||||
.chk input{width:26px;height:26px;margin:1px 0 0;flex:0 0 auto;accent-color:var(--naranja)}
|
.chk input{width:26px;height:26px;margin:1px 0 0;flex:0 0 auto;accent-color:var(--hg-teal)}
|
||||||
.chk .txt{flex:1}
|
.chk .txt{flex:1}
|
||||||
.medida{display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center}
|
.medida{display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center}
|
||||||
.medida .txt{flex:1 1 100%;font-weight:600}
|
.medida .txt{flex:1 1 100%;font-weight:600}
|
||||||
.medida .esp{flex:1 1 100%;color:var(--tinta2);font-size:13px;margin-top:-2px}
|
.medida .esp{flex:1 1 100%;color:var(--hg-texto-2);font-size:13px;margin-top:-2px}
|
||||||
.medida input{max-width:150px}
|
.medida input{max-width:150px}
|
||||||
.medida .u{color:var(--tinta2)}
|
.medida .u{color:var(--hg-texto-2)}
|
||||||
.nota-item{color:var(--tinta2);font-size:14px}
|
.nota-item{color:var(--hg-texto-2);font-size:14px}
|
||||||
.aviso{background:var(--aviso-bg);border-left:3px solid var(--aviso);padding:8px 12px;
|
.aviso{background:var(--aviso-bg);border:1px solid color-mix(in srgb,var(--aviso) 35%,transparent);padding:8px 12px;
|
||||||
border-radius:0 8px 8px 0;font-size:13.5px;margin:6px 0}
|
border-radius:8px;font-size:13.5px;margin:6px 0;color:var(--hg-texto)}
|
||||||
.firma{background:var(--naranja-suave);border:1px solid #ffd8a8}
|
.firma{background:var(--hg-plano)}
|
||||||
.resultado{display:flex;gap:10px;margin-top:6px}
|
.resultado{display:flex;gap:10px;margin-top:6px}
|
||||||
.resultado label{flex:1;border:2px solid var(--linea);border-radius:10px;padding:12px;
|
.resultado label{flex:1;border:2px solid var(--hg-linea-2);border-radius:10px;padding:12px;
|
||||||
text-align:center;cursor:pointer;font-weight:700;margin:0}
|
text-align:center;cursor:pointer;font-weight:700;margin:0;color:var(--hg-texto)}
|
||||||
.resultado input{position:absolute;opacity:0}
|
.resultado input{position:absolute;opacity:0}
|
||||||
.resultado label:has(input:checked).ok{border-color:var(--ok);background:var(--ok-bg);color:var(--ok)}
|
.resultado label:has(input:checked).ok{border-color:var(--ok);background:var(--ok-bg);color:var(--ok)}
|
||||||
.resultado label:has(input:checked).falla{border-color:var(--falla);background:var(--falla-bg);color:var(--falla)}
|
.resultado label:has(input:checked).falla{border-color:var(--falla);background:var(--falla-bg);color:var(--falla)}
|
||||||
.barra{position:fixed;left:0;right:0;bottom:0;background:#fff;border-top:1px solid var(--linea);
|
.resultado label:has(input:focus-visible){outline:2px solid var(--hg-foco);outline-offset:2px}
|
||||||
|
.barra{position:fixed;left:0;right:0;bottom:0;background:var(--hg-barra);border-top:1px solid var(--hg-linea);
|
||||||
padding:12px 16px;display:flex;gap:12px;align-items:center;justify-content:space-between}
|
padding:12px 16px;display:flex;gap:12px;align-items:center;justify-content:space-between}
|
||||||
.barra .estado{font-size:13px;color:var(--tinta2)}
|
.barra .estado{font-size:13px;color:var(--hg-texto-2)}
|
||||||
button.enviar{background:var(--naranja);color:#fff;border:0;border-radius:10px;
|
button.enviar{background:var(--hg-acento);color:var(--hg-sobre-acento);border:0;border-radius:10px;
|
||||||
padding:13px 22px;font-size:16px;font-weight:700;cursor:pointer}
|
padding:13px 22px;font:inherit;font-size:16px;font-weight:700;cursor:pointer}
|
||||||
|
button.enviar:hover:not(:disabled){filter:brightness(1.07)}
|
||||||
button.enviar:disabled{opacity:.5;cursor:not-allowed}
|
button.enviar:disabled{opacity:.5;cursor:not-allowed}
|
||||||
.msg{padding:12px 14px;border-radius:10px;margin-top:12px;font-weight:600;display:none}
|
.msg{padding:12px 14px;border-radius:10px;margin-top:12px;font-weight:600;display:none}
|
||||||
.msg.ok{display:block;background:var(--ok-bg);color:var(--ok);border:1px solid var(--ok)}
|
.msg.ok{display:block;background:var(--ok-bg);color:var(--ok);border:1px solid color-mix(in srgb,var(--ok) 40%,transparent)}
|
||||||
.msg.err{display:block;background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla)}
|
.msg.err{display:block;background:var(--falla-bg);color:var(--falla);border:1px solid color-mix(in srgb,var(--falla) 45%,transparent)}
|
||||||
.enviada{opacity:.6;pointer-events:none}
|
.enviada{opacity:.6;pointer-events:none}
|
||||||
a.volver{color:var(--naranja);font-size:14px;text-decoration:none}
|
a.volver{color:var(--hg-liga);font-size:14px;text-decoration:none}
|
||||||
button.limpiar{background:#fff;color:var(--tinta2);border:1px solid var(--linea);
|
a.volver:hover{text-decoration:underline}
|
||||||
border-radius:10px;padding:13px 16px;font-size:15px;cursor:pointer}
|
button.limpiar{background:transparent;color:var(--hg-texto-2);border:1px solid var(--hg-linea-2);
|
||||||
button.limpiar:hover{border-color:var(--naranja);color:var(--naranja)}
|
border-radius:10px;padding:13px 16px;font:inherit;font-size:15px;cursor:pointer}
|
||||||
|
button.limpiar:hover{border-color:var(--hg-linea-2);background:var(--hg-elevado);color:var(--hg-texto-fuerte)}
|
||||||
|
|
||||||
/* ---- actas compuestas (ensamble / integración): estado de cada módulo ---- */
|
/* ---- actas compuestas (ensamble / integración): estado de cada módulo ---- */
|
||||||
.datos{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
|
.datos{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
|
||||||
.datos .serie-calc{grid-column:1 / -1;background:var(--naranja-suave);border:1px solid #ffd8a8;border-radius:10px;padding:10px 12px}
|
.datos .serie-calc{grid-column:1 / -1;background:var(--hg-plano);border:1px solid var(--hg-linea);border-radius:10px;padding:10px 12px}
|
||||||
.datos .serie-calc input{font-weight:700;font-size:20px;letter-spacing:.04em;background:#fff}
|
.datos .serie-calc input{font-family:var(--hg-fuente-mono);font-weight:600;font-size:20px;letter-spacing:.04em;background:var(--hg-plano-2)}
|
||||||
.badge{display:inline-block;margin-top:6px;font-size:13px;font-weight:700;padding:3px 10px;border-radius:99px;background:#f1efe9;color:var(--tinta2)}
|
.badge{display:inline-block;margin-top:6px;font-size:13px;font-weight:600;padding:3px 10px;border-radius:6px;background:var(--hg-pend-bg);color:var(--hg-texto-2)}
|
||||||
.badge.ok{background:var(--ok-bg);color:var(--ok)}
|
.badge.ok{background:var(--ok-bg);color:var(--ok)}
|
||||||
.badge.falla{background:var(--falla-bg);color:var(--falla)}
|
.badge.falla{background:var(--falla-bg);color:var(--falla)}
|
||||||
.badge.pend{background:#f1efe9;color:var(--tinta2)}
|
.badge.pend{background:var(--hg-pend-bg);color:var(--hg-texto-2)}
|
||||||
.radios{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:4px}
|
.radios{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:4px}
|
||||||
.radios .rad{font-weight:500;display:flex;align-items:center;gap:6px;cursor:pointer}
|
.radios .rad{font-weight:500;display:flex;align-items:center;gap:6px;cursor:pointer;color:var(--hg-texto)}
|
||||||
.radios input{width:22px;height:22px;accent-color:var(--naranja)}
|
.radios input{width:22px;height:22px;accent-color:var(--hg-teal)}
|
||||||
|
|
||||||
/* ================= actas de campo (diagnóstico / instalación) ================= */
|
/* ================= actas de campo (diagnóstico / instalación) ================= */
|
||||||
|
|
||||||
@ -77,41 +114,42 @@ button.limpiar:hover{border-color:var(--naranja);color:var(--naranja)}
|
|||||||
.oculto{display:none !important}
|
.oculto{display:none !important}
|
||||||
|
|
||||||
/* la unidad elegida: máquina y terminal, para que el técnico confirme que abrió la correcta */
|
/* la unidad elegida: máquina y terminal, para que el técnico confirme que abrió la correcta */
|
||||||
.ficha-unidad:not(:empty){margin-top:8px;padding:8px 10px;background:var(--naranja-suave);
|
.ficha-unidad:not(:empty){margin-top:8px;padding:8px 10px;background:var(--hg-plano);
|
||||||
border:1px solid #ffd8a8;border-radius:8px;font-size:13.5px;line-height:1.35}
|
border:1px solid var(--hg-linea);border-radius:8px;font-size:13.5px;line-height:1.35}
|
||||||
|
|
||||||
/* historial de visitas anteriores a la misma unidad */
|
/* historial de visitas anteriores a la misma unidad */
|
||||||
.hist{margin:0 0 14px;padding:12px 14px;background:#eef3f8;border:1px solid #c9dcec;
|
.hist{margin:0 0 14px;padding:12px 14px;background:var(--hg-info-bg);border:1px solid color-mix(in srgb,var(--hg-info) 30%,transparent);
|
||||||
border-radius:10px;font-size:14px;color:#1b3b57}
|
border-radius:10px;font-size:14px;color:var(--hg-texto)}
|
||||||
.hist:empty{display:none}
|
.hist:empty{display:none}
|
||||||
.hist-t{margin-bottom:6px}
|
.hist-t{margin-bottom:6px}
|
||||||
.hist-r{padding:4px 0;border-top:1px solid #d6e4f0}
|
.hist-r{padding:4px 0;border-top:1px solid color-mix(in srgb,var(--hg-info) 22%,transparent)}
|
||||||
|
|
||||||
/* fotos */
|
/* fotos */
|
||||||
.item.foto .txt{display:block;font-weight:600;margin-bottom:8px}
|
.item.foto .txt{display:block;font-weight:600;margin-bottom:8px}
|
||||||
button.btn-foto{width:100%;background:#fff;color:var(--naranja);border:2px dashed var(--naranja);
|
button.btn-foto{width:100%;background:transparent;color:var(--hg-liga);border:2px dashed var(--hg-linea-2);
|
||||||
border-radius:12px;padding:16px;font-size:16px;font-weight:700;cursor:pointer}
|
border-radius:12px;padding:16px;font:inherit;font-size:16px;font-weight:700;cursor:pointer}
|
||||||
button.btn-foto:active{background:var(--naranja-suave)}
|
button.btn-foto:hover{border-color:var(--hg-liga)}
|
||||||
|
button.btn-foto:active{background:var(--hg-acento-suave)}
|
||||||
.miniaturas{display:flex;flex-wrap:wrap;gap:10px;margin-top:10px}
|
.miniaturas{display:flex;flex-wrap:wrap;gap:10px;margin-top:10px}
|
||||||
.miniaturas:empty{margin-top:0}
|
.miniaturas:empty{margin-top:0}
|
||||||
.mini{width:104px;border:1px solid var(--linea);border-radius:10px;padding:6px;background:#fff;
|
.mini{width:104px;border:1px solid var(--hg-linea);border-radius:10px;padding:6px;background:var(--hg-plano);
|
||||||
display:flex;flex-direction:column;gap:4px;align-items:center}
|
display:flex;flex-direction:column;gap:4px;align-items:center}
|
||||||
.mini img{width:90px;height:90px;object-fit:cover;border-radius:6px;background:#eee}
|
.mini img{width:90px;height:90px;object-fit:cover;border-radius:6px;background:var(--hg-pend-bg)}
|
||||||
.mini .est{font-size:11px;font-weight:700;text-align:center;line-height:1.2}
|
.mini .est{font-size:11px;font-weight:700;text-align:center;line-height:1.2}
|
||||||
.mini.ok{border-color:var(--ok)} .mini.ok .est{color:var(--ok)}
|
.mini.ok{border-color:color-mix(in srgb,var(--ok) 55%,transparent)} .mini.ok .est{color:var(--ok)}
|
||||||
.mini.err{border-color:var(--falla)} .mini.err .est{color:var(--falla)}
|
.mini.err{border-color:color-mix(in srgb,var(--falla) 55%,transparent)} .mini.err .est{color:var(--falla)}
|
||||||
.mini.subiendo .est{color:var(--tinta2)}
|
.mini.subiendo .est{color:var(--hg-texto-2)}
|
||||||
button.mini-btn{background:#fff;border:1px solid var(--linea);border-radius:6px;
|
button.mini-btn{background:transparent;border:1px solid var(--hg-linea-2);border-radius:6px;
|
||||||
font-size:11px;padding:3px 7px;cursor:pointer;color:var(--tinta2)}
|
font:inherit;font-size:11px;padding:3px 7px;cursor:pointer;color:var(--hg-texto-2)}
|
||||||
|
|
||||||
/* aviso de seguridad: más fuerte que un 'aviso' normal */
|
/* aviso de seguridad: más fuerte que un 'aviso' normal */
|
||||||
.peligro{background:var(--falla-bg);border:2px solid var(--falla);color:#a01a1a;
|
.peligro{background:var(--falla-bg);border:2px solid var(--falla);color:var(--hg-texto);
|
||||||
padding:12px 14px;border-radius:10px;margin:10px 0;font-size:14px;line-height:1.45}
|
padding:12px 14px;border-radius:10px;margin:10px 0;font-size:14px;line-height:1.45}
|
||||||
.peligro b{color:var(--falla)}
|
.peligro b{color:var(--falla)}
|
||||||
|
|
||||||
/* en el patio: con guantes, sol y prisa — todo más grande */
|
/* en el patio: con guantes, sol y prisa — todo más grande */
|
||||||
@media(max-width:560px){
|
@media(max-width:560px){
|
||||||
body{font:17px/1.55 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
body{font-size:17px;line-height:1.55}
|
||||||
.wrap{padding:12px 12px 130px}
|
.wrap{padding:12px 12px 130px}
|
||||||
.chk input{width:30px;height:30px}
|
.chk input{width:30px;height:30px}
|
||||||
.medida input{max-width:none;flex:1 1 140px;font-size:17px;padding:13px 12px}
|
.medida input{max-width:none;flex:1 1 140px;font-size:17px;padding:13px 12px}
|
||||||
@ -124,24 +162,32 @@ button.mini-btn{background:#fff;border:1px solid var(--linea);border-radius:6px;
|
|||||||
/* archivos de log adjuntos (capturas por USB) */
|
/* archivos de log adjuntos (capturas por USB) */
|
||||||
.mini.doc{width:auto;min-width:160px;max-width:260px;flex-direction:row;flex-wrap:wrap;
|
.mini.doc{width:auto;min-width:160px;max-width:260px;flex-direction:row;flex-wrap:wrap;
|
||||||
align-items:center;gap:6px;padding:8px 10px}
|
align-items:center;gap:6px;padding:8px 10px}
|
||||||
.mini.doc .ico{font-size:18px}
|
.mini.doc .ico{font-size:12px;font-weight:700;color:var(--hg-texto-3)}
|
||||||
.mini.doc .nom{font-size:12.5px;font-weight:600;word-break:break-all;flex:1 1 90px}
|
.mini.doc .nom{font-family:var(--hg-fuente-mono);font-size:12.5px;font-weight:500;word-break:break-all;flex:1 1 90px}
|
||||||
.mini.doc .est{font-size:11.5px;flex:0 0 auto}
|
.mini.doc .est{font-size:11.5px;flex:0 0 auto}
|
||||||
input[type=date]{width:100%;padding:11px 12px;border:1px solid var(--linea);border-radius:9px;
|
input[type=date]{width:100%;padding:11px 12px;border:1px solid var(--hg-linea-2);border-radius:9px;
|
||||||
font-size:16px;background:#fff;color:var(--tinta)}
|
font:inherit;font-size:16px;background:var(--hg-plano);color:var(--hg-texto)}
|
||||||
|
|
||||||
/* marca de daño / baja: se muestra en cualquier acta al elegir la serie */
|
/* marca de daño / baja: se muestra en cualquier acta al elegir la serie */
|
||||||
.marca{display:none;margin:0 0 14px;padding:12px 14px;border-radius:10px;font-size:14.5px;line-height:1.45}
|
.marca{display:none;margin:0 0 14px;padding:12px 14px;border-radius:10px;font-size:14.5px;line-height:1.45}
|
||||||
.marca.danada{display:block;background:var(--aviso-bg);border:2px solid var(--aviso);color:#7a4a00}
|
.marca.danada{display:block;background:var(--aviso-bg);border:2px solid var(--aviso);color:var(--hg-texto)}
|
||||||
.marca.baja{display:block;background:var(--falla-bg);border:2px solid var(--falla);color:#8a1a1a}
|
.marca.baja{display:block;background:var(--falla-bg);border:2px solid var(--falla);color:var(--hg-texto)}
|
||||||
.marca a{color:inherit;font-weight:700}
|
.marca a{color:inherit;font-weight:700}
|
||||||
|
|
||||||
/* tabla de registro (tipo 'tabla') */
|
/* tabla de registro (tipo 'tabla') */
|
||||||
.tabla-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;margin-top:6px}
|
.tabla-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;margin-top:6px}
|
||||||
.tabla{border-collapse:collapse;font-size:13px;min-width:100%}
|
.tabla{border-collapse:collapse;font-size:13px;min-width:100%}
|
||||||
.tabla th,.tabla td{border:1px solid #ddd;padding:4px 5px;text-align:left;vertical-align:middle;white-space:nowrap}
|
.tabla th,.tabla td{border:1px solid var(--hg-linea);padding:4px 5px;text-align:left;vertical-align:middle;white-space:nowrap}
|
||||||
.tabla thead th{background:#f3f3f3;font-weight:600;font-size:12px}
|
.tabla thead th{background:var(--hg-plano);color:var(--hg-texto-2);font-weight:600;font-size:12px}
|
||||||
.tabla tbody th{background:#fafafa;font-weight:600;font-variant-numeric:tabular-nums}
|
.tabla tbody th{background:var(--hg-plano-2);font-weight:600;font-variant-numeric:tabular-nums}
|
||||||
.tabla input,.tabla select{width:100%;min-width:64px;font-size:14px;padding:6px 6px;border:1px solid #ccc;border-radius:6px;box-sizing:border-box}
|
.tabla input,.tabla select{width:100%;min-width:64px;font-size:14px;padding:6px 6px;border:1px solid var(--hg-linea-2);border-radius:6px;box-sizing:border-box;background:var(--hg-plano);color:var(--hg-texto)}
|
||||||
.tabla input[inputmode=decimal]{max-width:90px}
|
.tabla input[inputmode=decimal]{max-width:90px}
|
||||||
.enviada .tabla input,.enviada .tabla select{background:#f6f6f6}
|
.enviada .tabla input,.enviada .tabla select{background:var(--hg-plano-2)}
|
||||||
|
|
||||||
|
@media print{
|
||||||
|
body{background:#fff;color:#000}
|
||||||
|
header.top{background:#fff !important;color:#000 !important;border-color:#000}
|
||||||
|
header.top p{color:#333 !important}
|
||||||
|
.hg-modo,.barra{display:none !important}
|
||||||
|
section.paso{background:#fff;border-color:#999}
|
||||||
|
}
|
||||||
|
|||||||
@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
function opciones(sel, prefijo, desde, hasta, pad) {
|
function opciones(sel, prefijo, desde, hasta, pad) {
|
||||||
sel.append($('option', { value: '', text: '— elegir —' }));
|
sel.append($('option', { value: '', text: '— elegir —' }));
|
||||||
sel.append($('option', { value: prefijo + '-PRUEBA', text: '⚙ PRUEBA (no cuenta)' }));
|
sel.append($('option', { value: prefijo + '-PRUEBA', text: 'PRUEBA (no cuenta)' }));
|
||||||
for (let i = desde; i <= hasta; i++) {
|
for (let i = desde; i <= hasta; i++) {
|
||||||
const s = prefijo + '-' + String(i).padStart(pad, '0');
|
const s = prefijo + '-' + String(i).padStart(pad, '0');
|
||||||
sel.append($('option', { value: s, text: s }));
|
sel.append($('option', { value: s, text: s }));
|
||||||
@ -81,7 +81,7 @@
|
|||||||
wrap.append(nav);
|
wrap.append(nav);
|
||||||
|
|
||||||
if (cfg.borrador)
|
if (cfg.borrador)
|
||||||
wrap.append($('div', { class: 'borrador', html: '⚠ <b>BORRADOR</b> — este formulario aún debe revisarse con ingeniería antes de usarlo como registro oficial.' }));
|
wrap.append($('div', { class: 'borrador', html: '<b>BORRADOR</b> — este formulario aún debe revisarse con ingeniería antes de usarlo como registro oficial.' }));
|
||||||
if (cfg.intro) wrap.append($('div', { class: 'nota-item', style: 'margin:0 0 10px', html: cfg.intro }));
|
if (cfg.intro) wrap.append($('div', { class: 'nota-item', style: 'margin:0 0 10px', html: cfg.intro }));
|
||||||
|
|
||||||
// ---------------- cabecera: serie (simple o compuesta) + técnico ----------------
|
// ---------------- cabecera: serie (simple o compuesta) + técnico ----------------
|
||||||
@ -108,9 +108,9 @@
|
|||||||
serieSel.append(og);
|
serieSel.append(og);
|
||||||
});
|
});
|
||||||
if (!cfg.sinEnsambladas) cargarEnsambladas();
|
if (!cfg.sinEnsambladas) cargarEnsambladas();
|
||||||
if (!cfg.sinEnsambladas) serieSel.insertBefore($('option', { value: '__HBOX__', text: '🧩 Unidad HBOX completa — elegir sus 3 piezas' }), serieSel.children[1] || null);
|
if (!cfg.sinEnsambladas) serieSel.insertBefore($('option', { value: '__HBOX__', text: 'Unidad HBOX completa: elegir sus 3 piezas' }), serieSel.children[1] || null);
|
||||||
serieSel.append($('option', { value: '__OTRA__', text: '➕ Otra unidad (no está en la lista)' }));
|
serieSel.append($('option', { value: '__OTRA__', text: 'Otra unidad (no está en la lista)' }));
|
||||||
serieSel.append($('option', { value: 'CAMPO-PRUEBA', text: '⚙ PRUEBA (no cuenta)' }));
|
serieSel.append($('option', { value: 'CAMPO-PRUEBA', text: 'PRUEBA (no cuenta)' }));
|
||||||
// Unidad HBOX completa armada a mano: tarjeta + ArduSimple + módem → misma serie que el acta de ensamble.
|
// Unidad HBOX completa armada a mano: tarjeta + ArduSimple + módem → misma serie que el acta de ensamble.
|
||||||
[['einar', 'Tarjeta EinarBoard', 'V5', 2], ['ard', 'ArduSimple', 'ARD', 3], ['modem', 'Módem 4G', 'RTK', 4]].forEach(([k, t, pre, pad]) => {
|
[['einar', 'Tarjeta EinarBoard', 'V5', 2], ['ard', 'ArduSimple', 'ARD', 3], ['modem', 'Módem 4G', 'RTK', 4]].forEach(([k, t, pre, pad]) => {
|
||||||
const sel = $('select'); opciones(sel, pre, 1, 30, pad); armarSel[k] = sel;
|
const sel = $('select'); opciones(sel, pre, 1, 30, pad); armarSel[k] = sel;
|
||||||
@ -178,10 +178,10 @@
|
|||||||
marcaBox.className = 'marca ' + (baja ? 'baja' : 'danada');
|
marcaBox.className = 'marca ' + (baja ? 'baja' : 'danada');
|
||||||
marcaBox.innerHTML = avisos.map(({ s, j }) => {
|
marcaBox.innerHTML = avisos.map(({ s, j }) => {
|
||||||
const a = j.acta || {};
|
const a = j.acta || {};
|
||||||
return (j.marca === 'BAJA' ? '⛔ <b>' + s + ' está dada de BAJA</b>' : '⚠ <b>' + s + ' está marcada como DAÑADA</b>')
|
return (j.marca === 'BAJA' ? '<b>' + s + ' está dada de BAJA</b>' : '<b>' + s + ' está marcada como DAÑADA</b>')
|
||||||
+ (a.fecha ? ' desde el ' + a.fecha : '') + (a.clasificacion ? ' — ' + a.clasificacion : '')
|
+ (a.fecha ? ' desde el ' + a.fecha : '') + (a.clasificacion ? ' — ' + a.clasificacion : '')
|
||||||
+ (a.resumen ? '<br>' + a.resumen : '')
|
+ (a.resumen ? '<br>' + a.resumen : '')
|
||||||
+ '<br><a href="../unidad.html?serie=' + encodeURIComponent(s) + '">Ver su expediente</a>'
|
+ '<br><a href="../tableros/unidad/?serie=' + encodeURIComponent(s) + '">Ver su expediente</a>'
|
||||||
+ (j.marca === 'BAJA' ? '. No debe usarse ni instalarse.' : '. Revisa el acta de daño antes de continuar.');
|
+ (j.marca === 'BAJA' ? '. No debe usarse ni instalarse.' : '. Revisa el acta de daño antes de continuar.');
|
||||||
}).join('<hr style="border:0;border-top:1px solid currentColor;opacity:.3;margin:8px 0">');
|
}).join('<hr style="border:0;border-top:1px solid currentColor;opacity:.3;margin:8px 0">');
|
||||||
}
|
}
|
||||||
@ -344,7 +344,7 @@
|
|||||||
box.append($('span', { class: 'txt', html: (it.req ? '<b>Archivo obligatorio</b> — ' : 'Archivo — ') + it.txt }));
|
box.append($('span', { class: 'txt', html: (it.req ? '<b>Archivo obligatorio</b> — ' : 'Archivo — ') + it.txt }));
|
||||||
if (it.ayuda) box.append($('div', { class: 'nota-item', html: it.ayuda }));
|
if (it.ayuda) box.append($('div', { class: 'nota-item', html: it.ayuda }));
|
||||||
const inp = $('input', { type: 'file', accept: '.txt,.log,.csv,.nmea,.ubx,.json,.cfg,text/plain', multiple: 'multiple', style: 'display:none' });
|
const inp = $('input', { type: 'file', accept: '.txt,.log,.csv,.nmea,.ubx,.json,.cfg,text/plain', multiple: 'multiple', style: 'display:none' });
|
||||||
const btn = $('button', { type: 'button', class: 'btn-foto', text: '📄 Subir archivo de captura' });
|
const btn = $('button', { type: 'button', class: 'btn-foto', text: 'Subir archivo de captura' });
|
||||||
const tiras = $('div', { class: 'miniaturas', 'data-fotos': id });
|
const tiras = $('div', { class: 'miniaturas', 'data-fotos': id });
|
||||||
const marca = $('input', { type: 'hidden', 'data-id': id, 'data-lbl': 'Archivos: ' + String(it.txt || '').replace(/<[^>]+>/g, ''), value: '' });
|
const marca = $('input', { type: 'hidden', 'data-id': id, 'data-lbl': 'Archivos: ' + String(it.txt || '').replace(/<[^>]+>/g, ''), value: '' });
|
||||||
btn.addEventListener('click', () => inp.click());
|
btn.addEventListener('click', () => inp.click());
|
||||||
@ -355,7 +355,7 @@
|
|||||||
box.append($('span', { class: 'txt', html: (it.req ? '<b>Foto obligatoria</b> — ' : 'Foto — ') + it.txt }));
|
box.append($('span', { class: 'txt', html: (it.req ? '<b>Foto obligatoria</b> — ' : 'Foto — ') + it.txt }));
|
||||||
if (it.ayuda) box.append($('div', { class: 'nota-item', html: it.ayuda }));
|
if (it.ayuda) box.append($('div', { class: 'nota-item', html: it.ayuda }));
|
||||||
const inp = $('input', { type: 'file', accept: 'image/*', capture: 'environment', multiple: 'multiple', style: 'display:none' });
|
const inp = $('input', { type: 'file', accept: 'image/*', capture: 'environment', multiple: 'multiple', style: 'display:none' });
|
||||||
const btn = $('button', { type: 'button', class: 'btn-foto', text: '📷 Tomar o elegir foto' });
|
const btn = $('button', { type: 'button', class: 'btn-foto', text: 'Tomar o elegir foto' });
|
||||||
const tiras = $('div', { class: 'miniaturas', 'data-fotos': id });
|
const tiras = $('div', { class: 'miniaturas', 'data-fotos': id });
|
||||||
const marca = $('input', { type: 'hidden', 'data-id': id, 'data-lbl': 'Fotos: ' + String(it.txt || '').replace(/<[^>]+>/g, ''), value: '' });
|
const marca = $('input', { type: 'hidden', 'data-id': id, 'data-lbl': 'Fotos: ' + String(it.txt || '').replace(/<[^>]+>/g, ''), value: '' });
|
||||||
btn.addEventListener('click', () => inp.click());
|
btn.addEventListener('click', () => inp.click());
|
||||||
@ -413,7 +413,7 @@
|
|||||||
if (!t) return;
|
if (!t) return;
|
||||||
const li = $('div', { class: 'mini ok' });
|
const li = $('div', { class: 'mini ok' });
|
||||||
li.append($('img', { src: '/api/adjunto?ruta=' + encodeURIComponent(f.ruta) }),
|
li.append($('img', { src: '/api/adjunto?ruta=' + encodeURIComponent(f.ruta) }),
|
||||||
$('span', { class: 'est', text: '✓ guardada' }));
|
$('span', { class: 'est', text: 'Guardada' }));
|
||||||
if (marca) li.append(quitarFoto(f, li, marca));
|
if (marca) li.append(quitarFoto(f, li, marca));
|
||||||
t.append(li);
|
t.append(li);
|
||||||
});
|
});
|
||||||
@ -423,9 +423,9 @@
|
|||||||
const t = wrap.querySelector(`.miniaturas[data-fotos="${a.campo}"]`);
|
const t = wrap.querySelector(`.miniaturas[data-fotos="${a.campo}"]`);
|
||||||
if (!t) return;
|
if (!t) return;
|
||||||
const li = $('div', { class: 'mini doc ok' });
|
const li = $('div', { class: 'mini doc ok' });
|
||||||
li.append($('span', { class: 'ico', text: '📄' }),
|
li.append($('span', { class: 'ico', text: 'Log' }),
|
||||||
$('span', { class: 'nom', text: String(a.nombre || '').slice(0, 28) }),
|
$('span', { class: 'nom', text: String(a.nombre || '').slice(0, 28) }),
|
||||||
$('span', { class: 'est', text: '✓ guardado' }));
|
$('span', { class: 'est', text: 'Guardado' }));
|
||||||
t.append(li);
|
t.append(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -471,7 +471,7 @@
|
|||||||
borrar.style.display = admin ? '' : 'none';
|
borrar.style.display = admin ? '' : 'none';
|
||||||
banner.className = 'msg ok';
|
banner.className = 'msg ok';
|
||||||
const f = (a._recibido || '').replace('T', ' ').slice(0, 16);
|
const f = (a._recibido || '').replace('T', ' ').slice(0, 16);
|
||||||
banner.innerHTML = '✓ <b>Acta ya enviada</b> por <b>' + (a.tecnico || '—') + '</b>'
|
banner.innerHTML = '<b>Acta ya enviada</b> por <b>' + (a.tecnico || '—') + '</b>'
|
||||||
+ (f ? ' el ' + f : '') + '. Registro final'
|
+ (f ? ' el ' + f : '') + '. Registro final'
|
||||||
+ (a.resultado ? ' — resultado <b>' + a.resultado + '</b>' : '') + '.'
|
+ (a.resultado ? ' — resultado <b>' + a.resultado + '</b>' : '') + '.'
|
||||||
+ (admin ? '' : ' Para rehacerla, pide a Leo que la borre.');
|
+ (admin ? '' : ' Para rehacerla, pide a Leo que la borre.');
|
||||||
@ -521,7 +521,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function quitarFoto(f, li, marca) {
|
function quitarFoto(f, li, marca) {
|
||||||
const b = $('button', { type: 'button', class: 'mini-btn', text: '✕ quitar' });
|
const b = $('button', { type: 'button', class: 'mini-btn', text: 'Quitar' });
|
||||||
b.addEventListener('click', () => {
|
b.addEventListener('click', () => {
|
||||||
fotos = fotos.filter(x => x !== f); li.remove();
|
fotos = fotos.filter(x => x !== f); li.remove();
|
||||||
marca.value = fotos.filter(x => x.campo === f.campo).length || '';
|
marca.value = fotos.filter(x => x.campo === f.campo).length || '';
|
||||||
@ -538,7 +538,7 @@
|
|||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||||
const f = j.fotos[0]; f.campo = campo; fotos.push(f);
|
const f = j.fotos[0]; f.campo = campo; fotos.push(f);
|
||||||
li.className = 'mini ok'; est.textContent = '✓ guardada';
|
li.className = 'mini ok'; est.textContent = 'Guardada';
|
||||||
li.append(quitarFoto(f, li, marca));
|
li.append(quitarFoto(f, li, marca));
|
||||||
marca.value = fotos.filter(x => x.campo === campo).length;
|
marca.value = fotos.filter(x => x.campo === campo).length;
|
||||||
guardarLocal(); aplicarCondiciones();
|
guardarLocal(); aplicarCondiciones();
|
||||||
@ -557,7 +557,7 @@
|
|||||||
if (!serieActual()) { fail('Primero elige la unidad: los archivos se guardan con su número de serie.'); return; }
|
if (!serieActual()) { fail('Primero elige la unidad: los archivos se guardan con su número de serie.'); return; }
|
||||||
for (const file of Array.from(lista).slice(0, 10)) {
|
for (const file of Array.from(lista).slice(0, 10)) {
|
||||||
const li = $('div', { class: 'mini doc subiendo' });
|
const li = $('div', { class: 'mini doc subiendo' });
|
||||||
li.append($('span', { class: 'ico', text: '📄' }),
|
li.append($('span', { class: 'ico', text: 'Log' }),
|
||||||
$('span', { class: 'nom', text: file.name.slice(0, 28) }));
|
$('span', { class: 'nom', text: file.name.slice(0, 28) }));
|
||||||
const est = $('span', { class: 'est', text: 'subiendo…' }); li.append(est); tiras.append(li);
|
const est = $('span', { class: 'est', text: 'subiendo…' }); li.append(est); tiras.append(li);
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
@ -568,8 +568,8 @@
|
|||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||||
const a = j.archivos[0]; a.campo = campo; archivos.push(a);
|
const a = j.archivos[0]; a.campo = campo; archivos.push(a);
|
||||||
li.className = 'mini doc ok'; est.textContent = '✓ ' + kb(a.bytes);
|
li.className = 'mini doc ok'; est.textContent = kb(a.bytes);
|
||||||
const q = $('button', { type: 'button', class: 'mini-btn', text: '✕ quitar' });
|
const q = $('button', { type: 'button', class: 'mini-btn', text: 'Quitar' });
|
||||||
q.addEventListener('click', () => {
|
q.addEventListener('click', () => {
|
||||||
archivos = archivos.filter(x => x !== a); li.remove();
|
archivos = archivos.filter(x => x !== a); li.remove();
|
||||||
marca.value = archivos.filter(x => x.campo === campo).length || '';
|
marca.value = archivos.filter(x => x.campo === campo).length || '';
|
||||||
@ -609,7 +609,7 @@
|
|||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (!j.total) { hist.innerHTML = '🆕 <b>Primera visita</b> registrada para esta unidad.'; return; }
|
if (!j.total) { hist.innerHTML = '🆕 <b>Primera visita</b> registrada para esta unidad.'; return; }
|
||||||
hist.innerHTML = '';
|
hist.innerHTML = '';
|
||||||
hist.append($('div', { class: 'hist-t', html: '📋 <b>' + j.total + ' visita(s) anterior(es)</b> a esta unidad — léelas antes de empezar:' }));
|
hist.append($('div', { class: 'hist-t', html: '<b>' + j.total + ' visita(s) anterior(es)</b> a esta unidad — léelas antes de empezar:' }));
|
||||||
j.visitas.slice(0, 6).forEach(v => {
|
j.visitas.slice(0, 6).forEach(v => {
|
||||||
const f = (v.recibido || '').replace('T', ' ').slice(0, 16);
|
const f = (v.recibido || '').replace('T', ' ').slice(0, 16);
|
||||||
hist.append($('div', { class: 'hist-r', html:
|
hist.append($('div', { class: 'hist-r', html:
|
||||||
@ -626,8 +626,8 @@
|
|||||||
let verif = {}; // clave -> 'OK' | 'FALLA' | 'SIN_ACTA' | 'PRUEBA' | 'EN_USO'
|
let verif = {}; // clave -> 'OK' | 'FALLA' | 'SIN_ACTA' | 'PRUEBA' | 'EN_USO'
|
||||||
let usados = null; // { clave: { valor: serieUnidad } } — componentes ya asignados en otras actas de este tipo
|
let usados = null; // { clave: { valor: serieUnidad } } — componentes ya asignados en otras actas de este tipo
|
||||||
function setBadge(el, st, extra) {
|
function setBadge(el, st, extra) {
|
||||||
const map = { OK: ['ok', '✓ Aprobado'], FALLA: ['falla', '✕ FALLA'], SIN_ACTA: ['falla', '✕ Sin acta'],
|
const map = { OK: ['ok', 'Aprobado'], FALLA: ['falla', 'Falla'], SIN_ACTA: ['falla', 'Sin acta'],
|
||||||
PRUEBA: ['pend', '⚙ prueba'], EN_USO: ['falla', '⚠ ya en ' + (extra || 'otra unidad')], CONSULTANDO: ['pend', '…'] };
|
PRUEBA: ['pend', 'Prueba'], EN_USO: ['falla', 'Ya en ' + (extra || 'otra unidad')], CONSULTANDO: ['pend', '…'] };
|
||||||
const m = map[st] || ['pend', '—'];
|
const m = map[st] || ['pend', '—'];
|
||||||
el.className = 'badge ' + m[0]; el.textContent = m[1];
|
el.className = 'badge ' + m[0]; el.textContent = m[1];
|
||||||
}
|
}
|
||||||
@ -745,7 +745,7 @@
|
|||||||
cargarLocal(); aplicarCondiciones();
|
cargarLocal(); aplicarCondiciones();
|
||||||
if (compuesta && cfg.exigirAprobados && !todoAprobado()) {
|
if (compuesta && cfg.exigirAprobados && !todoAprobado()) {
|
||||||
banner.className = 'msg err';
|
banner.className = 'msg err';
|
||||||
banner.innerHTML = '⛔ <b>No se puede ensamblar con esta selección.</b> Todos los módulos deben estar <b>aprobados</b> en su acta y no estar ya asignados a otra unidad. Revisa los indicadores de arriba.';
|
banner.innerHTML = '<b>No se puede ensamblar con esta selección.</b> Todos los módulos deben estar <b>aprobados</b> en su acta y no estar ya asignados a otra unidad. Revisa los indicadores de arriba.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -805,10 +805,10 @@
|
|||||||
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
if (!r.ok || !j.ok) throw new Error(j.error || ('HTTP ' + r.status));
|
||||||
try { localStorage.removeItem(key()); } catch (e) {}
|
try { localStorage.removeItem(key()); } catch (e) {}
|
||||||
msg.className = 'msg ok';
|
msg.className = 'msg ok';
|
||||||
msg.innerHTML = '✓ Acta enviada y registrada como <b>' + d.serie + '</b>.'
|
msg.innerHTML = 'Acta enviada y registrada como <b>' + d.serie + '</b>.'
|
||||||
+ (compuesta && !esPrueba(d.serie) ? '<br>Etiqueta de la unidad: <b style="font-size:18px;letter-spacing:.04em">' + d.serie + '</b>' : '');
|
+ (compuesta && !esPrueba(d.serie) ? '<br>Etiqueta de la unidad: <b style="font-size:18px;letter-spacing:.04em">' + d.serie + '</b>' : '');
|
||||||
if (cfg.repetible) {
|
if (cfg.repetible) {
|
||||||
msg.innerHTML = '✓ <b>Registro enviado</b> para <b>' + d.serie + '</b>'
|
msg.innerHTML = '<b>Registro enviado</b> para <b>' + d.serie + '</b>'
|
||||||
+ (d.fotos && d.fotos.length ? ' con ' + d.fotos.length + ' foto(s)' : '')
|
+ (d.fotos && d.fotos.length ? ' con ' + d.fotos.length + ' foto(s)' : '')
|
||||||
+ '.<br>Ingeniería ya puede verlo. Puedes cerrar o elegir otra unidad.';
|
+ '.<br>Ingeniería ya puede verlo. Puedes cerrar o elegir otra unidad.';
|
||||||
btn.disabled = false; btn.textContent = 'Enviar acta firmada';
|
btn.disabled = false; btn.textContent = 'Enviar acta firmada';
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de módulo — simpleRTK2B Pro (RTK)</title>
|
<title>Acta de módulo — simpleRTK2B Pro (RTK)</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de diagnóstico en banco — HBOX</title>
|
<title>Acta de diagnóstico en banco — HBOX</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de daño o incidente — HBOX</title>
|
<title>Acta de daño o incidente — HBOX</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de diagnóstico en campo — HBOX</title>
|
<title>Acta de diagnóstico en campo — HBOX</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de ensamble — unidad HBOX v5</title>
|
<title>Acta de ensamble — unidad HBOX v5</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de envío — HBOX</title>
|
<title>Acta de envío — HBOX</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Prueba de banco — Ethernet y puertos serie (V5)</title>
|
<title>Prueba de banco — Ethernet y puertos serie (V5)</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,9 +4,10 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de instalación en campo — HBOX</title>
|
<title>Acta de instalación en campo — HBOX</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
<style>
|
<style>
|
||||||
details.como{background:#eef3f8;border:1px solid #c9dcec;border-radius:10px;padding:8px 14px;margin:0 0 12px;font-size:14px;color:#1b3b57}
|
details.como{background:var(--hg-plano);border:1px solid var(--hg-linea);border-radius:10px;padding:8px 14px;margin:0 0 12px;font-size:14px;color:var(--hg-texto)}
|
||||||
details.como summary{cursor:pointer;font-weight:700}
|
details.como summary{cursor:pointer;font-weight:700}
|
||||||
details.como ol{margin:8px 0 4px;padding-left:20px} details.como li{margin:3px 0}
|
details.como ol{margin:8px 0 4px;padding-left:20px} details.como li{margin:3px 0}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de integración — RTK + 4G</title>
|
<title>Acta de integración — RTK + 4G</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de módulo — 4G NTRIP Master</title>
|
<title>Acta de módulo — 4G NTRIP Master</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Prueba de banco — módem 4G en batería</title>
|
<title>Prueba de banco — módem 4G en batería</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de recepción e inspección — HBOX</title>
|
<title>Acta de recepción e inspección — HBOX</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de reflasheo y configuración — HBOX</title>
|
<title>Acta de reflasheo y configuración — HBOX</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de prueba — EinarBoard V4 (fw 0.6.0)</title>
|
<title>Acta de prueba — EinarBoard V4 (fw 0.6.0)</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -55,7 +56,7 @@ Acta.render({
|
|||||||
+ '<a href="../procedimientos/v4-prueba-fw-0.5.6/">V4 — preparación y prueba con 0.6.0</a>. '
|
+ '<a href="../procedimientos/v4-prueba-fw-0.5.6/">V4 — preparación y prueba con 0.6.0</a>. '
|
||||||
+ 'Graba la consola desde el primer minuto; al final se sube completa. '
|
+ 'Graba la consola desde el primer minuto; al final se sube completa. '
|
||||||
+ 'Si una etapa no pasa, marca «me detengo»: lo que sigue se oculta, llenas el Cierre y envías como <b>FALLA</b>. '
|
+ 'Si una etapa no pasa, marca «me detengo»: lo que sigue se oculta, llenas el Cierre y envías como <b>FALLA</b>. '
|
||||||
+ 'Sin número de serie: el siguiente V4 libre del <a href="../tablero.html">tablero</a>. La serie PRUEBA sirve para practicar.',
|
+ 'Sin número de serie: el siguiente V4 libre del <a href="../tableros/actas/">tablero de actas</a>. La serie PRUEBA sirve para practicar.',
|
||||||
secciones: [
|
secciones: [
|
||||||
|
|
||||||
{ titulo: '1 · Datos',
|
{ titulo: '1 · Datos',
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Acta de prueba — EinarBoard V5</title>
|
<title>Acta de prueba — EinarBoard V5</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Ver acta — HBOX</title>
|
<title>Ver acta — HBOX</title>
|
||||||
|
<script src="../js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="acta.css">
|
<link rel="stylesheet" href="acta.css">
|
||||||
<style>
|
<style>
|
||||||
.doc{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:18px}
|
.doc{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:18px}
|
||||||
@ -25,7 +26,7 @@
|
|||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<header class="top"><h1 id="titulo">Acta</h1><p id="sub">Cargando…</p></header>
|
<header class="top"><h1 id="titulo">Acta</h1><p id="sub">Cargando…</p></header>
|
||||||
<div style="display:flex;justify-content:space-between;margin:2px 0 10px">
|
<div style="display:flex;justify-content:space-between;margin:2px 0 10px">
|
||||||
<a class="volver" href="../tablero.html">← Tablero</a>
|
<a class="volver" href="../tableros/actas/">← Estado de actas</a>
|
||||||
<a class="volver noprint" href="#" onclick="window.print();return false">Imprimir / PDF</a>
|
<a class="volver noprint" href="#" onclick="window.print();return false">Imprimir / PDF</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="cont"></div>
|
<div id="cont"></div>
|
||||||
@ -66,14 +67,14 @@ function render(a){
|
|||||||
if(sec!==curSec){ if(curSec!==-1) h+='</div>'; h+='<div class="sec"><h3>Sección '+(sec+1)+'</h3>'; curSec=sec; }
|
if(sec!==curSec){ if(curSec!==-1) h+='</div>'; h+='<div class="sec"><h3>Sección '+(sec+1)+'</h3>'; curSec=sec; }
|
||||||
const it=R[k]; const val=it.val;
|
const it=R[k]; const val=it.val;
|
||||||
let vista;
|
let vista;
|
||||||
if(val===true) vista='<span class="si">✓ Sí</span>';
|
if(val===true) vista='<span class="si">Sí</span>';
|
||||||
else if(val===false) vista='<span class="no">— No marcado</span>';
|
else if(val===false) vista='<span class="no">— No marcado</span>';
|
||||||
else vista = val==='' ? '<span class="no">—</span>' : esc(val);
|
else vista = val==='' ? '<span class="no">—</span>' : esc(val);
|
||||||
h+='<div class="row"><div class="t">'+ (it.lbl||k) +'</div><div class="a">'+vista+'</div></div>';
|
h+='<div class="row"><div class="t">'+ (it.lbl||k) +'</div><div class="a">'+vista+'</div></div>';
|
||||||
});
|
});
|
||||||
if(curSec!==-1) h+='</div>';
|
if(curSec!==-1) h+='</div>';
|
||||||
h += '<div class="sec"><div class="row"><div class="t">Conformidad del técnico</div><div class="a">'
|
h += '<div class="sec"><div class="row"><div class="t">Conformidad del técnico</div><div class="a">'
|
||||||
+ (a.conforme?'<span class="si">✓ Confirmado</span>':'<span class="no">—</span>') + '</div></div></div>';
|
+ (a.conforme?'<span class="si">Confirmado</span>':'<span class="no">—</span>') + '</div></div></div>';
|
||||||
h += '</div>';
|
h += '</div>';
|
||||||
cont.innerHTML = h;
|
cont.innerHTML = h;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ title: "Tarjetas y hardware"
|
|||||||
|
|
||||||
# Tarjetas y hardware
|
# Tarjetas y hardware
|
||||||
|
|
||||||
Estado de cada versión de la EinarBoard al **19-oct-2026**, con sus documentos vigentes, los históricos y el siguiente paso. ¿No sabes qué tarjeta tienes? [Manuales por tarjeta](../manuales/index.md) te la identifica por serigrafía, conectores y LEDs. Estado vivo (tareas, decisiones, responsables): [Tablero de ingeniería](../tableros/ingenieria.html).
|
Estado de cada versión de la EinarBoard al **19-oct-2026**, con sus documentos vigentes, los históricos y el siguiente paso. ¿No sabes qué tarjeta tienes? [Manuales por tarjeta](../manuales/index.md) te la identifica por serigrafía, conectores y LEDs. Estado vivo (tareas, decisiones, responsables): [Tablero de ingeniería](../tableros/ingenieria.md).
|
||||||
|
|
||||||
!!! danger "Re-auditoría del 19-oct: todavía no se pide ninguna, pero a la V5.6 le falta una hora"
|
!!! danger "Re-auditoría del 19-oct: todavía no se pide ninguna, pero a la V5.6 le falta una hora"
|
||||||
- **V5.6 — casi lista.** La pasada 3 de Michelle cerró lo grande: `/BAT_PLUS` fuera de capas internas, `/3V3_MODEM` a 1.0 mm, 4 vías en el EP de U7, sin cobre suelto, y el sensado Kelvin de RS1 sigue intacto. T30 ya está aplicado (`7650b96`).
|
- **V5.6 — casi lista.** La pasada 3 de Michelle cerró lo grande: `/BAT_PLUS` fuera de capas internas, `/3V3_MODEM` a 1.0 mm, 4 vías en el EP de U7, sin cobre suelto, y el sensado Kelvin de RS1 sigue intacto. T30 ya está aplicado (`7650b96`).
|
||||||
@ -147,7 +147,7 @@ El sensado de RS1 ya está bien (hay que conservarlo). T25 y T28 se volvieron a
|
|||||||
| Qué trae la V5.7 y por qué | [ECO V5.7](v57-eco-fuente.md) |
|
| Qué trae la V5.7 y por qué | [ECO V5.7](v57-eco-fuente.md) |
|
||||||
| Qué falta para pedir la v6 | [Auditoría v6 (16-oct)](v6-auditoria-produccion-16oct.md) · [Plan de corrección](v6-plan-correccion.md) |
|
| Qué falta para pedir la v6 | [Auditoría v6 (16-oct)](v6-auditoria-produccion-16oct.md) · [Plan de corrección](v6-plan-correccion.md) |
|
||||||
| Qué se hace cada fin de semana en la v6 | [Plan de fines de semana](v6-plan-fines-de-semana.md) |
|
| Qué se hace cada fin de semana en la v6 | [Plan de fines de semana](v6-plan-fines-de-semana.md) |
|
||||||
| Cuántas de cada versión y cuándo | [Tablero de producción](../tableros/produccion.html) |
|
| Cuántas de cada versión y cuándo | [Tablero de producción](../tableros/produccion.md) |
|
||||||
| El binario correcto | [Descargas](../descargas/index.md) |
|
| El binario correcto | [Descargas](../descargas/index.md) |
|
||||||
| KiCad o el repositorio | [Guía Git](../git/guia-git-es.md) |
|
| KiCad o el repositorio | [Guía Git](../git/guia-git-es.md) |
|
||||||
| Cómo llegamos aquí | [Historia del proyecto](../historia/index.md) |
|
| Cómo llegamos aquí | [Historia del proyecto](../historia/index.md) |
|
||||||
|
|||||||
@ -104,7 +104,7 @@ Firmware 0.6.3 a banco (1-oct). Se revisa el bus I²C de V5.6/v6 (5-oct), se dia
|
|||||||
| 12-oct | Instrucciones a Michelle para el layout final V5.6 (U26, sin P12). Borrador v0.1 de la ECO V5.7. | [Layout pasada 1](../hardware/v56-layout-pasada-1.md) — **superada** por la pasada 2 · `…2026-10-12_Drogheda_EinarBoard-V5.7_ECO-Fuente-a-bordo_ES_v0.1.md` — **superado** por v0.2 | V5.6 / V5.7 |
|
| 12-oct | Instrucciones a Michelle para el layout final V5.6 (U26, sin P12). Borrador v0.1 de la ECO V5.7. | [Layout pasada 1](../hardware/v56-layout-pasada-1.md) — **superada** por la pasada 2 · `…2026-10-12_Drogheda_EinarBoard-V5.7_ECO-Fuente-a-bordo_ES_v0.1.md` — **superado** por v0.2 | V5.6 / V5.7 |
|
||||||
| 13-oct | **Auditoría del layout de Michelle (`d71098d`): NO LISTA** — `/BAT_PLUS` por In1 a 0.5 mm (bloqueante); 18 piezas movidas (decisión: se quedan); MAIN5V/SYS5V/SW3 por capas internas desde V5.5. Pasada 2 con las correcciones. | [Auditoría 13-oct (inglés)](../hardware/v56-auditoria-layout-13oct.md) · [Layout pasada 2](../hardware/v56-layout-pasada-2.md) | V5.6 |
|
| 13-oct | **Auditoría del layout de Michelle (`d71098d`): NO LISTA** — `/BAT_PLUS` por In1 a 0.5 mm (bloqueante); 18 piezas movidas (decisión: se quedan); MAIN5V/SYS5V/SW3 por capas internas desde V5.5. Pasada 2 con las correcciones. | [Auditoría 13-oct (inglés)](../hardware/v56-auditoria-layout-13oct.md) · [Layout pasada 2](../hardware/v56-layout-pasada-2.md) | V5.6 |
|
||||||
| 13-oct | **V5.7 = V5.6 + fuente 24 V → 5 V a bordo** (U25 TPS54360B del bloque v6), sin SD-15B: esquemático aplicado (ERC 0), F2 fusible SMD 3 A, fuera U2 y P2, JP1 puenteado; proyecto KiCad separado `hbox-watchdog-v57`. Leo coloca la fuente; Michelle corrige V5.6 y después toma V5.7. | [ECO V5.7 v0.2](../hardware/v57-eco-fuente.md) · [Guía de layout V5.7](../hardware/v57-layout-guia.md) | V5.7 |
|
| 13-oct | **V5.7 = V5.6 + fuente 24 V → 5 V a bordo** (U25 TPS54360B del bloque v6), sin SD-15B: esquemático aplicado (ERC 0), F2 fusible SMD 3 A, fuera U2 y P2, JP1 puenteado; proyecto KiCad separado `hbox-watchdog-v57`. Leo coloca la fuente; Michelle corrige V5.6 y después toma V5.7. | [ECO V5.7 v0.2](../hardware/v57-eco-fuente.md) · [Guía de layout V5.7](../hardware/v57-layout-guia.md) | V5.7 |
|
||||||
| 13-oct | **Pedido de 570 HBOX:** 113 × V5.6 (con SD-15B) · 327 × V5.7 · 130 × v6 (+ reemplazos futuros en v6). Paquetes de fabricación V5.6/V5.7/v6 para cotizar. Caja plástica del HBOX: el cable Ethernet no cabe (cambio por definir). Sitios: ICAVE, LZC, EIT, TIMSA. | notas del proyecto (13-oct) · [Tablero de producción](../tableros/produccion.html) · [Kanban](../tableros/kanban.html) | V5.6 / V5.7 / v6 |
|
| 13-oct | **Pedido de 570 HBOX:** 113 × V5.6 (con SD-15B) · 327 × V5.7 · 130 × v6 (+ reemplazos futuros en v6). Paquetes de fabricación V5.6/V5.7/v6 para cotizar. Caja plástica del HBOX: el cable Ethernet no cabe (cambio por definir). Sitios: ICAVE, LZC, EIT, TIMSA. | notas del proyecto (13-oct) · [Tablero de producción](../tableros/produccion.md) · [Kanban](../tableros/pendientes.md) | V5.6 / V5.7 / v6 |
|
||||||
| 13-oct | Reorganización de este sitio por propósito; tableros, inventario y sitios. | esta página | — |
|
| 13-oct | Reorganización de este sitio por propósito; tableros, inventario y sitios. | esta página | — |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
docs/img/helgen-icono-180.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
1
docs/img/helgen-logo-claro.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 201.9"><title>Helgen</title><path fill="#E7ECF5" fill-rule="evenodd" d="M449.2 198.7C431.6 196.7 418.2 188.6 412.8 176.7C411.8 174.5 411.8 174.5 423.6 168.6C436.2 162.5 435.6 162.6 437.5 165.0C444.4 174.1 461.6 176.4 471.7 169.5C477.3 165.8 479.4 161.1 479.4 152.9C479.4 148.0 479.4 148.0 476.2 150.2C466.3 157.2 445.2 157.9 432.8 151.7C403.0 136.7 401.3 89.9 430.0 71.8C443.4 63.4 463.2 64.2 476.0 73.6C479.2 76.0 479.4 75.9 479.4 72.6C479.4 68.2 478.0 68.6 494.2 68.6C505.8 68.6 508.3 68.7 508.6 69.1C509.3 70.0 508.9 161.2 508.2 164.9C503.2 189.1 480.4 202.2 449.2 198.7ZM79.0 178.1C35.8 173.8 4.3 138.6 2.9 93.2C1.0 28.3 63.4 -16.4 118.4 10.5C171.1 36.4 182.8 111.0 140.9 154.0C124.6 170.8 100.1 180.3 79.0 178.1ZM95.3 166.3C137.8 159.3 164.8 112.7 152.3 68.1C139.8 23.6 93.8 1.2 54.9 20.8C14.7 40.9 1.2 96.0 27.0 135.1C42.6 158.7 68.9 170.7 95.3 166.3ZM323.6 155.6C313.8 153.8 309.7 152.2 303.0 147.8C279.2 131.9 276.2 99.2 296.7 79.4C309.0 67.4 325.5 63.4 343.9 67.7C358.2 71.1 369.4 81.6 373.4 95.5C375.0 101.2 376.0 113.5 374.9 114.6C374.6 114.9 367.4 115.0 344.6 115.0C327.7 115.0 314.4 115.2 314.2 115.3C313.2 115.9 313.9 120.4 315.3 123.3C318.4 129.4 324.4 132.5 333.3 132.4C341.0 132.3 344.1 131.1 349.7 126.0C351.3 124.6 352.7 123.5 352.9 123.5C353.6 123.5 372.9 133.4 373.1 133.9C373.4 134.7 372.4 136.4 369.8 139.6C363.0 147.7 354.0 152.8 341.9 155.0C339.0 155.5 325.7 156.0 323.6 155.6ZM549.3 155.3C512.9 148.7 498.7 105.4 524.5 79.8C534.2 70.2 544.0 66.3 558.6 66.3C586.5 66.3 603.6 83.3 603.6 110.9C603.6 113.0 603.5 114.3 603.2 114.6C602.9 114.9 595.6 115.0 572.8 115.0C539.1 115.0 541.5 114.8 541.9 117.6C543.2 126.6 549.4 131.9 559.5 132.5C567.4 133.0 572.3 131.2 578.2 125.8C581.3 123.0 579.8 122.6 591.8 128.7C602.9 134.3 602.5 133.8 599.5 137.8C592.0 147.7 581.7 153.4 568.2 155.3C563.2 156.0 553.2 156.0 549.3 155.3ZM175.7 153.5C175.3 153.4 174.8 153.1 174.6 152.9C173.8 152.3 173.9 17.8 174.7 17.2C175.4 16.6 204.4 16.5 205.0 17.0C205.1 17.2 205.3 29.4 205.4 44.1L205.6 70.9L227.4 70.9L249.2 70.9L249.3 44.3C249.4 20.1 249.5 17.6 250.0 17.2C250.9 16.4 279.9 16.5 280.4 17.2C280.6 17.6 280.7 43.3 280.7 85.1C280.7 127.0 280.6 152.7 280.4 153.0C279.9 153.8 250.9 153.8 250.0 153.1C249.5 152.6 249.4 150.2 249.3 125.8L249.2 99.0L227.4 99.0L205.6 99.0L205.4 126.0C205.3 140.8 205.1 153.1 205.0 153.2C204.6 153.6 176.9 153.8 175.7 153.5ZM378.8 153.4L378.0 153.2L377.9 82.5C377.8 30.6 377.9 11.7 378.2 11.4C378.7 10.7 405.9 10.7 406.6 11.3C407.2 12.0 407.2 152.9 406.5 153.3C406.0 153.6 380.0 153.8 378.8 153.4ZM607.1 153.4L606.3 153.2L606.3 111.0L606.3 68.8L620.3 68.7C630.1 68.6 634.5 68.7 634.8 69.0C635.1 69.2 635.3 70.5 635.4 72.9C635.5 77.1 635.6 77.2 638.1 75.1C647.2 67.3 656.3 64.7 668.0 66.7C683.8 69.3 694.2 78.2 696.7 91.0C697.5 95.4 697.4 152.9 696.6 153.3C695.2 153.8 668.7 153.6 668.3 153.0C668.1 152.7 668.0 142.9 668.0 128.2C668.0 101.8 668.0 101.5 666.0 97.5C661.2 87.6 648.7 86.1 640.4 94.4C635.2 99.6 635.3 99.4 635.3 128.7C635.3 147.7 635.2 153.0 634.8 153.3C634.3 153.6 608.3 153.8 607.1 153.4ZM465.4 130.5C481.7 127.1 485.6 103.2 471.3 94.2C466.5 91.1 459.1 90.2 453.7 92.1C435.8 98.3 436.4 125.4 454.5 130.1C457.5 130.9 462.6 131.1 465.4 130.5ZM50.7 110.8C42.7 107.9 37.4 102.3 35.5 94.5C33.9 88.0 33.9 72.5 35.5 62.7C36.4 57.2 36.2 57.2 41.2 62.1C45.8 66.6 50.4 69.9 56.6 73.4C71.5 81.6 75.7 89.9 70.4 101.0C67.0 108.3 57.2 113.1 50.7 110.8ZM113.8 111.3C107.7 109.8 101.7 104.3 99.7 98.3C96.4 88.5 101.0 80.9 114.3 73.5C121.0 69.8 125.5 66.6 129.8 62.2C132.1 59.9 134.1 58.0 134.3 58.0C137.0 58.0 137.8 88.4 135.2 96.4C132.3 105.6 121.5 113.1 113.8 111.3ZM57.3 101.3C60.5 99.8 62.6 96.8 62.9 93.0C63.3 87.9 61.6 85.9 53.6 82.0C50.7 80.6 47.2 78.7 45.9 77.9C41.8 75.4 41.7 75.6 42.0 82.8C42.5 96.8 49.3 104.9 57.3 101.3ZM121.8 100.8C126.7 98.0 129.4 90.7 129.4 80.7C129.5 75.6 129.3 75.5 125.4 77.9C124.1 78.7 120.5 80.7 117.4 82.1C109.7 85.9 108.3 87.4 108.3 92.0C108.3 99.6 115.7 104.4 121.8 100.8ZM345.9 98.4C347.2 97.4 344.8 91.9 341.9 89.2C334.6 82.4 319.9 84.5 316.0 92.9C314.6 95.9 314.3 97.8 315.0 98.4C315.9 99.1 345.0 99.1 345.9 98.4ZM574.0 98.5C575.3 97.7 573.4 92.6 570.9 89.9C563.9 82.3 548.4 84.1 544.2 93.0C542.8 96.0 542.5 97.8 543.3 98.4C544.1 98.9 573.2 99.1 574.0 98.5Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
1
docs/img/helgen-logo-indigo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 201.9"><title>Helgen</title><path fill="#2C3C5B" fill-rule="evenodd" d="M449.2 198.7C431.6 196.7 418.2 188.6 412.8 176.7C411.8 174.5 411.8 174.5 423.6 168.6C436.2 162.5 435.6 162.6 437.5 165.0C444.4 174.1 461.6 176.4 471.7 169.5C477.3 165.8 479.4 161.1 479.4 152.9C479.4 148.0 479.4 148.0 476.2 150.2C466.3 157.2 445.2 157.9 432.8 151.7C403.0 136.7 401.3 89.9 430.0 71.8C443.4 63.4 463.2 64.2 476.0 73.6C479.2 76.0 479.4 75.9 479.4 72.6C479.4 68.2 478.0 68.6 494.2 68.6C505.8 68.6 508.3 68.7 508.6 69.1C509.3 70.0 508.9 161.2 508.2 164.9C503.2 189.1 480.4 202.2 449.2 198.7ZM79.0 178.1C35.8 173.8 4.3 138.6 2.9 93.2C1.0 28.3 63.4 -16.4 118.4 10.5C171.1 36.4 182.8 111.0 140.9 154.0C124.6 170.8 100.1 180.3 79.0 178.1ZM95.3 166.3C137.8 159.3 164.8 112.7 152.3 68.1C139.8 23.6 93.8 1.2 54.9 20.8C14.7 40.9 1.2 96.0 27.0 135.1C42.6 158.7 68.9 170.7 95.3 166.3ZM323.6 155.6C313.8 153.8 309.7 152.2 303.0 147.8C279.2 131.9 276.2 99.2 296.7 79.4C309.0 67.4 325.5 63.4 343.9 67.7C358.2 71.1 369.4 81.6 373.4 95.5C375.0 101.2 376.0 113.5 374.9 114.6C374.6 114.9 367.4 115.0 344.6 115.0C327.7 115.0 314.4 115.2 314.2 115.3C313.2 115.9 313.9 120.4 315.3 123.3C318.4 129.4 324.4 132.5 333.3 132.4C341.0 132.3 344.1 131.1 349.7 126.0C351.3 124.6 352.7 123.5 352.9 123.5C353.6 123.5 372.9 133.4 373.1 133.9C373.4 134.7 372.4 136.4 369.8 139.6C363.0 147.7 354.0 152.8 341.9 155.0C339.0 155.5 325.7 156.0 323.6 155.6ZM549.3 155.3C512.9 148.7 498.7 105.4 524.5 79.8C534.2 70.2 544.0 66.3 558.6 66.3C586.5 66.3 603.6 83.3 603.6 110.9C603.6 113.0 603.5 114.3 603.2 114.6C602.9 114.9 595.6 115.0 572.8 115.0C539.1 115.0 541.5 114.8 541.9 117.6C543.2 126.6 549.4 131.9 559.5 132.5C567.4 133.0 572.3 131.2 578.2 125.8C581.3 123.0 579.8 122.6 591.8 128.7C602.9 134.3 602.5 133.8 599.5 137.8C592.0 147.7 581.7 153.4 568.2 155.3C563.2 156.0 553.2 156.0 549.3 155.3ZM175.7 153.5C175.3 153.4 174.8 153.1 174.6 152.9C173.8 152.3 173.9 17.8 174.7 17.2C175.4 16.6 204.4 16.5 205.0 17.0C205.1 17.2 205.3 29.4 205.4 44.1L205.6 70.9L227.4 70.9L249.2 70.9L249.3 44.3C249.4 20.1 249.5 17.6 250.0 17.2C250.9 16.4 279.9 16.5 280.4 17.2C280.6 17.6 280.7 43.3 280.7 85.1C280.7 127.0 280.6 152.7 280.4 153.0C279.9 153.8 250.9 153.8 250.0 153.1C249.5 152.6 249.4 150.2 249.3 125.8L249.2 99.0L227.4 99.0L205.6 99.0L205.4 126.0C205.3 140.8 205.1 153.1 205.0 153.2C204.6 153.6 176.9 153.8 175.7 153.5ZM378.8 153.4L378.0 153.2L377.9 82.5C377.8 30.6 377.9 11.7 378.2 11.4C378.7 10.7 405.9 10.7 406.6 11.3C407.2 12.0 407.2 152.9 406.5 153.3C406.0 153.6 380.0 153.8 378.8 153.4ZM607.1 153.4L606.3 153.2L606.3 111.0L606.3 68.8L620.3 68.7C630.1 68.6 634.5 68.7 634.8 69.0C635.1 69.2 635.3 70.5 635.4 72.9C635.5 77.1 635.6 77.2 638.1 75.1C647.2 67.3 656.3 64.7 668.0 66.7C683.8 69.3 694.2 78.2 696.7 91.0C697.5 95.4 697.4 152.9 696.6 153.3C695.2 153.8 668.7 153.6 668.3 153.0C668.1 152.7 668.0 142.9 668.0 128.2C668.0 101.8 668.0 101.5 666.0 97.5C661.2 87.6 648.7 86.1 640.4 94.4C635.2 99.6 635.3 99.4 635.3 128.7C635.3 147.7 635.2 153.0 634.8 153.3C634.3 153.6 608.3 153.8 607.1 153.4ZM465.4 130.5C481.7 127.1 485.6 103.2 471.3 94.2C466.5 91.1 459.1 90.2 453.7 92.1C435.8 98.3 436.4 125.4 454.5 130.1C457.5 130.9 462.6 131.1 465.4 130.5ZM50.7 110.8C42.7 107.9 37.4 102.3 35.5 94.5C33.9 88.0 33.9 72.5 35.5 62.7C36.4 57.2 36.2 57.2 41.2 62.1C45.8 66.6 50.4 69.9 56.6 73.4C71.5 81.6 75.7 89.9 70.4 101.0C67.0 108.3 57.2 113.1 50.7 110.8ZM113.8 111.3C107.7 109.8 101.7 104.3 99.7 98.3C96.4 88.5 101.0 80.9 114.3 73.5C121.0 69.8 125.5 66.6 129.8 62.2C132.1 59.9 134.1 58.0 134.3 58.0C137.0 58.0 137.8 88.4 135.2 96.4C132.3 105.6 121.5 113.1 113.8 111.3ZM57.3 101.3C60.5 99.8 62.6 96.8 62.9 93.0C63.3 87.9 61.6 85.9 53.6 82.0C50.7 80.6 47.2 78.7 45.9 77.9C41.8 75.4 41.7 75.6 42.0 82.8C42.5 96.8 49.3 104.9 57.3 101.3ZM121.8 100.8C126.7 98.0 129.4 90.7 129.4 80.7C129.5 75.6 129.3 75.5 125.4 77.9C124.1 78.7 120.5 80.7 117.4 82.1C109.7 85.9 108.3 87.4 108.3 92.0C108.3 99.6 115.7 104.4 121.8 100.8ZM345.9 98.4C347.2 97.4 344.8 91.9 341.9 89.2C334.6 82.4 319.9 84.5 316.0 92.9C314.6 95.9 314.3 97.8 315.0 98.4C315.9 99.1 345.0 99.1 345.9 98.4ZM574.0 98.5C575.3 97.7 573.4 92.6 570.9 89.9C563.9 82.3 548.4 84.1 544.2 93.0C542.8 96.0 542.5 97.8 543.3 98.4C544.1 98.9 573.2 99.1 574.0 98.5Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 37 KiB |
1
docs/img/helgen-mascara.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 214.5"><title>Helgen</title><ellipse cx="100" cy="107.25" rx="96" ry="103" fill="#2C3C5B"/><path fill="#51F3DA" fill-rule="evenodd" d="M96.2 213.2C32.3 210.2 -12.0 145.2 4.7 79.0C15.9 34.9 53.0 3.1 95.7 1.2C161.5 -1.7 211.4 63.4 196.0 132.3C185.1 181.0 142.7 215.4 96.2 213.2ZM104.0 199.9C160.7 196.6 198.8 136.5 181.2 78.1C169.4 39.2 134.8 12.9 97.1 14.5C40.8 16.7 1.8 75.9 18.2 134.3C29.5 174.7 65.4 202.1 104.0 199.9ZM62.5 132.8C60.4 132.5 57.0 131.1 53.2 129.0C44.3 124.0 40.5 116.9 39.4 103.5C39.0 98.4 39.4 83.9 40.1 78.2C40.2 76.8 41.7 67.7 41.7 67.5C41.9 67.2 42.3 67.6 46.7 71.9C52.2 77.4 53.3 78.4 57.7 81.4C60.9 83.6 63.3 85.0 67.7 87.3C82.4 95.3 87.9 104.4 84.6 115.9C82.6 122.8 79.1 126.8 72.0 130.3C67.7 132.4 65.1 133.1 62.5 132.8ZM137.4 132.8C133.3 132.3 126.2 128.4 122.9 125.0C117.9 119.7 115.7 110.4 118.0 103.9C119.8 98.6 123.9 94.1 130.7 89.7C132.1 88.8 133.6 88.0 136.9 86.2C141.2 83.9 145.9 80.8 149.6 77.9C150.6 77.1 152.3 75.5 155.7 72.1C160.4 67.3 160.7 67.1 160.8 67.7C161.0 68.2 162.3 76.5 162.4 77.7C163.1 83.0 163.5 99.2 163.1 104.2C161.9 117.6 157.9 124.4 148.5 129.4C143.2 132.2 140.2 133.1 137.4 132.8ZM65.6 120.9C70.8 119.4 73.8 115.0 73.8 109.1C73.8 103.2 72.1 101.3 62.4 97.0C57.7 94.8 52.6 91.9 49.6 89.7C49.0 89.2 48.9 89.3 48.7 90.3C47.4 95.7 49.0 107.8 51.6 113.2C53.7 117.4 57.0 120.2 60.8 121.1C61.9 121.3 64.5 121.2 65.6 120.9ZM142.2 121.0C146.5 119.8 150.2 115.9 151.9 110.9C153.9 104.7 154.9 95.2 154.0 90.6C153.7 89.3 153.6 89.2 152.9 89.8C149.3 92.3 145.2 94.6 139.0 97.5C130.9 101.2 129.0 103.1 128.7 107.9C128.3 114.7 131.8 119.8 137.6 121.1C138.6 121.3 141.3 121.2 142.2 121.0Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -21,13 +21,13 @@ La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J
|
|||||||
| **Diagnosticar** una unidad que falla en la máquina | [Diagnóstico en campo](diagnostico/procedimiento-diagnostico-campo.md) | [Diagnóstico en campo](formularios/diagnostico.html) |
|
| **Diagnosticar** una unidad que falla en la máquina | [Diagnóstico en campo](diagnostico/procedimiento-diagnostico-campo.md) | [Diagnóstico en campo](formularios/diagnostico.html) |
|
||||||
| **Diagnosticar** una unidad en la mesa | [Diagnóstico en banco](procedimientos/diagnostico-banco.md) | [Banco](formularios/banco.html) |
|
| **Diagnosticar** una unidad en la mesa | [Diagnóstico en banco](procedimientos/diagnostico-banco.md) | [Banco](formularios/banco.html) |
|
||||||
| **Instalar en sitio** (ICAVE, LZC, EIT, TIMSA) | [Checklist de puesta en marcha](procedimientos/v5-checklist-puesta-en-marcha.md) | [Instalación en campo](formularios/instalacion.html) |
|
| **Instalar en sitio** (ICAVE, LZC, EIT, TIMSA) | [Checklist de puesta en marcha](procedimientos/v5-checklist-puesta-en-marcha.md) | [Instalación en campo](formularios/instalacion.html) |
|
||||||
| **Ver los tableros** (kanban, ingeniería, producción, actas) | [Tableros](tableros/index.md) | — |
|
| **Ver los tableros** (pendientes, ingeniería, producción, actas) | [Tableros](tableros/index.md) | — |
|
||||||
| **Inventario** (existencias, entradas y salidas) | [Inventario](inventario/inventario.html) | — |
|
| **Inventario** (existencias, entradas y salidas) | [Inventario](tableros/inventario.md) | — |
|
||||||
| **Pedir una tarjeta o saber qué versión va** | [Tarjetas y hardware](hardware/index.md) | — |
|
| **Pedir una tarjeta o saber qué versión va** | [Tarjetas y hardware](hardware/index.md) | — |
|
||||||
| **Descargar firmware** (por tarjeta, con SHA-256) | [Descargas](descargas/index.md) | [Reflasheo](formularios/reflasheo.html) |
|
| **Descargar firmware** (por tarjeta, con SHA-256) | [Descargas](descargas/index.md) | [Reflasheo](formularios/reflasheo.html) |
|
||||||
| Reflashear una unidad en taller | [Reflasheo de unidades](procedimientos/reflasheo-unidades.md) | [Reflasheo](formularios/reflasheo.html) |
|
| Reflashear una unidad en taller | [Reflasheo de unidades](procedimientos/reflasheo-unidades.md) | [Reflasheo](formularios/reflasheo.html) |
|
||||||
| Enviar, recibir o reportar un daño | [Ciclo de vida](procedimientos/ciclo-de-vida.md) | [Envío](formularios/envio.html) · [Recepción](formularios/recepcion.html) · [Daño](formularios/dano.html) |
|
| Enviar, recibir o reportar un daño | [Ciclo de vida](procedimientos/ciclo-de-vida.md) | [Envío](formularios/envio.html) · [Recepción](formularios/recepcion.html) · [Daño](formularios/dano.html) |
|
||||||
| Ver todo lo que le pasó a una unidad | [Expediente de una unidad](unidad.html) | — |
|
| Ver todo lo que le pasó a una unidad | [Expediente de una unidad](tableros/unidad.md) | — |
|
||||||
| Saber qué tarjeta tengo en la mano | [Manuales por tarjeta](manuales/index.md) | — |
|
| Saber qué tarjeta tengo en la mano | [Manuales por tarjeta](manuales/index.md) | — |
|
||||||
| Entender cómo llegamos aquí | [Historia del proyecto](historia/index.md) | — |
|
| Entender cómo llegamos aquí | [Historia del proyecto](historia/index.md) | — |
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ La lupa busca por unidad (H00xx), serie (HBOX5-E07-A010-R0006), conector (P15, J
|
|||||||
| <span class="badge-v3">V3</span> | 3 unidades en campo (RTK-003/006/014). Registro congelado. | — | 4.0.0 | [Manual V3](manuales/manual-tarjeta-V3.md) |
|
| <span class="badge-v3">V3</span> | 3 unidades en campo (RTK-003/006/014). Registro congelado. | — | 4.0.0 | [Manual V3](manuales/manual-tarjeta-V3.md) |
|
||||||
| <span class="badge-fw">firmware</span> | El binario se elige **por tarjeta**, nunca por número de versión. 0.6.3 en banco desde el 1-oct, no para campo. | | | [Descargas](descargas/index.md#firmware-del-hbox-que-binario-va-en-que-tarjeta) · [notas de versión](descargas/notas-de-version.md) |
|
| <span class="badge-fw">firmware</span> | El binario se elige **por tarjeta**, nunca por número de versión. 0.6.3 en banco desde el 1-oct, no para campo. | | | [Descargas](descargas/index.md#firmware-del-hbox-que-binario-va-en-que-tarjeta) · [notas de versión](descargas/notas-de-version.md) |
|
||||||
|
|
||||||
Pedido total: **570 HBOX** (113 V5.6 + 327 V5.7 + 130 v6). Avance real contra el plan: [Tablero de producción](tableros/produccion.html).
|
Pedido total: **570 HBOX** (113 V5.6 + 327 V5.7 + 130 v6). Avance real contra el plan: [Tablero de producción](tableros/produccion.md).
|
||||||
|
|
||||||
## Hoy — 19 de octubre de 2026
|
## Hoy — 19 de octubre de 2026
|
||||||
|
|
||||||
@ -56,6 +56,6 @@ Pedido total: **570 HBOX** (113 V5.6 + 327 V5.7 + 130 v6). Avance real contra el
|
|||||||
- **Decisiones de Leo (D-19-1…4):** puertos de V5.6/V5.7 (si van, entran al piloto), Dbk2, auto-sync y clase HV24: [§7](hardware/re-auditoria-19oct.md#7-decisiones-para-leo).
|
- **Decisiones de Leo (D-19-1…4):** puertos de V5.6/V5.7 (si van, entran al piloto), Dbk2, auto-sync y clase HV24: [§7](hardware/re-auditoria-19oct.md#7-decisiones-para-leo).
|
||||||
- **V5.7 sin Mean Well acepta 9–32 V** (máquinas de 12 y 24 V). Recomendado: TVS bidireccional para que la polaridad invertida no abra el fusible.
|
- **V5.7 sin Mean Well acepta 9–32 V** (máquinas de 12 y 24 V). Recomendado: TVS bidireccional para que la polaridad invertida no abra el fusible.
|
||||||
- **Jesús:** medir en una V5.5 el límite del cargador con TP1 ([§4.5 de la auditoría del 16-oct](hardware/auditoria-produccion-16oct.md#45-prueba-definitiva-con-tp1-clprog-cualquier-tarjeta-cualquier-corriente)): (TP1 ÷ 32) ÷ voltaje en RS1 ≈ 3.1 confirma el defecto.
|
- **Jesús:** medir en una V5.5 el límite del cargador con TP1 ([§4.5 de la auditoría del 16-oct](hardware/auditoria-produccion-16oct.md#45-prueba-definitiva-con-tp1-clprog-cualquier-tarjeta-cualquier-corriente)): (TP1 ÷ 32) ÷ voltaje en RS1 ≈ 3.1 confirma el defecto.
|
||||||
- **Plan de 570 unidades** (113 / 327 / 130). Pendiente: la caja plástica del HBOX, donde el cable Ethernet no cabe. Tareas y responsables en el [Kanban](tableros/kanban.html).
|
- **Plan de 570 unidades** (113 / 327 / 130). Pendiente: la caja plástica del HBOX, donde el cable Ethernet no cabe. Tareas y responsables en [Pendientes](tableros/pendientes.md).
|
||||||
|
|
||||||
Fuente: repositorio Drogheda (`hbox-docs` se publica desde él). Contacto: leo@helgen.tech
|
Fuente: repositorio Drogheda (`hbox-docs` se publica desde él). Contacto: leo@helgen.tech
|
||||||
|
|||||||
@ -8,15 +8,15 @@ tags: [project-drogheda, hbox, inventario, produccion, compras]
|
|||||||
|
|
||||||
Qué hay en el taller para producir e instalar HBOX: tarjetas, módulos, gabinetes, cajas, fuentes, packs, antenas, cables y los componentes de entrega larga. No es una lista de BOM: aquí solo entra lo que se cuenta a mano.
|
Qué hay en el taller para producir e instalar HBOX: tarjetas, módulos, gabinetes, cajas, fuentes, packs, antenas, cables y los componentes de entrega larga. No es una lista de BOM: aquí solo entra lo que se cuenta a mano.
|
||||||
|
|
||||||
**[Abrir el inventario →](inventario.html)**
|
**[Abrir el inventario →](../tableros/inventario.md)**
|
||||||
|
|
||||||
## Qué hay aquí
|
## Qué hay aquí
|
||||||
|
|
||||||
| Página | Para qué |
|
| Página | Para qué |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [Inventario — existencias y movimientos](inventario.html) | Tabla de existencias por ítem con mínimo y alerta; alta de ítem; movimiento de **entrada** (compra, devolución) o **salida** (ensamble, envío, baja) con motivo y liga a acta/unidad; exportar CSV |
|
| [Inventario — existencias y movimientos](../tableros/inventario.md) | Tabla de existencias por ítem con mínimo y alerta; alta de ítem; movimiento de **entrada** (compra, devolución) o **salida** (ensamble, envío, baja) con motivo y liga a acta/unidad; exportar CSV |
|
||||||
| [Tablero de producción](../tableros/produccion.html) | Lo que el inventario tiene que cubrir: 113 V5.6 · 327 V5.7 · 130 v6 |
|
| [Tablero de producción](../tableros/produccion.md) | Lo que el inventario tiene que cubrir: 113 V5.6 · 327 V5.7 · 130 v6 |
|
||||||
| [Kanban — pista Compras](../tableros/kanban.html) | Pedidos en curso (SD-15B-05 ×113, paquetes LFP, gabinetes, cajas HBOX, antenas) |
|
| [Pendientes, pista Compras](../tableros/pendientes.md?pista=compras) | Pedidos en curso (SD-15B-05 ×113, paquetes LFP, gabinetes, cajas HBOX, antenas) |
|
||||||
|
|
||||||
## Qué se registra y qué no
|
## Qué se registra y qué no
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ Qué hay en el taller para producir e instalar HBOX: tarjetas, módulos, gabinet
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Tarjetas EinarBoard por versión (V5.5, V5.6, V5.7, v6) | Resistencias, capacitores y componentes de la BOM normal |
|
| Tarjetas EinarBoard por versión (V5.5, V5.6, V5.7, v6) | Resistencias, capacitores y componentes de la BOM normal |
|
||||||
| Módulos: ArduSimple simpleRTK2B Pro, 4G NTRIP Master | Herramienta y consumibles del banco |
|
| Módulos: ArduSimple simpleRTK2B Pro, 4G NTRIP Master | Herramienta y consumibles del banco |
|
||||||
| Gabinetes IP67, cajas plásticas HBOX, fuentes SD-15B, packs LFP | Unidades ya ensambladas (esas viven en el [tablero](../tablero.html) por su acta) |
|
| Gabinetes IP67, cajas plásticas HBOX, fuentes SD-15B, packs LFP | Unidades ya ensambladas (esas viven en el [tablero](../tableros/actas.md) por su acta) |
|
||||||
| Antenas GNSS y LTE, cable Ethernet, conectores GX16, fusibles DIN | — |
|
| Antenas GNSS y LTE, cable Ethernet, conectores GX16, fusibles DIN | — |
|
||||||
| Componentes críticos de entrega larga: LTC4040, TPS54360B, SRP1265A | — |
|
| Componentes críticos de entrega larga: LTC4040, TPS54360B, SRP1265A | — |
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ El stock es la **suma de los movimientos**; nadie lo escribe a mano. Cada movimi
|
|||||||
|
|
||||||
- El **mínimo** lo fija quien compra (Leo), pensando en el tiempo de entrega: chips y antenas RTK tardan ~4 semanas; LTC4040 y la fuente a bordo son de entrega larga.
|
- El **mínimo** lo fija quien compra (Leo), pensando en el tiempo de entrega: chips y antenas RTK tardan ~4 semanas; LTC4040 y la fuente a bordo son de entrega larga.
|
||||||
- Para el pedido de 570, la fuente Mean Well SD-15B-05 tiene mínimo **113** (solo la usa V5.6).
|
- Para el pedido de 570, la fuente Mean Well SD-15B-05 tiene mínimo **113** (solo la usa V5.6).
|
||||||
- Por debajo del mínimo el ítem se marca en rojo aquí y en el [tablero de producción](../tableros/produccion.html). La alerta no manda correos: hay que mirar el tablero.
|
- Por debajo del mínimo el ítem se marca en rojo aquí y en el [tablero de producción](../tableros/produccion.md). La alerta no manda correos: hay que mirar el tablero.
|
||||||
- Mínimo 0 = sin alerta (consumibles que no vale la pena vigilar).
|
- Mínimo 0 = sin alerta (consumibles que no vale la pena vigilar).
|
||||||
|
|
||||||
## Conteo inicial (pendiente del 13-oct)
|
## Conteo inicial (pendiente del 13-oct)
|
||||||
|
|||||||
@ -1,241 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
<!-- Desde el 20-oct-2026 este tablero vive dentro del sitio: docs/tableros/inventario.md → /tableros/inventario/
|
||||||
|
Este archivo solo redirige (conserva ?serie=, ?id=, ?pista=… y #…) para que las ligas viejas
|
||||||
|
guardadas en pendientes y actas sigan sirviendo. No se edita: los cambios van en tableros/inventario.md. -->
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Inventario de materiales — HBOX</title>
|
<meta name="robots" content="noindex">
|
||||||
<link rel="stylesheet" href="../tableros/tablero.css">
|
<title>Inventario - Drogheda</title>
|
||||||
<style>
|
<script>location.replace('/tableros/inventario/' + location.search + location.hash);</script>
|
||||||
table.t td.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
|
<meta http-equiv="refresh" content="0; url=/tableros/inventario/">
|
||||||
table.t th.num{text-align:right}
|
<style>body{margin:0;padding:24px;background:#0E1524;color:#E7ECF5;font:16px/1.5 system-ui,sans-serif}a{color:#7FE9D8}</style>
|
||||||
table.t tr.alerta td.num.stock{color:var(--falla);font-weight:700}
|
|
||||||
table.t td .cod{font-weight:700;cursor:pointer;color:var(--naranja)}
|
|
||||||
table.t td .cat{font-size:11px;color:var(--tinta2);display:block}
|
|
||||||
table.t td button{padding:5px 9px;font-size:12.5px;margin:1px 2px 1px 0}
|
|
||||||
.acciones{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 12px}
|
|
||||||
.acciones .cnt{color:var(--tinta2);font-size:13px;margin-left:auto}
|
|
||||||
.mov-fila{display:grid;grid-template-columns:120px 70px 70px 1fr;gap:6px 10px;padding:6px 0;border-bottom:1px dashed var(--linea);font-size:13px;align-items:baseline}
|
|
||||||
.mov-fila .d{font-variant-numeric:tabular-nums}
|
|
||||||
.mov-fila .pos{color:var(--ok);font-weight:700} .mov-fila .neg{color:var(--falla);font-weight:700}
|
|
||||||
@media(max-width:560px){.mov-fila{grid-template-columns:1fr 1fr}}
|
|
||||||
.ayuda{font-size:12.5px;color:var(--tinta2);margin-top:3px}
|
|
||||||
.mini{font-size:12.5px;color:var(--tinta2)}
|
|
||||||
@media(max-width:760px){#cont table.t{min-width:780px}} /* en teléfono: desliza de lado, no aplastes columnas */
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<p>Este tablero ahora está dentro del sitio: <a href="/tableros/inventario/">Inventario</a>.</p>
|
||||||
<header class="top">
|
|
||||||
<div><h1>Inventario de materiales</h1><div class="meta" id="meta">Cargando…</div></div>
|
|
||||||
<nav>
|
|
||||||
<a href="../">← Documentación</a>
|
|
||||||
<a href="../tableros/produccion.html">Producción</a>
|
|
||||||
<a href="../tableros/kanban.html">Pendientes</a>
|
|
||||||
<a href="./">Política de inventario</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<details class="como">
|
|
||||||
<summary>Cómo se usa</summary>
|
|
||||||
<ol>
|
|
||||||
<li>El <b>stock no se escribe a mano</b>: es la suma de los movimientos. Llegó material → <b>Entrada</b>. Se usó en un ensamble o se envió → <b>Salida</b> con la unidad o el acta como referencia.</li>
|
|
||||||
<li>Contaste y no cuadra → <b>Ajuste</b> con el conteo físico; el sistema calcula la diferencia y la deja registrada con tu nombre.</li>
|
|
||||||
<li>Toca el <b>código</b> para ver los movimientos de ese ítem. Cada uno dice quién, cuándo, cuánto y por qué.</li>
|
|
||||||
<li><b>Alerta</b> (rojo) = stock por debajo del mínimo. El mínimo lo pone quien compra; el tablero de producción lo repite.</li>
|
|
||||||
<li><b>Nuevo ítem</b> solo para cosas que se cuentan: tarjetas, módulos, gabinetes, cajas, fuentes, packs, antenas, cables y los componentes de entrega larga. Nada de resistencias.</li>
|
|
||||||
<li><b>Exportar CSV</b> baja la tabla para Excel. Nada se borra: si un ítem ya no aplica, ponle «(obsoleto)» en la descripción.</li>
|
|
||||||
</ol>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div class="acciones">
|
|
||||||
<select id="fCat"><option value="">Todas las categorías</option></select>
|
|
||||||
<select id="fVer"><option value="">Aplica a: todas</option></select>
|
|
||||||
<label style="margin:0;display:flex;gap:6px;align-items:center;font-weight:500"><input type="checkbox" id="fAlerta" style="width:auto"> solo alertas</label>
|
|
||||||
<input type="search" id="fBuscar" placeholder="Buscar código o descripción…" style="flex:1;min-width:150px">
|
|
||||||
<button id="bMov">+ Movimiento</button>
|
|
||||||
<button id="bNuevo" class="sec">+ Nuevo ítem</button>
|
|
||||||
<a href="/api/inventario/export.csv" class="sec" style="padding:8px 12px;border:1px solid var(--linea);border-radius:9px;text-decoration:none;font-size:14px;font-weight:600;background:var(--tarjeta);color:var(--tinta2)">⬇ Exportar CSV</a>
|
|
||||||
<span class="cnt" id="cnt"></span>
|
|
||||||
</div>
|
|
||||||
<div class="msg" id="aviso"></div>
|
|
||||||
<div id="cont" class="tblwrap"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dialog id="dlg"></dialog>
|
|
||||||
<script>
|
|
||||||
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 CAT = { tarjeta: 'Tarjeta', modulo: 'Módulo', gabinete: 'Gabinete', caja: 'Caja', fuente: 'Fuente', bateria: 'Batería', antena: 'Antena', cable: 'Cable', conector: 'Conector', 'componente-critico': 'Componente crítico', otro: 'Otro' };
|
|
||||||
const VERS = ['V5.5', 'V5.6', 'V5.7', 'v6'];
|
|
||||||
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
|
|
||||||
let ITEMS = [], ADMIN = false, USER = '';
|
|
||||||
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, ayuda) { const d = el('div'); d.append(el('label', { text: l }), i); if (ayuda) d.append(el('div', { class: 'ayuda', text: ayuda })); return d; }
|
|
||||||
|
|
||||||
Object.entries(CAT).forEach(([k, v]) => $('fCat').append(el('option', { value: k, text: v })));
|
|
||||||
VERS.forEach(v => $('fVer').append(el('option', { value: v, text: 'Aplica a: ' + v })));
|
|
||||||
|
|
||||||
function visibles() {
|
|
||||||
const cat = $('fCat').value, ver = $('fVer').value, solo = $('fAlerta').checked, q = $('fBuscar').value.trim().toLowerCase();
|
|
||||||
return ITEMS.filter(i => (!cat || i.categoria === cat) && (!ver || (i.aplica_a || []).includes(ver)) && (!solo || i.alerta)
|
|
||||||
&& (!q || (i.codigo + ' ' + i.descripcion + ' ' + (i.proveedor || '') + ' ' + (i.ubicacion || '')).toLowerCase().includes(q)));
|
|
||||||
}
|
|
||||||
function pintar() {
|
|
||||||
const cont = $('cont'); cont.innerHTML = '';
|
|
||||||
const vis = visibles();
|
|
||||||
$('cnt').textContent = vis.length + ' ítem(s)' + (vis.length !== ITEMS.length ? ' de ' + ITEMS.length : '') + ' · ' + vis.filter(i => i.alerta).length + ' en alerta';
|
|
||||||
if (!vis.length) { cont.append(el('div', { class: 'vacio', text: ITEMS.length ? 'Nada coincide con el filtro.' : 'Todavía no hay ítems. Crea el primero con «Nuevo ítem» o corre la siembra.' })); return; }
|
|
||||||
const t = el('table', { class: 't' });
|
|
||||||
t.innerHTML = '<tr><th>Código</th><th>Descripción</th><th>Aplica a</th><th class="num">Stock</th><th class="num">Mínimo</th><th>Ubicación</th><th>Proveedor</th><th></th></tr>';
|
|
||||||
vis.forEach(i => {
|
|
||||||
const tr = el('tr', { class: i.alerta ? 'alerta' : '' });
|
|
||||||
const cod = el('span', { class: 'cod', text: i.codigo, title: 'Ver movimientos' }); cod.addEventListener('click', () => verMovimientos(i));
|
|
||||||
tr.append(el('td', null, cod, el('span', { class: 'cat', text: CAT[i.categoria] || i.categoria })));
|
|
||||||
tr.append(el('td', null, document.createTextNode(i.descripcion), i.notas ? el('div', { class: 'mini', text: i.notas.length > 90 ? i.notas.slice(0, 89) + '…' : i.notas }) : null));
|
|
||||||
const ap = el('td'); (i.aplica_a || []).forEach(v => ap.append(el('span', { class: BADGE[v] || 'pill pend', text: v, style: 'margin-right:3px' }))); tr.append(ap);
|
|
||||||
tr.append(el('td', { class: 'num stock', html: esc(i.stock) + ' <span class="mini">' + esc(i.unidad) + '</span>' + (i.alerta ? ' <span class="pill falla">alerta</span>' : '') }));
|
|
||||||
tr.append(el('td', { class: 'num', text: i.minimo || '—' }));
|
|
||||||
tr.append(el('td', { text: i.ubicacion || '—' }));
|
|
||||||
tr.append(el('td', { html: esc(i.proveedor || '—') + (i.codigo_proveedor ? '<div class="mini">' + esc(i.codigo_proveedor) + '</div>' : '') }));
|
|
||||||
const acc = el('td', { style: 'white-space:nowrap' });
|
|
||||||
const bm = el('button', { text: 'Movimiento' }); bm.addEventListener('click', () => movimiento(i));
|
|
||||||
const be = el('button', { class: 'sec', text: 'Editar' }); be.addEventListener('click', () => editar(i));
|
|
||||||
acc.append(bm, be); tr.append(acc);
|
|
||||||
t.append(tr);
|
|
||||||
});
|
|
||||||
cont.append(t);
|
|
||||||
}
|
|
||||||
async function cargar() {
|
|
||||||
$('meta').textContent = 'Actualizando…';
|
|
||||||
try {
|
|
||||||
const j = await api('/api/inventario');
|
|
||||||
ITEMS = j.items || []; ADMIN = !!j.admin; USER = j.user || '';
|
|
||||||
pintar();
|
|
||||||
$('meta').textContent = ITEMS.length + ' ítems · ' + (j.alertas || 0) + ' en alerta · actualizado ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }) + (USER ? ' · ' + USER : '');
|
|
||||||
} catch (e) { $('cont').innerHTML = ''; $('cont').append(el('div', { class: 'err', text: 'No se pudo cargar el inventario: ' + e.message })); $('meta').textContent = 'Error'; }
|
|
||||||
}
|
|
||||||
['fCat', 'fVer', 'fAlerta'].forEach(id => $(id).addEventListener('change', pintar));
|
|
||||||
$('fBuscar').addEventListener('input', pintar);
|
|
||||||
|
|
||||||
// ---------- diálogos ----------
|
|
||||||
function abrirDialogo(titulo, cuerpoFn, botones) {
|
|
||||||
dlg.innerHTML = '';
|
|
||||||
const cab = el('div', { class: 'dcab' }); cab.append(el('h2', { text: titulo }));
|
|
||||||
const x = el('button', { type: 'button', text: '✕' }); x.addEventListener('click', () => dlg.close()); cab.append(x); dlg.append(cab);
|
|
||||||
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
|
||||||
const m = el('div', { class: 'msg' });
|
|
||||||
cuerpoFn(cuerpo, m); cuerpo.append(m);
|
|
||||||
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
|
||||||
const cancelar = el('button', { type: 'button', class: 'sec', text: botones ? 'Cancelar' : 'Cerrar' }); cancelar.addEventListener('click', () => dlg.close()); pie.append(cancelar);
|
|
||||||
(botones || []).forEach(([txt, fn]) => { const b = el('button', { type: 'button', text: txt }); b.addEventListener('click', async () => { b.disabled = true; try { await fn(m); } catch (e) { m.className = 'msg err'; m.textContent = e.message; } b.disabled = false; }); pie.append(b); });
|
|
||||||
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute('open', '');
|
|
||||||
}
|
|
||||||
function selItems(sel) {
|
|
||||||
const s = el('select'); s.append(el('option', { value: '', text: '— elegir ítem —' }));
|
|
||||||
ITEMS.forEach(i => s.append(el('option', { value: i.id, text: i.codigo + ' — ' + i.descripcion + ' (' + i.stock + ' ' + i.unidad + ')' })));
|
|
||||||
if (sel) s.value = sel.id; return s;
|
|
||||||
}
|
|
||||||
function movimiento(item) {
|
|
||||||
abrirDialogo('Registrar movimiento', (c) => {
|
|
||||||
const fItem = selItems(item);
|
|
||||||
const fTipo = el('select'); [['entrada', 'Entrada — llegó material (+)'], ['salida', 'Salida — se usó o se envió (−)'], ['ajuste', 'Ajuste — conteo físico (fija el stock)']].forEach(([v, t]) => fTipo.append(el('option', { value: v, text: t })));
|
|
||||||
const fCant = el('input', { type: 'number', min: '0', step: '1', inputmode: 'numeric', placeholder: '0' });
|
|
||||||
const fMot = el('input', { type: 'text', maxlength: '200', placeholder: 'llegó pedido Mouser · ensamble · envío a ICAVE · conteo físico' });
|
|
||||||
const fRef = el('input', { type: 'text', maxlength: '120', placeholder: 'HBOX5-E07-A010-R0006 · OC-0042 · acta de envío' });
|
|
||||||
const ayudaCant = el('div', { class: 'ayuda', text: 'Cantidad que entra.' });
|
|
||||||
fTipo.addEventListener('change', () => { ayudaCant.textContent = fTipo.value === 'entrada' ? 'Cantidad que entra.' : fTipo.value === 'salida' ? 'Cantidad que sale. No puede dejar el stock en negativo.' : 'Lo que contaste físicamente: el stock quedará exactamente en ese número.'; });
|
|
||||||
c.append(campo('Ítem', fItem), campo('Tipo', fTipo));
|
|
||||||
const g = el('div', { class: 'dos-col' }); const cc = campo('Cantidad', fCant); cc.append(ayudaCant); g.append(cc, campo('Referencia (unidad, acta u orden)', fRef)); c.append(g);
|
|
||||||
c.append(campo('Motivo (obligatorio)', fMot));
|
|
||||||
c._leer = () => ({ item_id: fItem.value, tipo: fTipo.value, cantidad: fCant.value === '' ? null : +fCant.value, motivo: fMot.value.trim(), referencia: fRef.value.trim() });
|
|
||||||
setTimeout(() => (item ? fCant : fItem).focus(), 50);
|
|
||||||
}, [['Registrar', async (m) => {
|
|
||||||
const d = dlg.querySelector('.dcuerpo')._leer();
|
|
||||||
if (!d.item_id) throw new Error('Elige el ítem.');
|
|
||||||
if (d.cantidad == null || isNaN(d.cantidad)) throw new Error('Escribe la cantidad.');
|
|
||||||
if (!d.motivo) throw new Error('Escribe el motivo.');
|
|
||||||
const j = await api('/api/inventario/movimiento', json('POST', d));
|
|
||||||
dlg.close(); mensaje('Movimiento registrado: ' + j.item.codigo + ' ahora tiene ' + j.item.stock + ' ' + j.item.unidad + '.', true); cargar();
|
|
||||||
}]]);
|
|
||||||
}
|
|
||||||
function formItem(c, i) {
|
|
||||||
i = i || {};
|
|
||||||
const f = {};
|
|
||||||
f.codigo = el('input', { type: 'text', maxlength: '40', value: i.codigo || '', placeholder: 'EB-V56 · SD-15B-05', style: 'text-transform:uppercase' });
|
|
||||||
f.descripcion = el('input', { type: 'text', maxlength: '200', value: i.descripcion || '' });
|
|
||||||
f.categoria = el('select'); Object.entries(CAT).forEach(([k, v]) => f.categoria.append(el('option', { value: k, text: v }))); f.categoria.value = i.categoria || 'otro';
|
|
||||||
f.unidad = el('select'); [['pza', 'pza (piezas)'], ['m', 'm (metros)']].forEach(([v, t]) => f.unidad.append(el('option', { value: v, text: t }))); f.unidad.value = i.unidad || 'pza';
|
|
||||||
f.aplica = el('div', { class: 'lista-chk', style: 'max-height:none;display:flex;gap:14px;flex-wrap:wrap' });
|
|
||||||
VERS.forEach(v => { const l = el('label', { style: 'margin:0' }); const ck = el('input', { type: 'checkbox', value: v }); ck.checked = (i.aplica_a || []).includes(v); l.append(ck, document.createTextNode(v)); f.aplica.append(l); });
|
|
||||||
f.minimo = el('input', { type: 'number', min: '0', step: '1', value: i.minimo || 0 });
|
|
||||||
f.ubicacion = el('input', { type: 'text', maxlength: '120', value: i.ubicacion || '', placeholder: 'Anaquel A2 · taller Veracruz' });
|
|
||||||
f.proveedor = el('input', { type: 'text', maxlength: '120', value: i.proveedor || '' });
|
|
||||||
f.codigo_proveedor = el('input', { type: 'text', maxlength: '60', value: i.codigo_proveedor || '', placeholder: 'LCSC C2057152 · Mouser 709-SD15B05' });
|
|
||||||
f.notas = el('textarea', { rows: '2' }); f.notas.value = i.notas || '';
|
|
||||||
const g = el('div', { class: 'dos-col' });
|
|
||||||
g.append(campo('Código (único)', f.codigo), campo('Categoría', f.categoria));
|
|
||||||
c.append(g, campo('Descripción', f.descripcion), campo('Aplica a (versión de tarjeta)', f.aplica));
|
|
||||||
const g2 = el('div', { class: 'dos-col' }); g2.append(campo('Unidad', f.unidad), campo('Mínimo (0 = sin alerta)', f.minimo), campo('Ubicación', f.ubicacion), campo('Proveedor', f.proveedor), campo('Código del proveedor', f.codigo_proveedor)); c.append(g2);
|
|
||||||
if (!i.id) { f.stock_inicial = el('input', { type: 'number', min: '0', step: '1', value: '0' }); c.append(campo('Stock inicial (queda registrado como entrada)', f.stock_inicial)); }
|
|
||||||
c.append(campo('Notas', f.notas));
|
|
||||||
return () => {
|
|
||||||
const d = { codigo: f.codigo.value.trim().toUpperCase(), descripcion: f.descripcion.value.trim(), categoria: f.categoria.value, unidad: f.unidad.value,
|
|
||||||
aplica_a: [...f.aplica.querySelectorAll('input:checked')].map(x => x.value), minimo: +f.minimo.value || 0, ubicacion: f.ubicacion.value.trim(),
|
|
||||||
proveedor: f.proveedor.value.trim(), codigo_proveedor: f.codigo_proveedor.value.trim(), notas: f.notas.value.trim() };
|
|
||||||
if (f.stock_inicial) d.stock_inicial = +f.stock_inicial.value || 0;
|
|
||||||
return d;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function nuevo() {
|
|
||||||
let leer;
|
|
||||||
abrirDialogo('Nuevo ítem', c => { leer = formItem(c); }, [['Crear ítem', async () => {
|
|
||||||
const d = leer(); if (!d.codigo) throw new Error('Escribe el código.'); if (!d.descripcion) throw new Error('Escribe la descripción.');
|
|
||||||
await api('/api/inventario', json('POST', d)); dlg.close(); mensaje('Ítem ' + d.codigo + ' creado.', true); cargar();
|
|
||||||
}]]);
|
|
||||||
}
|
|
||||||
function editar(i) {
|
|
||||||
let leer;
|
|
||||||
abrirDialogo('Editar ' + i.codigo, c => {
|
|
||||||
leer = formItem(c, i);
|
|
||||||
const h = el('div', { class: 'hist' });
|
|
||||||
h.append(el('div', { html: '<b>Creado</b> por ' + esc(i.creado_por) + ' el ' + esc(fmt(i.creado_en)) + ' · stock actual <b>' + esc(i.stock) + ' ' + esc(i.unidad) + '</b> (se cambia con movimientos, no aquí)' }));
|
|
||||||
(i.historial || []).slice().reverse().slice(0, 12).forEach(x => h.append(el('div', { html: esc(fmt(x.cuando)) + ' · <b>' + esc(x.quien) + '</b> · ' + esc(x.cambio) })));
|
|
||||||
c.append(h);
|
|
||||||
}, [['Guardar', async () => {
|
|
||||||
const d = leer(); const cambios = {};
|
|
||||||
Object.keys(d).forEach(k => { if (JSON.stringify(d[k]) !== JSON.stringify(i[k] == null ? (Array.isArray(d[k]) ? [] : (typeof d[k] === 'number' ? 0 : '')) : i[k])) cambios[k] = d[k]; });
|
|
||||||
if (Object.keys(cambios).length) await api('/api/inventario/item?id=' + encodeURIComponent(i.id), json('PATCH', cambios));
|
|
||||||
dlg.close(); mensaje('Ítem guardado.', true); cargar();
|
|
||||||
}]]);
|
|
||||||
}
|
|
||||||
async function verMovimientos(i) {
|
|
||||||
abrirDialogo('Movimientos de ' + i.codigo, async c => {
|
|
||||||
c.append(el('p', { class: 'sub', html: esc(i.descripcion) + ' · stock <b>' + esc(i.stock) + ' ' + esc(i.unidad) + '</b>' + (i.minimo ? ' · mínimo ' + esc(i.minimo) : '') }));
|
|
||||||
const lista = el('div', { text: 'Cargando…' }); c.append(lista);
|
|
||||||
const b = el('button', { text: '+ Movimiento', style: 'margin-top:10px' }); b.addEventListener('click', () => movimiento(i)); c.append(b);
|
|
||||||
try {
|
|
||||||
const j = await api('/api/inventario/movimientos?item_id=' + encodeURIComponent(i.id));
|
|
||||||
lista.innerHTML = '';
|
|
||||||
if (!j.total) { lista.append(el('div', { class: 'vacio', text: 'Sin movimientos. El stock es 0 hasta que registres una entrada o un ajuste.' })); return; }
|
|
||||||
lista.append(el('div', { class: 'mov-fila', style: 'font-weight:700;color:var(--tinta2);font-size:11.5px;text-transform:uppercase' }, el('span', { text: 'Cuándo · quién' }), el('span', { text: 'Tipo' }), el('span', { text: 'Δ · stock' }), el('span', { text: 'Motivo · referencia' })));
|
|
||||||
j.movimientos.forEach(m => {
|
|
||||||
lista.append(el('div', { class: 'mov-fila' },
|
|
||||||
el('span', { html: esc(fmt(m.cuando)) + '<br><b>' + esc(m.quien) + '</b>' }),
|
|
||||||
el('span', { text: m.tipo + (m.tipo === 'ajuste' ? ' → ' + m.cantidad : '') }),
|
|
||||||
el('span', { class: 'd', html: '<span class="' + (m.delta >= 0 ? 'pos' : 'neg') + '">' + (m.delta >= 0 ? '+' : '') + esc(m.delta) + '</span> · ' + esc(m.stock_despues) }),
|
|
||||||
el('span', { html: esc(m.motivo) + (m.referencia ? '<br><span class="mini">ref. ' + esc(m.referencia) + '</span>' : '') })));
|
|
||||||
});
|
|
||||||
} catch (e) { lista.textContent = 'No se pudieron leer los movimientos: ' + e.message; }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
$('bMov').addEventListener('click', () => movimiento(null));
|
|
||||||
$('bNuevo').addEventListener('click', nuevo);
|
|
||||||
cargar();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
33
docs/js/sitio.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/* Sitio Drogheda: detalles que necesitan datos del servidor en todas las páginas.
|
||||||
|
- Junto a «Pendientes» en el menú lateral, cuántos hay abiertos (de /api/tareas/resumen).
|
||||||
|
Si el servidor no responde, el menú queda igual. */
|
||||||
|
(function () {
|
||||||
|
function cuentaPendientes() {
|
||||||
|
var ligas = [].slice.call(document.querySelectorAll('.md-nav--primary a.md-nav__link')).filter(function (a) {
|
||||||
|
try { return /\/tableros\/pendientes\/?$/.test(new URL(a.href, location.href).pathname); } catch (e) { return false; }
|
||||||
|
});
|
||||||
|
if (!ligas.length) return;
|
||||||
|
fetch('/api/tareas/resumen', { cache: 'no-store', credentials: 'same-origin' })
|
||||||
|
.then(function (r) { return r.ok ? r.json() : null; })
|
||||||
|
.then(function (j) {
|
||||||
|
if (!j || !j.ok || !j.por_pista) return;
|
||||||
|
var abiertos = 0;
|
||||||
|
Object.keys(j.por_pista).forEach(function (p) {
|
||||||
|
var e = j.por_pista[p] || {};
|
||||||
|
abiertos += (e['por-hacer'] || 0) + (e['en-curso'] || 0) + (e['bloqueado'] || 0);
|
||||||
|
});
|
||||||
|
var vencidos = (j.vencidas || []).length;
|
||||||
|
ligas.forEach(function (a) {
|
||||||
|
if (a.querySelector('.hg-cuenta')) return;
|
||||||
|
var s = document.createElement('span');
|
||||||
|
s.className = 'hg-cuenta' + (vencidos ? ' hg-cuenta--vencidos' : '');
|
||||||
|
s.textContent = abiertos;
|
||||||
|
s.title = abiertos + ' abiertos' + (vencidos ? ', ' + vencidos + ' vencido' + (vencidos > 1 ? 's' : '') : '');
|
||||||
|
a.appendChild(s);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(function () {});
|
||||||
|
}
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', cuentaPendientes);
|
||||||
|
else cuentaPendientes();
|
||||||
|
})();
|
||||||
43
docs/js/tema.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/* Modo claro u oscuro en las páginas sueltas (actas, cuenta, administración).
|
||||||
|
Lee la misma preferencia que el sitio (Material la guarda en localStorage con la
|
||||||
|
clave "<scope>.__palette"; scope = extra.scope de mkdocs.yml = "/drogheda/").
|
||||||
|
Oscuro por omisión. Va en el <head> de cada página, antes de su hoja de estilo,
|
||||||
|
para que no parpadee. Agrega un botón de modo al encabezado de la página. */
|
||||||
|
(function () {
|
||||||
|
var CLAVE = '/drogheda/.__palette';
|
||||||
|
var html = document.documentElement;
|
||||||
|
function leer() {
|
||||||
|
try { var p = JSON.parse(localStorage.getItem(CLAVE) || 'null'); return p && p.color && p.color.scheme === 'default' ? 'claro' : 'oscuro'; }
|
||||||
|
catch (e) { return 'oscuro'; }
|
||||||
|
}
|
||||||
|
function aplicar(modo) { if (modo === 'claro') html.setAttribute('data-tema', 'claro'); else html.removeAttribute('data-tema'); }
|
||||||
|
function guardar(modo) {
|
||||||
|
// mismo formato que Material: índice 0 = oscuro (slate), 1 = claro (default)
|
||||||
|
var p = { index: modo === 'claro' ? 1 : 0, color: { media: '', scheme: modo === 'claro' ? 'default' : 'slate', primary: 'custom', accent: 'custom' } };
|
||||||
|
try { localStorage.setItem(CLAVE, JSON.stringify(p)); } catch (e) {}
|
||||||
|
}
|
||||||
|
aplicar(leer());
|
||||||
|
var SOL = '<path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM12 2.8v2.2M12 19v2.2M4.9 4.9l1.6 1.6M17.5 17.5l1.6 1.6M2.8 12H5M19 12h2.2M4.9 19.1l1.6-1.6M17.5 6.5l1.6-1.6"/>';
|
||||||
|
var LUNA = '<path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z"/>';
|
||||||
|
function boton() {
|
||||||
|
var cab = document.querySelector('header.top');
|
||||||
|
if (!cab || cab.querySelector('.hg-modo')) return;
|
||||||
|
var b = document.createElement('button');
|
||||||
|
b.type = 'button'; b.className = 'hg-modo';
|
||||||
|
function pintar() {
|
||||||
|
var claro = html.getAttribute('data-tema') === 'claro';
|
||||||
|
b.setAttribute('aria-label', claro ? 'Cambiar a modo oscuro' : 'Cambiar a modo claro');
|
||||||
|
b.title = b.getAttribute('aria-label');
|
||||||
|
b.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + (claro ? LUNA : SOL) + '</svg>';
|
||||||
|
}
|
||||||
|
b.addEventListener('click', function () {
|
||||||
|
var modo = html.getAttribute('data-tema') === 'claro' ? 'oscuro' : 'claro';
|
||||||
|
aplicar(modo); guardar(modo); pintar();
|
||||||
|
});
|
||||||
|
pintar();
|
||||||
|
cab.appendChild(b);
|
||||||
|
}
|
||||||
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boton); else boton();
|
||||||
|
// si la persona cambia el modo en otra pestaña del sitio, esta página lo sigue
|
||||||
|
window.addEventListener('storage', function (e) { if (e.key === CLAVE) aplicar(leer()); });
|
||||||
|
})();
|
||||||
@ -15,7 +15,7 @@ version: 1.2
|
|||||||
- Tiempo: acta de envío o recepción 15–30 min por unidad.
|
- Tiempo: acta de envío o recepción 15–30 min por unidad.
|
||||||
- Necesitas: los [formularios](../actas/index.md); para recepción, fuente de 24 V (≥ 1 A; límite en 1 A si lo tiene), multímetro, cable USB con datos y laptop.
|
- Necesitas: los [formularios](../actas/index.md); para recepción, fuente de 24 V (≥ 1 A; límite en 1 A si lo tiene), multímetro, cable USB con datos y laptop.
|
||||||
|
|
||||||
Todas las actas cuelgan del número de serie; el [expediente](../unidad.html) de cualquier tarjeta, módulo o unidad cuenta la historia completa, daños incluidos.
|
Todas las actas cuelgan del número de serie; el [expediente](../tableros/unidad.md) de cualquier tarjeta, módulo o unidad cuenta la historia completa, daños incluidos.
|
||||||
|
|
||||||
## Mapa
|
## Mapa
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ version: 1.6
|
|||||||
|
|
||||||
!!! abstract "Para quién · Cuándo · Tiempo · Necesitas"
|
!!! abstract "Para quién · Cuándo · Tiempo · Necesitas"
|
||||||
- Para quién: técnico de banco.
|
- Para quién: técnico de banco.
|
||||||
- Cuándo: una vez por tarjeta V4 (EinarBoard 2.0, Blackpill en zócalo), antes de darla por buena con el firmware de campo. Sin serie: etiquétala V4-NN con el siguiente libre del [tablero](../tablero.html).
|
- Cuándo: una vez por tarjeta V4 (EinarBoard 2.0, Blackpill en zócalo), antes de darla por buena con el firmware de campo. Sin serie: etiquétala V4-NN con el siguiente libre del [tablero](../tableros/actas.md).
|
||||||
- Tiempo: ~1 h por tarjeta, más ~20 min en batería.
|
- Tiempo: ~1 h por tarjeta, más ~20 min en batería.
|
||||||
- Necesitas: tarjeta V4 completa (Blackpill, simpleRTK2B, módem con SIM, antena GNSS con cielo, antena LTE, batería); 24 V al GX16 que puedas desconectar; diodo Schottky BAT54 (como D5), BAT85, BAT43 o 1N5711, nunca 1N5819; alambre wire-wrap, cautín fino, flux, Kapton, termofil, lupa; multímetro con MIN/MAX; laptop con terminal serie y `dfu-util` o STM32CubeProgrammer; dos cables USB-A a USB-C (uno normal y uno sin 5 V, Preparación 2); cargador Li-ion de 1 celda o fuente de banco; monitor y [acta de prueba V4](../formularios/v4.html) abierta desde el inicio (se guarda sola en el navegador).
|
- Necesitas: tarjeta V4 completa (Blackpill, simpleRTK2B, módem con SIM, antena GNSS con cielo, antena LTE, batería); 24 V al GX16 que puedas desconectar; diodo Schottky BAT54 (como D5), BAT85, BAT43 o 1N5711, nunca 1N5819; alambre wire-wrap, cautín fino, flux, Kapton, termofil, lupa; multímetro con MIN/MAX; laptop con terminal serie y `dfu-util` o STM32CubeProgrammer; dos cables USB-A a USB-C (uno normal y uno sin 5 V, Preparación 2); cargador Li-ion de 1 celda o fuente de banco; monitor y [acta de prueba V4](../formularios/v4.html) abierta desde el inicio (se guarda sola en el navegador).
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ Pasa si en los dos casos el módem registra solo en < 2 min y el monitor vuelve
|
|||||||
|
|
||||||
## Registro
|
## Registro
|
||||||
|
|
||||||
Todo va en el [acta de prueba V4](../formularios/v4.html), con el log completo de consola. Cada etapa termina con una pregunta de paso; si la tarjeta no pasa, marca "me detengo", llena el cierre y envía con resultado FALLA. Queda en el [expediente](../unidad.html) y en el [tablero](../tablero.html).
|
Todo va en el [acta de prueba V4](../formularios/v4.html), con el log completo de consola. Cada etapa termina con una pregunta de paso; si la tarjeta no pasa, marca "me detengo", llena el cierre y envía con resultado FALLA. Queda en el [expediente](../tableros/unidad.md) y en el [tablero](../tableros/actas.md).
|
||||||
|
|
||||||
!!! danger "Detente y avisa a ingeniería"
|
!!! danger "Detente y avisa a ingeniería"
|
||||||
- P2 en 0 V con 24 V puestos.
|
- P2 en 0 V con 24 V puestos.
|
||||||
|
|||||||
@ -4,7 +4,7 @@ title: "Producción"
|
|||||||
|
|
||||||
# Producción
|
# Producción
|
||||||
|
|
||||||
De la tarjeta desnuda a la unidad lista para embarcar. Una unidad a la vez; cada paso deja su acta y la unidad no avanza si falla un paso. Plan y avance real: [Tablero de producción](../tableros/produccion.html). Existencias: [Inventario](../inventario/inventario.html).
|
De la tarjeta desnuda a la unidad lista para embarcar. Una unidad a la vez; cada paso deja su acta y la unidad no avanza si falla un paso. Plan y avance real: [Tablero de producción](../tableros/produccion.md). Existencias: [Inventario](../tableros/inventario.md).
|
||||||
|
|
||||||
## Plan de 570 unidades (13-oct-2026)
|
## Plan de 570 unidades (13-oct-2026)
|
||||||
|
|
||||||
@ -36,10 +36,10 @@ La V4 no entra al pipeline de unidades V5; se prepara y prueba en [Taller](../ta
|
|||||||
|
|
||||||
## Dónde ver el avance
|
## Dónde ver el avance
|
||||||
|
|
||||||
- [Tablero de producción](../tableros/produccion.html): plan 113 / 327 / 130 contra las unidades reales por etapa; bloqueos.
|
- [Tablero de producción](../tableros/produccion.md): plan 113 / 327 / 130 contra las unidades reales por etapa; bloqueos.
|
||||||
- [Estado de actas](../tablero.html): qué tarjeta, módulo y unidad pasó cada paso (series V5-NN, ARD-NNN, RTK-NNNN).
|
- [Estado de actas](../tableros/actas.md): qué tarjeta, módulo y unidad pasó cada paso (series V5-NN, ARD-NNN, RTK-NNNN).
|
||||||
- [Expediente de una unidad](../unidad.html): la historia completa de una serie.
|
- [Expediente de una unidad](../tableros/unidad.md): la historia completa de una serie.
|
||||||
- [Inventario](../inventario/index.md): tarjetas, módulos, gabinetes, cajas, paquetes, fuentes; mínimos y alertas.
|
- [Inventario](../inventario/index.md): tarjetas, módulos, gabinetes, cajas, paquetes, fuentes; mínimos y alertas.
|
||||||
- [Kanban](../tableros/kanban.html): tareas de la pista Producción y Compras.
|
- [Pendientes](../tableros/pendientes.md): tareas de la pista Producción y Compras.
|
||||||
- [Descargas](../descargas/index.md): firmware vigente por tarjeta y SHA-256. Hoy: 0.6.0 CAMPO; 0.6.3 solo banco.
|
- [Descargas](../descargas/index.md): firmware vigente por tarjeta y SHA-256. Hoy: 0.6.0 CAMPO; 0.6.3 solo banco.
|
||||||
- [Índice de actas](../actas/index.md): cómo se llenan y qué registra cada una.
|
- [Índice de actas](../actas/index.md): cómo se llenan y qué registra cada una.
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Cerrar sesión — HBOX</title>
|
<title>Cerrar sesión — HBOX</title>
|
||||||
|
<script src="js/tema.js"></script>
|
||||||
<link rel="stylesheet" href="formularios/acta.css">
|
<link rel="stylesheet" href="formularios/acta.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -12,7 +13,7 @@
|
|||||||
<section class="paso">
|
<section class="paso">
|
||||||
<p id="estado">Cerrando sesión…</p>
|
<p id="estado">Cerrando sesión…</p>
|
||||||
<div id="listo" style="display:none">
|
<div id="listo" style="display:none">
|
||||||
<p><b>✓ Sesión cerrada.</b></p>
|
<p><b>Sesión cerrada.</b></p>
|
||||||
<p>Para entrar con otro usuario, abre de nuevo el sitio y aparecerá la ventana de
|
<p>Para entrar con otro usuario, abre de nuevo el sitio y aparecerá la ventana de
|
||||||
inicio de sesión.</p>
|
inicio de sesión.</p>
|
||||||
<p style="color:var(--tinta2);font-size:14px">Si por seguridad del navegador la ventana
|
<p style="color:var(--tinta2);font-size:14px">Si por seguridad del navegador la ventana
|
||||||
|
|||||||
@ -27,10 +27,10 @@ Un sitio es un puerto o terminal donde hay unidades HBOX instaladas (hoy: ICAVE,
|
|||||||
|
|
||||||
| Sitio | Puerto | Operador | Página |
|
| Sitio | Puerto | Operador | Página |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **ICAVE** | Veracruz | Hutchison Ports | [Abrir](sitio.html?id=icave) |
|
| **ICAVE** | Veracruz | Hutchison Ports | [Abrir](../tableros/sitio.md?id=icave) |
|
||||||
| **LZC** | Lázaro Cárdenas | Hutchison Ports | [Abrir](sitio.html?id=lzc) |
|
| **LZC** | Lázaro Cárdenas | Hutchison Ports | [Abrir](../tableros/sitio.md?id=lzc) |
|
||||||
| **EIT** | Ensenada | Hutchison Ports | [Abrir](sitio.html?id=eit) |
|
| **EIT** | Ensenada | Hutchison Ports | [Abrir](../tableros/sitio.md?id=eit) |
|
||||||
| **TIMSA** | Manzanillo | Hutchison Ports | [Abrir](sitio.html?id=timsa) |
|
| **TIMSA** | Manzanillo | Hutchison Ports | [Abrir](../tableros/sitio.md?id=timsa) |
|
||||||
|
|
||||||
En notas antiguas Lázaro Cárdenas aparece como «LCT»; el nombre oficial en el sitio es **LZC**.
|
En notas antiguas Lázaro Cárdenas aparece como «LCT»; el nombre oficial en el sitio es **LZC**.
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ En notas antiguas Lázaro Cárdenas aparece como «LCT»; el nombre oficial en e
|
|||||||
|
|
||||||
## Dar de alta un sitio (administrador)
|
## Dar de alta un sitio (administrador)
|
||||||
|
|
||||||
1. En la página de cualquier sitio, elige **+ Nuevo sitio…** en el selector, o entra a [`sitio.html?nuevo=1`](sitio.html?nuevo=1).
|
1. En la página de cualquier sitio, elige **+ Nuevo sitio…** en el selector, o entra a [`sitio.html?nuevo=1`](../tableros/sitio.md?nuevo=1).
|
||||||
2. Id corto en minúsculas (`icave`, `lzc`), nombre como lo dice el cliente, cliente, ubicación.
|
2. Id corto en minúsculas (`icave`, `lzc`), nombre como lo dice el cliente, cliente, ubicación.
|
||||||
3. **Alias**: los nombres con los que ese sitio aparece en actas viejas (por ejemplo `LCT` para LZC), para que la búsqueda por texto las encuentre.
|
3. **Alias**: los nombres con los que ese sitio aparece en actas viejas (por ejemplo `LCT` para LZC), para que la búsqueda por texto las encuentre.
|
||||||
4. Contactos: nombre, rol, teléfono, correo. Guarda. El sitio aparece de inmediato en los formularios.
|
4. Contactos: nombre, rol, teléfono, correo. Guarda. El sitio aparece de inmediato en los formularios.
|
||||||
@ -59,6 +59,6 @@ Un sitio no se borra: se marca **inactivo** y deja de salir en los formularios,
|
|||||||
## Relacionados
|
## Relacionados
|
||||||
|
|
||||||
- [Acta de instalación en campo](../formularios/instalacion.html) — una por unidad y vehículo.
|
- [Acta de instalación en campo](../formularios/instalacion.html) — una por unidad y vehículo.
|
||||||
- [Tablero de campo](../diagnostico/tablero-campo.html) — diagnósticos por unidad.
|
- [Tablero de campo](../tableros/campo.md) — diagnósticos por unidad.
|
||||||
- [Expediente de una unidad](../unidad.html) — producción → envío → instalación → campo.
|
- [Expediente de una unidad](../tableros/unidad.md) — producción → envío → instalación → campo.
|
||||||
- [Pendientes de campo](../tableros/kanban.html?pista=campo)
|
- [Pendientes de campo](../tableros/pendientes.md?pista=campo)
|
||||||
|
|||||||
@ -1,204 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
<!-- Desde el 20-oct-2026 este tablero vive dentro del sitio: docs/tableros/sitio.md → /tableros/sitio/
|
||||||
|
Este archivo solo redirige (conserva ?serie=, ?id=, ?pista=… y #…) para que las ligas viejas
|
||||||
|
guardadas en pendientes y actas sigan sirviendo. No se edita: los cambios van en tableros/sitio.md. -->
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Sitio — HBOX</title>
|
<meta name="robots" content="noindex">
|
||||||
<link rel="stylesheet" href="../tableros/tablero.css">
|
<title>Sitios - Drogheda</title>
|
||||||
<style>
|
<script>location.replace('/tableros/sitio/' + location.search + location.hash);</script>
|
||||||
.ficha{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin-bottom:14px}
|
<meta http-equiv="refresh" content="0; url=/tableros/sitio/">
|
||||||
.ficha .c{background:var(--tarjeta);border:1px solid var(--linea);border-radius:10px;padding:10px 12px}
|
<style>body{margin:0;padding:24px;background:#0E1524;color:#E7ECF5;font:16px/1.5 system-ui,sans-serif}a{color:#7FE9D8}</style>
|
||||||
.ficha .l{font-size:11px;color:var(--tinta2);text-transform:uppercase;letter-spacing:.05em}
|
|
||||||
.ficha .v{font-weight:600;font-size:15px}
|
|
||||||
.contacto{padding:6px 0;border-top:1px dashed var(--linea);font-size:14px}
|
|
||||||
.contacto:first-of-type{border-top:0}
|
|
||||||
.contacto .r{color:var(--tinta2);font-size:12.5px}
|
|
||||||
.linea{position:relative;padding-left:18px;border-left:2px solid var(--linea);margin-left:6px}
|
|
||||||
.ev{position:relative;padding:9px 0 9px 4px;border-bottom:1px solid var(--linea);font-size:14px}
|
|
||||||
.ev:last-child{border-bottom:0}
|
|
||||||
.ev::before{content:"";position:absolute;left:-25px;top:15px;width:11px;height:11px;border-radius:50%;background:var(--pend);border:2px solid var(--papel)}
|
|
||||||
.ev.instalacion-campo::before{background:var(--ok)} .ev.diagnostico-campo::before{background:var(--naranja)}
|
|
||||||
.ev.dano::before{background:var(--falla)} .ev.envio::before,.ev.recepcion::before{background:var(--info)}
|
|
||||||
.ev .f{color:var(--tinta2);font-size:12.5px}
|
|
||||||
.sitio-sel{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px}
|
|
||||||
.contactos-ed .fila{display:grid;grid-template-columns:1.2fr 1fr 1fr 1.2fr auto;gap:6px;margin-bottom:6px}
|
|
||||||
@media(max-width:700px){.contactos-ed .fila{grid-template-columns:1fr 1fr}}
|
|
||||||
.contactos-ed .fila button{padding:6px 9px;font-size:13px}
|
|
||||||
.por-texto{font-size:11px;color:var(--tinta2);border:1px dashed var(--linea);border-radius:99px;padding:0 7px}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<p>Este tablero ahora está dentro del sitio: <a href="/tableros/sitio/">Sitios</a>.</p>
|
||||||
<header class="top">
|
|
||||||
<div><h1 id="titulo">Sitio</h1><div class="meta" id="meta">Cargando…</div></div>
|
|
||||||
<nav>
|
|
||||||
<a href="../">← Documentación</a>
|
|
||||||
<a href="./">Sitios</a>
|
|
||||||
<a href="../formularios/instalacion.html">Nueva instalación</a>
|
|
||||||
<a href="../formularios/diagnostico.html">Diagnóstico</a>
|
|
||||||
<a href="../tableros/kanban.html?pista=campo">Pendientes de campo</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<details class="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="./">lista de sitios</a> → «Nuevo sitio» (administrador).</li>
|
|
||||||
</ol>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div class="sitio-sel"><label style="margin:0">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>
|
|
||||||
</div>
|
|
||||||
<dialog id="dlg"></dialog>
|
|
||||||
<script>
|
|
||||||
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', '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 + ' — sitio HBOX';
|
|
||||||
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);
|
|
||||||
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="../unidad.html?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="../unidad.html?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="../unidad.html?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', text: '✕' }); q.addEventListener('click', () => r.remove()); r.append(q); ced.append(r); }
|
|
||||||
(s.contactos || []).forEach(fila);
|
|
||||||
const add = el('button', { type: 'button', class: 'sec', text: '+ 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', text: '✕' }); 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>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,165 +1,443 @@
|
|||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
HBOX — Documentación técnica · hoja de estilo Helgen
|
Drogheda · piel Helgen para Material for MkDocs
|
||||||
Colores tomados de _brand/estilo-helgen.tex (\definecolor):
|
Los colores y las letras vienen de helgen.css (tokens --hg-*). Aquí solo se
|
||||||
HelgenIndigo #2C3C5B · HelgenCyan #51F3DA · HelgenTeal #17A08C
|
conectan con las variables de Material y se ajusta el diseño:
|
||||||
HelgenMist #EFF1F5 · HelgenLightCyan #E7FCF8 · HelgenLightIndigo #EDEFF4
|
barra lateral fija a la izquierda (como un proyecto de Notion), columna de
|
||||||
|
lectura centrada y tableros a todo lo ancho (plantilla tablero.html).
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
:root {
|
/* ---- Variables de Material a partir de los tokens ------------------------ */
|
||||||
--helgen-indigo: #2C3C5B;
|
[data-md-color-scheme="slate"],
|
||||||
--helgen-indigo-dark: #1F2A44;
|
|
||||||
--helgen-indigo-light: #3D5078;
|
|
||||||
--helgen-cyan: #51F3DA;
|
|
||||||
--helgen-teal: #17A08C;
|
|
||||||
--helgen-mist: #EFF1F5;
|
|
||||||
--helgen-light-cyan: #E7FCF8;
|
|
||||||
--helgen-light-indigo: #EDEFF4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Paleta "custom" de Material ---------------------------------------- */
|
|
||||||
[data-md-color-primary="custom"] {
|
|
||||||
--md-primary-fg-color: var(--helgen-indigo);
|
|
||||||
--md-primary-fg-color--light: var(--helgen-indigo-light);
|
|
||||||
--md-primary-fg-color--dark: var(--helgen-indigo-dark);
|
|
||||||
--md-primary-bg-color: #ffffff;
|
|
||||||
--md-primary-bg-color--light: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
[data-md-color-accent="custom"] {
|
|
||||||
--md-accent-fg-color: var(--helgen-teal);
|
|
||||||
--md-accent-fg-color--transparent: rgba(23, 160, 140, 0.1);
|
|
||||||
--md-accent-bg-color: #ffffff;
|
|
||||||
--md-accent-bg-color--light: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
[data-md-color-scheme="default"] {
|
[data-md-color-scheme="default"] {
|
||||||
--md-typeset-a-color: #14685c;
|
--md-default-bg-color: var(--hg-fondo);
|
||||||
}
|
--md-default-bg-color--light: color-mix(in srgb, var(--hg-fondo) 54%, transparent);
|
||||||
[data-md-color-scheme="slate"][data-md-color-accent="custom"] {
|
--md-default-bg-color--lighter: color-mix(in srgb, var(--hg-fondo) 26%, transparent);
|
||||||
--md-accent-fg-color: var(--helgen-cyan);
|
--md-default-bg-color--lightest: color-mix(in srgb, var(--hg-fondo) 7%, transparent);
|
||||||
--md-accent-fg-color--transparent: rgba(81, 243, 218, 0.12);
|
--md-default-fg-color: var(--hg-texto);
|
||||||
--md-accent-bg-color: var(--helgen-indigo-dark);
|
--md-default-fg-color--light: var(--hg-texto-2);
|
||||||
}
|
--md-default-fg-color--lighter: var(--hg-texto-3);
|
||||||
[data-md-color-scheme="slate"] {
|
--md-default-fg-color--lightest: var(--hg-linea);
|
||||||
--md-typeset-a-color: var(--helgen-cyan);
|
|
||||||
--md-default-bg-color: #161d2b;
|
--md-primary-fg-color: var(--hg-cabecera);
|
||||||
--md-code-bg-color: #1c2536;
|
--md-primary-fg-color--light: var(--hg-indigo);
|
||||||
|
--md-primary-fg-color--dark: var(--hg-barra);
|
||||||
|
--md-primary-bg-color: var(--hg-sobre-cabecera);
|
||||||
|
--md-primary-bg-color--light: color-mix(in srgb, var(--hg-sobre-cabecera) 70%, transparent);
|
||||||
|
|
||||||
|
--md-accent-fg-color: var(--hg-liga-hover);
|
||||||
|
--md-accent-fg-color--transparent: var(--hg-acento-suave);
|
||||||
|
--md-accent-bg-color: var(--hg-plano);
|
||||||
|
--md-accent-bg-color--light: var(--hg-plano-2);
|
||||||
|
|
||||||
|
--md-typeset-color: var(--hg-texto);
|
||||||
|
--md-typeset-a-color: var(--hg-liga);
|
||||||
|
--md-typeset-mark-color: color-mix(in srgb, var(--hg-aviso) 30%, transparent);
|
||||||
|
--md-typeset-table-color: var(--hg-linea);
|
||||||
|
--md-typeset-table-color--light: color-mix(in srgb, var(--hg-elevado) 45%, transparent);
|
||||||
|
--md-typeset-kbd-color: var(--hg-plano);
|
||||||
|
--md-typeset-kbd-accent-color: var(--hg-linea-2);
|
||||||
|
--md-typeset-kbd-border-color: var(--hg-linea);
|
||||||
|
|
||||||
|
--md-code-fg-color: var(--hg-texto);
|
||||||
|
--md-code-bg-color: var(--hg-plano);
|
||||||
|
--md-code-bg-color--light: var(--hg-plano-2);
|
||||||
|
--md-code-bg-color--lighter: var(--hg-plano-2);
|
||||||
|
|
||||||
|
--md-admonition-fg-color: var(--hg-texto);
|
||||||
|
--md-admonition-bg-color: var(--hg-plano);
|
||||||
|
|
||||||
|
--md-footer-fg-color: var(--hg-texto);
|
||||||
|
--md-footer-fg-color--light: var(--hg-texto-2);
|
||||||
|
--md-footer-fg-color--lighter: var(--hg-texto-3);
|
||||||
|
--md-footer-bg-color: var(--hg-barra);
|
||||||
|
--md-footer-bg-color--dark: var(--hg-barra);
|
||||||
|
|
||||||
|
--md-shadow-z1: none;
|
||||||
|
--md-shadow-z2: var(--hg-sombra-suave);
|
||||||
|
--md-shadow-z3: var(--hg-sombra);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fuentes del sistema (sitio utilizable sin Internet) */
|
|
||||||
:root {
|
:root {
|
||||||
--md-text-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
--hg-ancho-barra: 14rem;
|
||||||
--md-code-font: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
--md-text-font: "Atkinson Hyperlegible Next";
|
||||||
|
--md-code-font: "Atkinson Hyperlegible Mono";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Tamaño: pensado para tableta en el taller -------------------------- */
|
body {
|
||||||
.md-grid { max-width: 1400px; }
|
background-color: var(--hg-fondo);
|
||||||
.md-typeset { font-size: 0.85rem; line-height: 1.6; }
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible { outline: 2px solid var(--hg-foco); outline-offset: 2px; }
|
||||||
|
|
||||||
|
/* Íconos de línea: Material pinta los SVG con relleno; los nuestros son trazo. */
|
||||||
|
svg.hg-i { fill: none !important; stroke: currentColor; }
|
||||||
|
|
||||||
|
/* ---- Encabezado ---------------------------------------------------------- */
|
||||||
|
.md-header {
|
||||||
|
background-color: var(--hg-cabecera);
|
||||||
|
color: var(--hg-sobre-cabecera);
|
||||||
|
border-bottom: 1px solid var(--hg-linea-suave);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
[data-md-color-scheme="default"] .md-header { border-bottom-color: transparent; }
|
||||||
|
.md-header--shadow { box-shadow: none; }
|
||||||
|
|
||||||
|
.md-header__button.md-logo { margin: 0.2rem 0.1rem 0.2rem 0.35rem; padding: 0.2rem; }
|
||||||
|
.md-header__button.md-logo svg,
|
||||||
|
.md-nav__title .md-nav__button.md-logo svg { width: auto; height: 1.6rem; }
|
||||||
|
|
||||||
|
.md-header__title { margin-left: 0.3rem; }
|
||||||
|
.md-header__topic:first-child {
|
||||||
|
font-family: var(--hg-fuente-titulo);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
.md-header__topic + .md-header__topic { font-weight: 600; font-size: 0.8rem; }
|
||||||
|
|
||||||
|
/* búsqueda */
|
||||||
|
@media screen and (min-width: 60em) {
|
||||||
|
.md-search__form {
|
||||||
|
height: 1.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--hg-plano);
|
||||||
|
border: 1px solid var(--hg-linea);
|
||||||
|
}
|
||||||
|
.md-search__form:hover { background-color: var(--hg-elevado); }
|
||||||
|
[data-md-color-scheme="default"] .md-search__form {
|
||||||
|
background-color: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
[data-md-color-scheme="default"] .md-search__form:hover { background-color: rgba(255, 255, 255, 0.18); }
|
||||||
|
[data-md-color-scheme="default"] .md-search__input,
|
||||||
|
[data-md-color-scheme="default"] .md-search__input::placeholder,
|
||||||
|
[data-md-color-scheme="default"] .md-search__input ~ .md-search__icon { color: rgba(255, 255, 255, 0.82); }
|
||||||
|
[data-md-toggle="search"]:checked ~ .md-header .md-search__form {
|
||||||
|
background-color: var(--hg-plano);
|
||||||
|
border-color: var(--hg-linea-2);
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
[data-md-color-scheme="default"] [data-md-toggle="search"]:checked ~ .md-header .md-search__input { color: var(--hg-texto); }
|
||||||
|
.md-search__output { border-radius: 0 0 10px 10px; }
|
||||||
|
}
|
||||||
|
.md-search__input { font-size: 0.75rem; }
|
||||||
|
.md-search__input::placeholder { color: var(--hg-texto-3); }
|
||||||
|
.md-search__output { background-color: var(--hg-plano); }
|
||||||
|
.md-search-result__meta { background-color: var(--hg-plano-2); color: var(--hg-texto-2); }
|
||||||
|
.md-search-result__item { border-top: 1px solid var(--hg-linea-suave); }
|
||||||
|
.md-search-result__link:focus,
|
||||||
|
.md-search-result__link:hover,
|
||||||
|
.md-search-result__more > summary:hover { background-color: var(--hg-elevado); }
|
||||||
|
.md-search-result mark { color: var(--hg-liga); background: none; }
|
||||||
|
|
||||||
|
/* ---- Esqueleto: barra lateral a la izquierda, columna de lectura --------- */
|
||||||
@media screen and (min-width: 76.25em) {
|
@media screen and (min-width: 76.25em) {
|
||||||
.md-typeset { font-size: 0.9rem; }
|
.md-header__inner.md-grid,
|
||||||
|
.md-main__inner.md-grid { max-width: none; margin-left: 0; margin-right: 0; }
|
||||||
|
.md-main__inner { margin-top: 0; }
|
||||||
|
|
||||||
|
/* la barra lateral se pinta en el fondo de .md-main: Material le da altura 0 a .md-sidebar */
|
||||||
|
.md-main {
|
||||||
|
background: linear-gradient(to right,
|
||||||
|
var(--hg-barra) 0, var(--hg-barra) var(--hg-ancho-barra),
|
||||||
|
var(--hg-linea-suave) var(--hg-ancho-barra), var(--hg-linea-suave) calc(var(--hg-ancho-barra) + 1px),
|
||||||
|
transparent calc(var(--hg-ancho-barra) + 1px));
|
||||||
|
}
|
||||||
|
.md-sidebar--primary {
|
||||||
|
padding: 0.7rem 0 1.2rem;
|
||||||
|
width: var(--hg-ancho-barra);
|
||||||
|
}
|
||||||
|
.md-sidebar--primary .md-sidebar__scrollwrap { margin: 0 0.25rem; }
|
||||||
|
[dir=ltr] .md-sidebar--primary .md-sidebar__inner { padding-right: 0.15rem; }
|
||||||
|
/* el nombre del sitio ya está en el encabezado */
|
||||||
|
.md-nav--primary > .md-nav__title { display: none; }
|
||||||
|
.md-sidebar--secondary { padding-top: 1.6rem; }
|
||||||
|
|
||||||
|
/* la columna de lectura se centra en el espacio libre */
|
||||||
|
[dir=ltr] .md-sidebar--primary:not([hidden]) ~ .md-content > .md-content__inner,
|
||||||
|
[dir=ltr] .md-sidebar--secondary:not([hidden]) ~ .md-content > .md-content__inner,
|
||||||
|
.md-content > .md-content__inner {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.md-typeset h1 { color: var(--helgen-indigo); font-weight: 700; }
|
.md-content__inner {
|
||||||
[data-md-color-scheme="slate"] .md-typeset h1 { color: var(--helgen-cyan); }
|
max-width: var(--hg-ancho-contenido, 46rem);
|
||||||
.md-typeset h2 { font-weight: 600; border-bottom: 2px solid var(--helgen-light-indigo); padding-bottom: 0.2em; }
|
padding: 1.4rem 1.2rem 0;
|
||||||
[data-md-color-scheme="slate"] .md-typeset h2 { border-bottom-color: var(--helgen-indigo-light); }
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 76.234375em) {
|
||||||
|
.md-content__inner { padding: 1rem 0.2rem 0; }
|
||||||
|
}
|
||||||
|
.md-content__inner::before { display: none; }
|
||||||
|
|
||||||
/* Cabecera y pestañas */
|
/* ---- Menú lateral -------------------------------------------------------- */
|
||||||
.md-header__button.md-logo img { height: 1.6rem; width: auto; }
|
.md-nav { font-size: 0.7rem; line-height: 1.35; }
|
||||||
.md-tabs__link { font-size: 0.75rem; font-weight: 600; }
|
.md-nav--primary .md-nav__link {
|
||||||
.md-tabs__link--active { border-bottom: 3px solid var(--helgen-cyan); }
|
margin-top: 1px;
|
||||||
|
padding: 0.32rem 0.5rem;
|
||||||
|
border-radius: var(--hg-radio-chico);
|
||||||
|
color: var(--hg-texto-suave);
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.md-nav--primary .md-nav__link svg.hg-i { width: 18px; height: 18px; margin: 0; color: var(--hg-texto-3); }
|
||||||
|
.md-nav--primary .md-nav__link[href]:hover,
|
||||||
|
.md-nav--primary .md-nav__link[for]:hover { color: var(--hg-texto-fuerte); background-color: color-mix(in srgb, var(--hg-elevado) 55%, transparent); }
|
||||||
|
.md-nav--primary .md-nav__item .md-nav__link--active,
|
||||||
|
.md-nav--primary .md-nav__item .md-nav__link--active code {
|
||||||
|
color: var(--hg-texto-fuerte);
|
||||||
|
background-color: var(--hg-elevado);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.md-nav--primary .md-nav__link--active svg.hg-i { color: var(--hg-acento); }
|
||||||
|
.md-nav--primary .md-nav__container > .md-nav__link { padding: 0; margin: 0; background: none; }
|
||||||
|
.md-nav--primary .md-nav__container { padding: 0; }
|
||||||
|
.md-nav--primary .md-nav__container > .md-nav__link:first-child { padding: 0.32rem 0.5rem; border-radius: var(--hg-radio-chico); }
|
||||||
|
.md-nav--primary .md-nav__container > .md-nav__link[for] { padding: 0.32rem 0.35rem; border-radius: var(--hg-radio-chico); }
|
||||||
|
.md-nav__icon::after { background-color: var(--hg-texto-3); }
|
||||||
|
/* lo que no tiene ícono deja el hueco del ícono, para que los nombres queden alineados */
|
||||||
|
.md-nav--primary .md-nav__link:not(.md-nav__container):not(:has(> svg))::before,
|
||||||
|
.md-nav--primary .md-nav__container > .md-nav__link:first-child:not(:has(> svg))::before {
|
||||||
|
content: "";
|
||||||
|
flex: none;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
.md-nav--primary .md-nav__container > .md-nav__link[for]::before { display: none; }
|
||||||
|
@media screen and (min-width: 76.25em) {
|
||||||
|
.md-nav--primary .md-nav__item--section > .md-nav__link::before,
|
||||||
|
.md-nav--primary .md-nav__item--section > .md-nav__container > .md-nav__link:first-child::before { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Tarjetas (grid cards) ---------------------------------------------- */
|
/* secciones de primer nivel (Tableros, Documentos, Cuenta): rótulo discreto, sin mayúsculas */
|
||||||
|
@media screen and (min-width: 76.25em) {
|
||||||
|
.md-nav--primary > .md-nav__list > .md-nav__item--section { margin: 1.1rem 0 0; }
|
||||||
|
.md-nav--primary > .md-nav__list > .md-nav__item--section > .md-nav__link,
|
||||||
|
.md-nav--primary > .md-nav__list > .md-nav__item--section > .md-nav__container > .md-nav__link:first-child {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--hg-texto-3);
|
||||||
|
padding: 0 0.5rem 0.3rem;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.md-nav--primary > .md-nav__list > .md-nav__item--section > .md-nav__container > .md-nav__link:first-child svg.hg-i { display: none; }
|
||||||
|
}
|
||||||
|
.md-nav--primary .md-nav__item--nested:not(.md-nav__item--section) > .md-nav { padding-left: 0.55rem; }
|
||||||
|
|
||||||
|
/* el campo «status:» de los documentos es texto para personas, no un ícono en el menú */
|
||||||
|
.md-nav .md-status { display: none; }
|
||||||
|
|
||||||
|
/* conteo junto a «Pendientes» (js/sitio.js) */
|
||||||
|
.hg-cuenta {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--hg-texto-3);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.hg-cuenta--vencidos::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px; height: 6px;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--hg-bloqueado);
|
||||||
|
vertical-align: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* en teléfono y tableta el menú es un cajón */
|
||||||
|
@media screen and (max-width: 76.234375em) {
|
||||||
|
.md-nav--primary,
|
||||||
|
.md-nav--primary .md-nav { background-color: var(--hg-barra); }
|
||||||
|
.md-nav--primary .md-nav__title { background-color: var(--hg-cabecera); color: var(--hg-sobre-cabecera); }
|
||||||
|
.md-nav--primary .md-nav__title .md-logo svg { height: 2rem; }
|
||||||
|
.md-nav--primary .md-nav__item { font-size: 0.75rem; }
|
||||||
|
.md-nav--primary .md-nav__link { padding: 0.45rem 0.6rem; margin: 1px 0.4rem; }
|
||||||
|
.md-nav--primary .md-nav__link.md-nav__container { padding: 0; }
|
||||||
|
.md-nav--primary .md-nav__container > .md-nav__link:first-child { padding: 0.45rem 0.6rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* índice de la página (derecha) */
|
||||||
|
.md-nav--secondary .md-nav__title { color: var(--hg-texto-3); font-weight: 600; font-size: 0.62rem; background: none; box-shadow: none; }
|
||||||
|
.md-nav--secondary .md-nav__link { color: var(--hg-texto-2); margin-top: 0.4em; }
|
||||||
|
.md-nav--secondary .md-nav__link--active { color: var(--hg-texto-fuerte); }
|
||||||
|
.md-nav--secondary .md-nav__link:hover { color: var(--hg-liga-hover); }
|
||||||
|
|
||||||
|
/* ---- Texto --------------------------------------------------------------- */
|
||||||
|
.md-typeset { font-size: 0.8rem; line-height: 1.62; color: var(--hg-texto); }
|
||||||
|
.md-typeset h1,
|
||||||
|
.md-typeset h2 {
|
||||||
|
font-family: var(--hg-fuente-titulo);
|
||||||
|
color: var(--hg-texto-fuerte);
|
||||||
|
}
|
||||||
|
.md-typeset h1 {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 2.15em;
|
||||||
|
line-height: 1.08;
|
||||||
|
letter-spacing: -0.012em;
|
||||||
|
margin: 0 0 0.8em;
|
||||||
|
}
|
||||||
|
.md-typeset h2 {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.5em;
|
||||||
|
line-height: 1.18;
|
||||||
|
letter-spacing: -0.004em;
|
||||||
|
margin: 1.9em 0 0.6em;
|
||||||
|
}
|
||||||
|
.md-typeset h3 { font-weight: 700; font-size: 1.12em; line-height: 1.35; letter-spacing: 0; margin: 1.6em 0 0.5em; color: var(--hg-texto-fuerte); }
|
||||||
|
.md-typeset h4 { font-weight: 700; letter-spacing: 0; }
|
||||||
|
.md-typeset h5, .md-typeset h6 { text-transform: none; color: var(--hg-texto-2); }
|
||||||
|
.md-typeset .headerlink { color: var(--hg-texto-3); }
|
||||||
|
|
||||||
|
.md-typeset a { color: var(--hg-liga); text-underline-offset: 0.18em; }
|
||||||
|
.md-typeset a:hover, .md-typeset a:focus { color: var(--hg-liga-hover); text-decoration: underline; }
|
||||||
|
.md-typeset strong { font-weight: 700; color: var(--hg-texto-fuerte); }
|
||||||
|
|
||||||
|
.md-typeset hr { border-bottom: 1px solid var(--hg-linea); }
|
||||||
|
.md-typeset blockquote { border-left: 2px solid var(--hg-linea-2); color: var(--hg-texto-2); }
|
||||||
|
.md-typeset mark { color: inherit; }
|
||||||
|
|
||||||
|
/* código: la mono solo para lo que se teclea o se compara */
|
||||||
|
.md-typeset code { border-radius: 4px; font-size: 0.86em; background-color: var(--hg-elevado); }
|
||||||
|
.md-typeset pre > code { background-color: var(--hg-plano); border: 1px solid var(--hg-linea); border-radius: var(--hg-radio); }
|
||||||
|
.md-typeset kbd { font-family: var(--hg-fuente-mono); }
|
||||||
|
|
||||||
|
/* tablas */
|
||||||
|
.md-typeset table:not([class]) {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
border: 1px solid var(--hg-linea);
|
||||||
|
border-radius: var(--hg-radio);
|
||||||
|
background-color: var(--hg-plano-2);
|
||||||
|
}
|
||||||
|
.md-typeset table:not([class]) th {
|
||||||
|
background-color: var(--hg-plano);
|
||||||
|
color: var(--hg-texto-2);
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 4rem;
|
||||||
|
padding: 0.7em 1em;
|
||||||
|
}
|
||||||
|
.md-typeset table:not([class]) td { padding: 0.75em 1em; border-top: 1px solid var(--hg-linea); }
|
||||||
|
.md-typeset table:not([class]) tbody tr:hover { background-color: color-mix(in srgb, var(--hg-elevado) 35%, transparent); box-shadow: none; }
|
||||||
|
|
||||||
|
/* listas de pasos numeradas (índices de sección) */
|
||||||
|
.md-typeset ol.pasos > li { margin-bottom: 0.6em; }
|
||||||
|
.md-typeset ol.pasos > li::marker { font-weight: 700; color: var(--hg-texto-2); }
|
||||||
|
|
||||||
|
/* avisos y bloques plegables: borde completo y tenue, el color solo en el ícono */
|
||||||
|
.md-typeset .admonition,
|
||||||
|
.md-typeset details {
|
||||||
|
--hg-av: var(--hg-texto-2);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
border-radius: var(--hg-radio);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.md-typeset :is(.admonition, details)[class] { border: 1px solid color-mix(in srgb, var(--hg-av) 30%, var(--hg-linea)); background-color: var(--hg-plano); }
|
||||||
|
.md-typeset :is(.admonition, details)[class]:focus-within { box-shadow: 0 0 0 3px var(--hg-acento-suave); }
|
||||||
|
.md-typeset :is(.admonition, details)[class] > :is(.admonition-title, summary) {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--hg-texto-fuerte);
|
||||||
|
}
|
||||||
|
.md-typeset :is(.admonition, details)[class] > :is(.admonition-title, summary)::before { background-color: var(--hg-av); }
|
||||||
|
.md-typeset :is(.admonition, details)[class] > summary::after { background-color: var(--hg-texto-3); }
|
||||||
|
.md-typeset :is(.admonition, details):is(.info, .todo, .abstract, .summary, .tldr, .example) { --hg-av: var(--hg-info); }
|
||||||
|
.md-typeset :is(.admonition, details):is(.tip, .hint, .important, .success, .check, .done) { --hg-av: var(--hg-hecho); }
|
||||||
|
.md-typeset :is(.admonition, details):is(.warning, .caution, .attention, .question, .help, .faq) { --hg-av: var(--hg-aviso); }
|
||||||
|
.md-typeset :is(.admonition, details):is(.danger, .error, .failure, .fail, .missing, .bug) { --hg-av: var(--hg-bloqueado); }
|
||||||
|
|
||||||
|
/* tarjetas en rejilla (índices de sección) */
|
||||||
.md-typeset .grid.cards > ul > li,
|
.md-typeset .grid.cards > ul > li,
|
||||||
.md-typeset .grid > .card {
|
.md-typeset .grid > .card {
|
||||||
border-radius: 0.5rem;
|
border: 1px solid var(--hg-linea);
|
||||||
border: 1px solid var(--helgen-light-indigo);
|
border-radius: var(--hg-radio);
|
||||||
transition: box-shadow 0.15s ease, transform 0.15s ease, border-color 0.15s ease;
|
background-color: var(--hg-plano);
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
[data-md-color-scheme="slate"] .md-typeset .grid.cards > ul > li { border-color: #2a3650; }
|
|
||||||
.md-typeset .grid.cards > ul > li:hover,
|
.md-typeset .grid.cards > ul > li:hover,
|
||||||
.md-typeset .grid > .card:hover {
|
.md-typeset .grid > .card:hover { border-color: var(--hg-linea-2); box-shadow: none; transform: none; }
|
||||||
border-color: var(--helgen-teal);
|
|
||||||
box-shadow: 0 6px 20px rgba(44, 60, 91, 0.18);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
.md-typeset .grid.cards > ul > li > hr { margin: 0.6em 0; }
|
.md-typeset .grid.cards > ul > li > hr { margin: 0.6em 0; }
|
||||||
.md-typeset .grid.cards > ul > li h3 { margin-top: 0; }
|
.md-typeset .grid.cards > ul > li h3 { margin-top: 0; }
|
||||||
.md-typeset .grid.cards > ul > li .md-button { margin: 0.2em 0.4em 0.2em 0; font-size: 0.78em; padding: 0.45em 1em; }
|
.md-typeset .grid.cards > ul > li .md-button { margin: 0.2em 0.4em 0.2em 0; font-size: 0.78em; padding: 0.45em 1em; }
|
||||||
|
|
||||||
/* Botones */
|
/* botones: uno principal por pantalla */
|
||||||
.md-typeset .md-button--primary {
|
.md-typeset .md-button {
|
||||||
background-color: var(--helgen-indigo);
|
border: 1px solid var(--hg-linea-2);
|
||||||
border-color: var(--helgen-indigo);
|
border-radius: var(--hg-radio-campo);
|
||||||
color: #fff;
|
color: var(--hg-texto);
|
||||||
}
|
font-weight: 600;
|
||||||
.md-typeset .md-button--primary:hover {
|
padding: 0.5em 1.1em;
|
||||||
background-color: var(--helgen-teal);
|
|
||||||
border-color: var(--helgen-teal);
|
|
||||||
}
|
}
|
||||||
|
.md-typeset .md-button:hover,
|
||||||
|
.md-typeset .md-button:focus { background-color: var(--hg-elevado); border-color: var(--hg-linea-2); color: var(--hg-texto-fuerte); }
|
||||||
|
.md-typeset .md-button--primary { background-color: var(--hg-acento); border-color: var(--hg-acento); color: var(--hg-sobre-acento); }
|
||||||
|
.md-typeset .md-button--primary:hover,
|
||||||
|
.md-typeset .md-button--primary:focus { background-color: var(--hg-acento); border-color: var(--hg-acento); color: var(--hg-sobre-acento); filter: brightness(1.08); }
|
||||||
|
|
||||||
/* ---- Pastillas de estado de tarjeta ------------------------------------- */
|
/* aviso de búsqueda en la portada */
|
||||||
.badge-v3, .badge-v4, .badge-v55, .badge-v56, .badge-v57, .badge-v6, .badge-fw {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.1em 0.65em;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.72em;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
line-height: 1.6;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.badge-v56 { background: var(--helgen-indigo); color: #fff; }
|
|
||||||
.badge-v55 { background: var(--helgen-teal); color: #fff; }
|
|
||||||
.badge-v57 { background: #e3f5e8; color: #1c5c2e; border: 1px solid #8fd1a3; }
|
|
||||||
.badge-v6 { background: #fff3cd; color: #7a5200; border: 1px solid #f0c36d; }
|
|
||||||
.badge-v4 { background: var(--helgen-light-indigo); color: var(--helgen-indigo); border: 1px solid #c9d0e0; }
|
|
||||||
.badge-v3 { background: #e9e9e9; color: #555; border: 1px solid #ccc; }
|
|
||||||
.badge-fw { background: var(--helgen-light-cyan); color: #0f5f52; border: 1px solid #9fe6d8; }
|
|
||||||
[data-md-color-scheme="slate"] .badge-v4 { background: #2a3650; color: #dfe6f5; border-color: #3d5078; }
|
|
||||||
[data-md-color-scheme="slate"] .badge-v3 { background: #333; color: #ccc; border-color: #555; }
|
|
||||||
[data-md-color-scheme="slate"] .badge-v57 { background: #143d22; color: #9fe6b4; border-color: #1c5c2e; }
|
|
||||||
[data-md-color-scheme="slate"] .badge-v6 { background: #4a3a10; color: #ffd97a; border-color: #7a5200; }
|
|
||||||
[data-md-color-scheme="slate"] .badge-fw { background: #123a34; color: var(--helgen-cyan); border-color: #17A08C; }
|
|
||||||
|
|
||||||
/* Aviso de búsqueda en la portada */
|
|
||||||
.buscar-aviso {
|
.buscar-aviso {
|
||||||
background: var(--helgen-light-cyan);
|
display: flex;
|
||||||
border-left: 4px solid var(--helgen-teal);
|
gap: 0.6rem;
|
||||||
padding: 0.6em 1em;
|
align-items: flex-start;
|
||||||
border-radius: 0.3rem;
|
background: var(--hg-plano);
|
||||||
|
border: 1px solid var(--hg-linea);
|
||||||
|
border-radius: var(--hg-radio);
|
||||||
|
color: var(--hg-texto-2);
|
||||||
|
padding: 0.7em 1em;
|
||||||
margin: 1em 0 1.5em;
|
margin: 1em 0 1.5em;
|
||||||
}
|
}
|
||||||
[data-md-color-scheme="slate"] .buscar-aviso { background: #123a34; }
|
.buscar-aviso::before {
|
||||||
|
content: "";
|
||||||
/* Tablas: más compactas y con cabecera Helgen */
|
flex: none;
|
||||||
.md-typeset table:not([class]) th {
|
width: 18px; height: 18px;
|
||||||
background: var(--helgen-light-indigo);
|
margin-top: 0.15em;
|
||||||
color: var(--helgen-indigo);
|
background-color: var(--hg-texto-3);
|
||||||
|
-webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.8" stroke-linecap="round"><path d="M11 4.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM20 20l-4.4-4.4"/></svg>') center / contain no-repeat;
|
||||||
|
mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.8" stroke-linecap="round"><path d="M11 4.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM20 20l-4.4-4.4"/></svg>') center / contain no-repeat;
|
||||||
}
|
}
|
||||||
[data-md-color-scheme="slate"] .md-typeset table:not([class]) th {
|
.buscar-aviso > :first-child { margin-top: 0; }
|
||||||
background: #2a3650;
|
.buscar-aviso > :last-child { margin-bottom: 0; }
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.md-typeset table:not([class]) td, .md-typeset table:not([class]) th { padding: 0.5em 0.8em; }
|
|
||||||
|
|
||||||
/* Listas de pasos numeradas (índices de sección) */
|
/* casillas de las listas de verificación */
|
||||||
.md-typeset ol.pasos > li { margin-bottom: 0.6em; }
|
.md-typeset .task-list-indicator::before { background-color: var(--hg-linea-2); }
|
||||||
.md-typeset ol.pasos > li::marker { font-weight: 700; color: var(--helgen-teal); }
|
.md-typeset [type="checkbox"]:checked + .task-list-indicator::before { background-color: var(--hg-hecho); }
|
||||||
|
|
||||||
/* ---- Impresión (checklists y procedimientos) ---------------------------- */
|
/* ---- Pie de página ------------------------------------------------------- */
|
||||||
|
.md-footer { border-top: 1px solid var(--hg-linea-suave); }
|
||||||
|
.md-footer__link { color: var(--hg-texto-2); }
|
||||||
|
.md-footer__link:hover { color: var(--hg-texto-fuerte); opacity: 1; }
|
||||||
|
.md-footer__title { font-size: 0.75rem; }
|
||||||
|
.md-footer__direction { color: var(--hg-texto-3); }
|
||||||
|
.md-footer-meta { background-color: var(--hg-barra); }
|
||||||
|
.md-footer-meta__inner { color: var(--hg-texto-3); }
|
||||||
|
.md-copyright { color: var(--hg-texto-3); }
|
||||||
|
.md-copyright__highlight { color: var(--hg-texto-2); }
|
||||||
|
.md-top { background-color: var(--hg-plano); color: var(--hg-texto-2); border: 1px solid var(--hg-linea); box-shadow: none; }
|
||||||
|
.md-top:hover { background-color: var(--hg-elevado); color: var(--hg-texto-fuerte); }
|
||||||
|
|
||||||
|
/* ---- Impresión (checklists y procedimientos) ----------------------------- */
|
||||||
@media print {
|
@media print {
|
||||||
.md-header, .md-tabs, .md-sidebar, .md-footer, .md-top, .md-search,
|
.md-header, .md-sidebar, .md-footer, .md-top, .md-search,
|
||||||
.md-content__button, .md-source-file, .md-typeset .headerlink { display: none !important; }
|
.md-content__button, .md-source-file, .md-typeset .headerlink { display: none !important; }
|
||||||
|
body, .md-typeset { background: #fff !important; color: #000 !important; }
|
||||||
.md-main__inner { margin: 0; }
|
.md-main__inner { margin: 0; }
|
||||||
.md-content { max-width: 100%; margin: 0; }
|
.md-content { max-width: 100%; margin: 0; }
|
||||||
.md-typeset { font-size: 10.5pt; line-height: 1.35; color: #000; }
|
.md-content__inner { max-width: none; padding: 0; }
|
||||||
.md-typeset h1 { font-size: 16pt; color: #000; page-break-after: avoid; }
|
.md-typeset { font-size: 10.5pt; line-height: 1.35; }
|
||||||
|
.md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset strong { color: #000 !important; }
|
||||||
|
.md-typeset h1 { font-size: 16pt; page-break-after: avoid; }
|
||||||
.md-typeset h2 { font-size: 13pt; page-break-after: avoid; border-bottom: 1px solid #000; }
|
.md-typeset h2 { font-size: 13pt; page-break-after: avoid; border-bottom: 1px solid #000; }
|
||||||
.md-typeset h3 { font-size: 11.5pt; page-break-after: avoid; }
|
.md-typeset h3 { font-size: 11.5pt; page-break-after: avoid; }
|
||||||
|
.md-typeset table:not([class]) { background: #fff; border-color: #000; }
|
||||||
|
.md-typeset table:not([class]) th { background: #fff; color: #000; }
|
||||||
.md-typeset table { page-break-inside: avoid; font-size: 9.5pt; }
|
.md-typeset table { page-break-inside: avoid; font-size: 9.5pt; }
|
||||||
.md-typeset .admonition, .md-typeset details { page-break-inside: avoid; border: 1px solid #000; box-shadow: none; }
|
.md-typeset .admonition, .md-typeset details { page-break-inside: avoid; border: 1px solid #000 !important; background: #fff !important; box-shadow: none; }
|
||||||
.md-typeset .task-list-control .task-list-indicator { border: 1px solid #000; background: #fff; }
|
.md-typeset .task-list-control .task-list-indicator { border: 1px solid #000; background: #fff; }
|
||||||
.md-typeset a { color: #000; text-decoration: none; }
|
.md-typeset a { color: #000; text-decoration: none; }
|
||||||
.md-typeset a[href^="http"]::after { content: none; }
|
.md-typeset a[href^="http"]::after { content: none; }
|
||||||
.badge-v3, .badge-v4, .badge-v55, .badge-v56, .badge-v57, .badge-v6, .badge-fw { border: 1px solid #000; background: none; color: #000; }
|
.md-typeset code { background: #fff; border: 1px solid #999; color: #000; }
|
||||||
|
.badge-v3, .badge-v4, .badge-v55, .badge-v56, .badge-v57, .badge-v6, .badge-fw { border: 1px solid #000; color: #000; }
|
||||||
@page { margin: 12mm 14mm; }
|
@page { margin: 12mm 14mm; }
|
||||||
}
|
}
|
||||||
|
|||||||
202
docs/stylesheets/helgen.css
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
Helgen · tokens del sitio drogheda.helgen.online
|
||||||
|
Un solo archivo de colores y letras para el sitio (Material), los tableros y
|
||||||
|
las actas. Oscuro primero; el modo claro usa los mismos nombres.
|
||||||
|
|
||||||
|
- Dentro del sitio (Material) el modo lo decide <body data-md-color-scheme>:
|
||||||
|
"slate" = oscuro (por omisión), "default" = claro.
|
||||||
|
- Las páginas sueltas (actas, cuenta) usan <html data-tema="claro"> cuando la
|
||||||
|
persona eligió claro en el sitio (lo pone js/tema.js).
|
||||||
|
|
||||||
|
Reglas de la casa: sin emoji, sin MAYÚSCULAS de adorno, sin tarjetas con
|
||||||
|
borde de color a la izquierda. El cian solo marca lo que está vivo
|
||||||
|
(activo, en curso, foco). Íconos de línea de 1.8 px con puntas redondas.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* ---- Letras (latin, ≈ 76 KB en total; licencia OFL en docs/fonts/) ------ */
|
||||||
|
@font-face {
|
||||||
|
font-family: "League Spartan";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/league-spartan-latin.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Atkinson Hyperlegible Next";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/atkinson-hyperlegible-next-latin.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Atkinson Hyperlegible Mono";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("../fonts/atkinson-hyperlegible-mono-latin.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Oscuro (por omisión) ------------------------------------------------ */
|
||||||
|
:root,
|
||||||
|
[data-md-color-scheme="slate"] {
|
||||||
|
--hg-fuente-titulo: "League Spartan", "Futura", "Avenir Next", system-ui, sans-serif;
|
||||||
|
--hg-fuente: "Atkinson Hyperlegible Next", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||||
|
--hg-fuente-mono: "Atkinson Hyperlegible Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||||
|
|
||||||
|
/* superficies */
|
||||||
|
--hg-fondo: #0E1524; /* Noche: fondo de página */
|
||||||
|
--hg-barra: #0B111D; /* Barra: barra lateral y encabezado */
|
||||||
|
--hg-plano: #141D30; /* Plano: tarjetas y campos */
|
||||||
|
--hg-plano-2: #111A2C; /* paneles y celdas */
|
||||||
|
--hg-elevado: #1A2540; /* selección y menú activo */
|
||||||
|
--hg-linea: #233049; /* bordes */
|
||||||
|
--hg-linea-2: #2E3D5C; /* bordes de controles */
|
||||||
|
--hg-linea-suave: #1A2438; /* divisiones discretas */
|
||||||
|
--hg-indigo: #2C3C5B; /* índigo Helgen */
|
||||||
|
--hg-cabecera: #0B111D; /* encabezado del sitio */
|
||||||
|
--hg-sobre-cabecera: #E7ECF5;
|
||||||
|
|
||||||
|
/* texto */
|
||||||
|
--hg-texto: #E7ECF5; /* Niebla */
|
||||||
|
--hg-texto-fuerte: #F2F5FA;
|
||||||
|
--hg-texto-suave: #C3CCDD;
|
||||||
|
--hg-texto-2: #A9B4CB; /* Secundario */
|
||||||
|
--hg-texto-3: #8C99B5; /* Terciario: etiquetas y conteos */
|
||||||
|
|
||||||
|
/* acento: solo lo vivo */
|
||||||
|
--hg-acento: #51F3DA; /* Cian */
|
||||||
|
--hg-sobre-acento: #0B111D;
|
||||||
|
--hg-acento-suave: rgba(81, 243, 218, 0.14);
|
||||||
|
--hg-foco: #51F3DA;
|
||||||
|
--hg-liga: #7FE9D8;
|
||||||
|
--hg-liga-hover: #51F3DA;
|
||||||
|
--hg-teal: #17A08C;
|
||||||
|
|
||||||
|
/* estados */
|
||||||
|
--hg-curso: #51F3DA; --hg-curso-bg: #133A40;
|
||||||
|
--hg-hecho: #4FD8BE; --hg-hecho-bg: #123A36; --hg-hecho-icono: #2FBF9F;
|
||||||
|
--hg-bloqueado: #FF9494; --hg-bloqueado-bg: #3A1F27;
|
||||||
|
--hg-aviso: #F2C46B; --hg-aviso-bg: #3A3020;
|
||||||
|
--hg-info: #9DB8FF; --hg-info-bg: #1B2847;
|
||||||
|
--hg-pend: #8C99B5; --hg-pend-bg: #1F2A42;
|
||||||
|
|
||||||
|
/* pistas del tablero */
|
||||||
|
--hg-p-ingenieria: #8FB0FF;
|
||||||
|
--hg-p-produccion: #63D6A8;
|
||||||
|
--hg-p-mecanica: #E9B865;
|
||||||
|
--hg-p-compras: #C6A8FF;
|
||||||
|
--hg-p-campo: #FF9E70;
|
||||||
|
|
||||||
|
/* versiones de tarjeta: etiqueta de serigrafía (texto y borde) */
|
||||||
|
--hg-v55: #4FD8BE; --hg-v55-borde: rgba(79, 216, 190, 0.42);
|
||||||
|
--hg-v56: #A9C1FF; --hg-v56-borde: rgba(169, 193, 255, 0.42);
|
||||||
|
--hg-v57: #8BE3A7; --hg-v57-borde: rgba(139, 227, 167, 0.42);
|
||||||
|
--hg-v6: #F2C46B; --hg-v6-borde: rgba(242, 196, 107, 0.42);
|
||||||
|
--hg-v4: #B9C3D8; --hg-v4-borde: rgba(185, 195, 216, 0.38);
|
||||||
|
--hg-v3: #9AA3B5; --hg-v3-borde: rgba(154, 163, 181, 0.36);
|
||||||
|
--hg-fw: #7FE9D8; --hg-fw-borde: rgba(127, 233, 216, 0.38);
|
||||||
|
|
||||||
|
/* forma */
|
||||||
|
--hg-radio: 10px;
|
||||||
|
--hg-radio-campo: 9px;
|
||||||
|
--hg-radio-chico: 7px;
|
||||||
|
--hg-sombra: 0 18px 48px rgba(3, 7, 15, 0.55);
|
||||||
|
--hg-sombra-suave: 0 1px 2px rgba(3, 7, 15, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Claro: mismos nombres ----------------------------------------------- */
|
||||||
|
[data-md-color-scheme="default"],
|
||||||
|
html[data-tema="claro"],
|
||||||
|
:where(html:has(> body[data-md-color-scheme="default"])) {
|
||||||
|
--hg-fondo: #EFF1F5; /* Mist */
|
||||||
|
--hg-barra: #F7F8FB;
|
||||||
|
--hg-plano: #FFFFFF;
|
||||||
|
--hg-plano-2: #F6F7FA;
|
||||||
|
--hg-elevado: #E2E8F3;
|
||||||
|
--hg-linea: #D5DAE5;
|
||||||
|
--hg-linea-2: #C2CADA;
|
||||||
|
--hg-linea-suave: #E1E5EE;
|
||||||
|
--hg-indigo: #2C3C5B;
|
||||||
|
--hg-cabecera: #2C3C5B; /* el encabezado conserva el índigo de la marca */
|
||||||
|
--hg-sobre-cabecera: #FFFFFF;
|
||||||
|
|
||||||
|
--hg-texto: #1F2A40;
|
||||||
|
--hg-texto-fuerte: #141C2E;
|
||||||
|
--hg-texto-suave: #34415C;
|
||||||
|
--hg-texto-2: #4A5875;
|
||||||
|
--hg-texto-3: #65728D;
|
||||||
|
|
||||||
|
--hg-acento: #14685C; /* sobre claro, el cian no se lee: teal oscuro */
|
||||||
|
--hg-sobre-acento: #FFFFFF;
|
||||||
|
--hg-acento-suave: rgba(20, 104, 92, 0.10);
|
||||||
|
--hg-foco: #17A08C;
|
||||||
|
--hg-liga: #14685C;
|
||||||
|
--hg-liga-hover: #0E4F46;
|
||||||
|
--hg-teal: #17A08C;
|
||||||
|
|
||||||
|
--hg-curso: #0E6E62; --hg-curso-bg: #DDF5F1;
|
||||||
|
--hg-hecho: #13806F; --hg-hecho-bg: #E2F5F1; --hg-hecho-icono: #17A08C;
|
||||||
|
--hg-bloqueado: #B42318; --hg-bloqueado-bg: #FDECEC;
|
||||||
|
--hg-aviso: #8A5A00; --hg-aviso-bg: #FFF3D6;
|
||||||
|
--hg-info: #2F55B5; --hg-info-bg: #E6EDFC;
|
||||||
|
--hg-pend: #65728D; --hg-pend-bg: #E8ECF3;
|
||||||
|
|
||||||
|
--hg-p-ingenieria: #3D63C9;
|
||||||
|
--hg-p-produccion: #1E8A5E;
|
||||||
|
--hg-p-mecanica: #A26B0A;
|
||||||
|
--hg-p-compras: #7A4FD0;
|
||||||
|
--hg-p-campo: #C2531F;
|
||||||
|
|
||||||
|
--hg-v55: #13806F; --hg-v55-borde: rgba(19, 128, 111, 0.45);
|
||||||
|
--hg-v56: #3552A8; --hg-v56-borde: rgba(53, 82, 168, 0.45);
|
||||||
|
--hg-v57: #2E7D46; --hg-v57-borde: rgba(46, 125, 70, 0.45);
|
||||||
|
--hg-v6: #8F6208; --hg-v6-borde: rgba(143, 98, 8, 0.45);
|
||||||
|
--hg-v4: #4F5B75; --hg-v4-borde: rgba(79, 91, 117, 0.40);
|
||||||
|
--hg-v3: #646B7A; --hg-v3-borde: rgba(100, 107, 122, 0.38);
|
||||||
|
--hg-fw: #14685C; --hg-fw-borde: rgba(20, 104, 92, 0.40);
|
||||||
|
|
||||||
|
--hg-sombra: 0 18px 48px rgba(31, 42, 64, 0.22);
|
||||||
|
--hg-sombra-suave: 0 1px 2px rgba(31, 42, 64, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barras de desplazamiento y controles nativos del mismo modo */
|
||||||
|
html { color-scheme: dark; }
|
||||||
|
html[data-tema="claro"],
|
||||||
|
:where(html:has(> body[data-md-color-scheme="default"])) { color-scheme: light; }
|
||||||
|
[data-md-color-scheme="slate"] { color-scheme: dark; }
|
||||||
|
[data-md-color-scheme="default"] { color-scheme: light; }
|
||||||
|
|
||||||
|
/* ---- Etiquetas de versión de tarjeta (sirven en todo el sitio) ----------- */
|
||||||
|
.badge-v3, .badge-v4, .badge-v55, .badge-v56, .badge-v57, .badge-v6, .badge-fw {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: var(--hg-fuente-mono);
|
||||||
|
font-size: 0.78em;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.28em 0.5em;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
background: transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: 0.08em;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
.badge-v55 { color: var(--hg-v55); border-color: var(--hg-v55-borde); }
|
||||||
|
.badge-v56 { color: var(--hg-v56); border-color: var(--hg-v56-borde); }
|
||||||
|
.badge-v57 { color: var(--hg-v57); border-color: var(--hg-v57-borde); }
|
||||||
|
.badge-v6 { color: var(--hg-v6); border-color: var(--hg-v6-borde); }
|
||||||
|
.badge-v4 { color: var(--hg-v4); border-color: var(--hg-v4-borde); }
|
||||||
|
.badge-v3 { color: var(--hg-v3); border-color: var(--hg-v3-borde); }
|
||||||
|
.badge-fw { color: var(--hg-fw); border-color: var(--hg-fw-borde); font-family: var(--hg-fuente); font-weight: 600; }
|
||||||
|
|
||||||
|
/* Íconos de línea (los SVG del sitio llevan esta clase) */
|
||||||
|
.hg-i {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex: none;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.8;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
@ -1,224 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
<!-- Desde el 20-oct-2026 este tablero vive dentro del sitio: docs/tableros/actas.md → /tableros/actas/
|
||||||
|
Este archivo solo redirige (conserva ?serie=, ?id=, ?pista=… y #…) para que las ligas viejas
|
||||||
|
guardadas en pendientes y actas sigan sirviendo. No se edita: los cambios van en tableros/actas.md. -->
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Tablero de estado — HBOX</title>
|
<meta name="robots" content="noindex">
|
||||||
<style>
|
<title>Estado de actas - Drogheda</title>
|
||||||
:root{
|
<script>location.replace('/tableros/actas/' + location.search + location.hash);</script>
|
||||||
--naranja:#e8590c; --tinta:#1d1c19; --tinta2:#5a564d; --linea:#e6e1d6;
|
<meta http-equiv="refresh" content="0; url=/tableros/actas/">
|
||||||
--papel:#faf8f4; --tarjeta:#fff; --superf:#fcfcfb;
|
<style>body{margin:0;padding:24px;background:#0E1524;color:#E7ECF5;font:16px/1.5 system-ui,sans-serif}a{color:#7FE9D8}</style>
|
||||||
--apro:#2f9e44; --apro-bg:#ebfbee; --falla:#e03131; --falla-bg:#fff5f5;
|
|
||||||
--pend:#adb5bd; --pend-bg:#f4f2ee; --ens:#1971c2; --ens-bg:#e7f1fb;
|
|
||||||
}
|
|
||||||
@media(prefers-color-scheme:dark){:root:not([data-theme=light]){
|
|
||||||
--papel:#191b1e;--tinta:#e6e1d6;--tinta2:#a09a8d;--linea:#33363b;--tarjeta:#1f2226;--superf:#1b1d20;
|
|
||||||
--apro:#69db7c;--apro-bg:#1b2a1f;--falla:#ff8787;--falla-bg:#2c1d1d;--pend:#5c636a;--pend-bg:#26282c;--ens:#74c0fc;--ens-bg:#182734;}}
|
|
||||||
*{box-sizing:border-box}
|
|
||||||
body{margin:0;background:var(--papel);color:var(--tinta);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
||||||
.wrap{max-width:1000px;margin:0 auto;padding:18px 16px 70px}
|
|
||||||
header.top{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;
|
|
||||||
background:#2C3C5B;color:#fff;padding:16px;border-radius:12px;margin-bottom:16px}
|
|
||||||
header.top h1{margin:0;font-size:20px}
|
|
||||||
header.top .meta{color:#d7d2c8;font-size:13px}
|
|
||||||
button{background:var(--naranja);color:#fff;border:0;border-radius:9px;padding:9px 16px;font-size:14px;font-weight:600;cursor:pointer}
|
|
||||||
a.volver{color:#fff;font-size:13px;text-decoration:none;opacity:.85}
|
|
||||||
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:18px}
|
|
||||||
.kpi{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:14px 16px}
|
|
||||||
.kpi .n{font-size:30px;font-weight:700;line-height:1}
|
|
||||||
.kpi .l{color:var(--tinta2);font-size:13px;margin-top:4px}
|
|
||||||
section.bloque{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:16px;margin:14px 0}
|
|
||||||
section.bloque h2{margin:0 0 2px;font-size:17px}
|
|
||||||
.resumen{color:var(--tinta2);font-size:13.5px;margin:0 0 10px}
|
|
||||||
.barra{display:flex;height:14px;border-radius:7px;overflow:hidden;background:var(--pend-bg);margin:6px 0 14px;gap:2px}
|
|
||||||
.barra span{display:block;height:100%}
|
|
||||||
.barra .a{background:var(--apro)} .barra .f{background:var(--falla)} .barra .e{background:var(--ens)}
|
|
||||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:8px}
|
|
||||||
.cel{display:flex;flex-direction:column;gap:2px;text-decoration:none;color:inherit;
|
|
||||||
border:1px solid var(--linea);border-radius:10px;padding:9px 10px;background:var(--superf)}
|
|
||||||
.cel .s{font-weight:700;font-size:14px}
|
|
||||||
.cel .st{font-size:11.5px;font-weight:600;display:flex;align-items:center;gap:4px}
|
|
||||||
.cel .who{font-size:11px;color:var(--tinta2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
||||||
.cel.apro{background:var(--apro-bg);border-color:var(--apro)} .cel.apro .st{color:var(--apro)}
|
|
||||||
.cel.falla{background:var(--falla-bg);border-color:var(--falla)} .cel.falla .st{color:var(--falla)}
|
|
||||||
.cel.ens{background:var(--ens-bg);border-color:var(--ens)} .cel.ens .st{color:var(--ens)}
|
|
||||||
.cel.pend .st{color:var(--tinta2)}
|
|
||||||
.leyenda{display:flex;flex-wrap:wrap;gap:14px;font-size:12.5px;color:var(--tinta2);margin-top:10px}
|
|
||||||
.leyenda b{font-weight:600}
|
|
||||||
table{border-collapse:collapse;width:100%;font-size:13.5px;margin-top:6px}
|
|
||||||
th{text-align:left;color:var(--tinta2);font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;padding:6px 8px 6px 0;border-bottom:1px solid var(--tinta2)}
|
|
||||||
td{padding:7px 8px 7px 0;border-bottom:1px solid var(--linea)}
|
|
||||||
.pill{font-size:11.5px;font-weight:700;padding:2px 8px;border-radius:99px}
|
|
||||||
.pill.apro{background:var(--apro-bg);color:var(--apro)} .pill.falla{background:var(--falla-bg);color:var(--falla)}
|
|
||||||
.dos{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
|
||||||
@media(max-width:680px){.dos{grid-template-columns:1fr}}
|
|
||||||
.err{background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla);padding:10px 14px;border-radius:10px}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<p>Este tablero ahora está dentro del sitio: <a href="/tableros/actas/">Estado de actas</a>.</p>
|
||||||
<header class="top">
|
|
||||||
<div><h1>Tablero de estado — HBOX</h1><div class="meta" id="meta">Cargando…</div></div>
|
|
||||||
<div style="display:flex;gap:12px;align-items:center">
|
|
||||||
<a class="volver" href="./">← Documentación</a>
|
|
||||||
<button id="refrescar">Actualizar</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div id="cont"></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const STREAMS = [
|
|
||||||
{ tipo:'prueba-v5', prefijo:'V5', desde:1, n:30, pad:2, titulo:'Tarjetas EinarBoard — V5', form:'formularios/v5.html' },
|
|
||||||
{ tipo:'modulo-ard', prefijo:'ARD', desde:1, n:30, pad:3, titulo:'Módulos simpleRTK2B Pro — ARD', form:'formularios/ardusimple.html', usoClave:'ard' },
|
|
||||||
{ tipo:'modulo-4g', prefijo:'RTK', desde:1, n:30, pad:4, titulo:'Módems 4G NTRIP Master — RTK', form:'formularios/modem4g.html', usoClave:'modem' },
|
|
||||||
{ tipo:'prueba-v4', prefijo:'V4', desde:1, n:15, pad:2, titulo:'Tarjetas EinarBoard — V4 (fw 0.6.x)', form:'formularios/v4.html' },
|
|
||||||
];
|
|
||||||
const TABLAS = [
|
|
||||||
{ tipo:'integracion', titulo:'Integración ArduSimple + 4G (parejas)', form:'formularios/integracion.html', cols:[['ard','ArduSimple'],['modem','4G (RTK-)']] },
|
|
||||||
{ tipo:'ensamble', titulo:'Unidades ensambladas — HBOX v5', form:'formularios/ensamble.html', cols:[['einar','EinarBoard'],['ard','ArduSimple'],['modem','4G (RTK-)']], unidad:true },
|
|
||||||
];
|
|
||||||
const ICON = { apro:'✓', falla:'✕', pend:'·', ens:'▣' };
|
|
||||||
const cont = document.getElementById('cont');
|
|
||||||
const meta = document.getElementById('meta');
|
|
||||||
|
|
||||||
function fmt(iso){ return iso ? iso.replace('T',' ').slice(0,16) : ''; }
|
|
||||||
|
|
||||||
function celda(st, sc){
|
|
||||||
const cls = st.estado;
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.className = 'cel ' + cls;
|
|
||||||
a.href = (cls==='pend') ? sc.form : ('formularios/ver.html?tipo='+encodeURIComponent(sc.tipo)+'&serie='+encodeURIComponent(st.serie));
|
|
||||||
a.title = st.serie + (st.tecnico? ' — '+st.tecnico : '');
|
|
||||||
const label = cls==='apro' ? 'Aprobada' : cls==='falla' ? 'Falla' : cls==='ens' ? 'Ensamblada' : 'Pendiente';
|
|
||||||
a.innerHTML = '<span class="s">'+st.serie+'</span>'
|
|
||||||
+ '<span class="st">'+ (ICON[cls]||'·') +' '+label+'</span>'
|
|
||||||
+ '<span class="who">'+ (st.tecnico? st.tecnico : '—') +'</span>';
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
function bloque(sc, actas){
|
|
||||||
const map = {};
|
|
||||||
actas.filter(a=>a.tipo===sc.tipo).forEach(a=>{ map[a.serie]=a; });
|
|
||||||
let apro=0, falla=0, pend=0, ens=0;
|
|
||||||
const cells=[];
|
|
||||||
const desde = sc.desde||1, pad = sc.pad||2;
|
|
||||||
for(let i=desde;i<desde+sc.n;i++){
|
|
||||||
const serie = sc.prefijo+'-'+String(i).padStart(pad,'0');
|
|
||||||
const a = map[serie];
|
|
||||||
let estado='pend', tecnico='';
|
|
||||||
if(a){ tecnico=a.tecnico||'';
|
|
||||||
estado = a.resultado==='FALLA' ? 'falla' : 'apro';
|
|
||||||
// módulo aprobado y ya montado en una unidad → 'ens' (ensamblado)
|
|
||||||
if(estado==='apro' && sc.usoClave && enUnidad[sc.usoClave] && enUnidad[sc.usoClave][serie]){ estado='ens'; tecnico='en '+enUnidad[sc.usoClave][serie]; }
|
|
||||||
}
|
|
||||||
if(estado==='apro')apro++; else if(estado==='falla')falla++; else if(estado==='ens')ens++; else pend++;
|
|
||||||
cells.push(celda({serie,estado,tecnico,recibido:a?a.recibido:''}, sc));
|
|
||||||
}
|
|
||||||
const hechas = sc.n - pend;
|
|
||||||
const sec = document.createElement('section'); sec.className='bloque';
|
|
||||||
sec.innerHTML = '<h2>'+sc.titulo+'</h2>'
|
|
||||||
+ '<p class="resumen">'+hechas+' de '+sc.n+' registradas · '+apro+' aprobadas'
|
|
||||||
+ (ens ? ' · '+ens+' ya en unidad' : '') + ' · '+falla+' con falla · '+pend+' pendientes</p>';
|
|
||||||
const barra = document.createElement('div'); barra.className='barra';
|
|
||||||
const seg=(c,n)=>{ if(!n)return; const s=document.createElement('span'); s.className=c; s.style.flex=n; barra.append(s); };
|
|
||||||
seg('a',apro); seg('e',ens); seg('f',falla);
|
|
||||||
const rest=document.createElement('span'); rest.style.flex=pend; barra.append(rest);
|
|
||||||
sec.append(barra);
|
|
||||||
const grid=document.createElement('div'); grid.className='grid'; cells.forEach(c=>grid.append(c));
|
|
||||||
sec.append(grid);
|
|
||||||
return sec;
|
|
||||||
}
|
|
||||||
|
|
||||||
let enUnidad = {}; // clave -> { serieModulo: serieUnidad }
|
|
||||||
function tabla(tc, actas){
|
|
||||||
const rows = actas.filter(a=>a.tipo===tc.tipo);
|
|
||||||
const sec=document.createElement('section'); sec.className='bloque';
|
|
||||||
const ok = rows.filter(a=>a.resultado!=='FALLA').length, bad = rows.length-ok;
|
|
||||||
sec.innerHTML='<h2>'+tc.titulo+'</h2><p class="resumen">'+rows.length+' registradas · '+ok+' aprobadas · '+bad+' con falla · <a href="'+tc.form+'">+ nueva acta</a></p>';
|
|
||||||
const t=document.createElement('table');
|
|
||||||
t.innerHTML='<tr><th>'+(tc.unidad?'Unidad':'Pareja')+'</th>'+tc.cols.map(c=>'<th>'+c[1]+'</th>').join('')+'<th>Técnico</th><th>Resultado</th><th>Fecha</th></tr>';
|
|
||||||
rows.forEach(a=>{
|
|
||||||
const tr=document.createElement('tr');
|
|
||||||
const p = a.resultado==='FALLA'?'<span class="pill falla">FALLA</span>':'<span class="pill apro">OK</span>';
|
|
||||||
const link='formularios/ver.html?tipo='+encodeURIComponent(tc.tipo)+'&serie='+encodeURIComponent(a.serie||'');
|
|
||||||
tr.innerHTML='<td><a href="'+link+'"><b>'+(a.serie||'')+'</b></a></td>'
|
|
||||||
+ tc.cols.map(c=>'<td>'+((a.componentes||{})[c[0]]||'—')+'</td>').join('')
|
|
||||||
+ '<td>'+(a.tecnico||'')+'</td><td>'+p+'</td><td>'+fmt(a.recibido)+'</td>';
|
|
||||||
t.append(tr);
|
|
||||||
});
|
|
||||||
sec.append(t); if(!rows.length) sec.innerHTML+='<p class="resumen">Aún no hay registros.</p>';
|
|
||||||
return sec;
|
|
||||||
}
|
|
||||||
function render(actas){
|
|
||||||
cont.innerHTML='';
|
|
||||||
// módulos ya montados en unidades (actas de ensamble reales)
|
|
||||||
enUnidad = {};
|
|
||||||
actas.filter(a=>a.tipo==='ensamble' && a.componentes && a.resultado!=='FALLA').forEach(a=>{
|
|
||||||
for(const k in a.componentes){ enUnidad[k]=enUnidad[k]||{}; enUnidad[k][a.componentes[k]]=a.serie; }
|
|
||||||
});
|
|
||||||
// KPIs
|
|
||||||
const v5 = actas.filter(a=>a.tipo==='prueba-v5');
|
|
||||||
const rtk = actas.filter(a=>a.tipo==='modulo-ard'), g4 = actas.filter(a=>a.tipo==='modulo-4g');
|
|
||||||
const integ = actas.filter(a=>a.tipo==='integracion');
|
|
||||||
const ens = actas.filter(a=>a.tipo==='ensamble');
|
|
||||||
const okN = l=>l.filter(a=>a.resultado!=='FALLA').length;
|
|
||||||
const kpis=document.createElement('div'); kpis.className='kpis';
|
|
||||||
const kpi=(n,l)=>{ const d=document.createElement('div'); d.className='kpi'; d.innerHTML='<div class="n">'+n+'</div><div class="l">'+l+'</div>'; return d; };
|
|
||||||
kpis.append(kpi(okN(v5),'EinarBoard V5 aprobadas'));
|
|
||||||
kpis.append(kpi(okN(rtk),'ArduSimple aprobados'));
|
|
||||||
kpis.append(kpi(okN(g4),'Módems 4G aprobados'));
|
|
||||||
kpis.append(kpi(okN(integ),'Parejas integradas'));
|
|
||||||
kpis.append(kpi(okN(ens),'Unidades HBOX listas'));
|
|
||||||
cont.append(kpis);
|
|
||||||
// unidades e integración primero, luego inventario de módulos
|
|
||||||
TABLAS.slice().reverse().forEach(tc=>cont.append(tabla(tc,actas)));
|
|
||||||
STREAMS.forEach(sc=>cont.append(bloque(sc,actas)));
|
|
||||||
// leyenda
|
|
||||||
const ley=document.createElement('div'); ley.className='leyenda';
|
|
||||||
ley.innerHTML='<span><b style="color:var(--apro)">✓</b> Aprobada</span>'
|
|
||||||
+'<span><b style="color:var(--falla)">✕</b> Falla</span>'
|
|
||||||
+'<span><b style="color:var(--ens)">▣</b> Ya montado en una unidad</span>'
|
|
||||||
+'<span><b>·</b> Pendiente</span>'
|
|
||||||
+'<span>· Toca una tarjeta para ver o llenar su acta.</span>';
|
|
||||||
cont.append(ley);
|
|
||||||
// actividad reciente + por técnico
|
|
||||||
const dos=document.createElement('div'); dos.className='dos';
|
|
||||||
const rec=document.createElement('section'); rec.className='bloque';
|
|
||||||
rec.innerHTML='<h2>Actividad reciente</h2>';
|
|
||||||
const tbl=document.createElement('table');
|
|
||||||
tbl.innerHTML='<tr><th>Serie</th><th>Técnico</th><th>Resultado</th><th>Fecha</th></tr>';
|
|
||||||
actas.slice(0,12).forEach(a=>{
|
|
||||||
const tr=document.createElement('tr');
|
|
||||||
const p = a.resultado==='FALLA'?'<span class="pill falla">FALLA</span>':'<span class="pill apro">OK</span>';
|
|
||||||
tr.innerHTML='<td><b>'+(a.serie||'')+'</b></td><td>'+(a.tecnico||'')+'</td><td>'+p+'</td><td>'+fmt(a.recibido)+'</td>';
|
|
||||||
tbl.append(tr);
|
|
||||||
});
|
|
||||||
rec.append(tbl); if(!actas.length) rec.innerHTML+='<p class="resumen">Aún no hay actas registradas.</p>';
|
|
||||||
const pt=document.createElement('section'); pt.className='bloque';
|
|
||||||
pt.innerHTML='<h2>Por técnico</h2>';
|
|
||||||
const cnt={}; actas.forEach(a=>{ const t=(a.tecnico||'—').trim()||'—'; cnt[t]=(cnt[t]||0)+1; });
|
|
||||||
const t2=document.createElement('table'); t2.innerHTML='<tr><th>Técnico</th><th>Actas</th></tr>';
|
|
||||||
Object.entries(cnt).sort((x,y)=>y[1]-x[1]).forEach(([t,c])=>{ const tr=document.createElement('tr'); tr.innerHTML='<td>'+t+'</td><td>'+c+'</td>'; t2.append(tr); });
|
|
||||||
pt.append(t2); if(!actas.length) pt.innerHTML+='<p class="resumen">—</p>';
|
|
||||||
dos.append(rec,pt); cont.append(dos);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cargar(){
|
|
||||||
meta.textContent='Actualizando…';
|
|
||||||
try{
|
|
||||||
const r=await fetch('/api/actas',{cache:'no-store'});
|
|
||||||
const j=await r.json();
|
|
||||||
if(!j.ok) throw new Error(j.error||'error');
|
|
||||||
render(j.actas||[]);
|
|
||||||
meta.textContent='Actualizado '+new Date().toLocaleTimeString('es-MX',{hour:'2-digit',minute:'2-digit'})+' · '+(j.total||0)+' actas';
|
|
||||||
}catch(e){
|
|
||||||
cont.innerHTML='<div class="err">No se pudo cargar el tablero: '+e.message+'</div>';
|
|
||||||
meta.textContent='Error';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.getElementById('refrescar').addEventListener('click',cargar);
|
|
||||||
cargar();
|
|
||||||
setInterval(cargar, 30000);
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
173
docs/tableros/actas.md
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
---
|
||||||
|
title: "Actas"
|
||||||
|
description: "Qué tarjeta, módulo o unidad pasó cada acta: series V5, ARD, RTK y V4, parejas y unidades."
|
||||||
|
template: tablero.html
|
||||||
|
hide:
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="tb tb-actas">
|
||||||
|
<header class="tb-cab">
|
||||||
|
<div class="tb-cab__t"><h1>Estado de actas</h1><span class="meta" id="meta">Cargando…</span></div>
|
||||||
|
<div class="tb-cab__acc"><button type="button" class="sec" id="refrescar"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4"/></svg>Actualizar</button></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="cont"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const STREAMS = [
|
||||||
|
{ tipo:'prueba-v5', prefijo:'V5', desde:1, n:30, pad:2, titulo:'Tarjetas EinarBoard — V5', form:'/formularios/v5.html' },
|
||||||
|
{ tipo:'modulo-ard', prefijo:'ARD', desde:1, n:30, pad:3, titulo:'Módulos simpleRTK2B Pro — ARD', form:'/formularios/ardusimple.html', usoClave:'ard' },
|
||||||
|
{ tipo:'modulo-4g', prefijo:'RTK', desde:1, n:30, pad:4, titulo:'Módems 4G NTRIP Master — RTK', form:'/formularios/modem4g.html', usoClave:'modem' },
|
||||||
|
{ tipo:'prueba-v4', prefijo:'V4', desde:1, n:15, pad:2, titulo:'Tarjetas EinarBoard — V4 (fw 0.6.x)', form:'/formularios/v4.html' },
|
||||||
|
];
|
||||||
|
const TABLAS = [
|
||||||
|
{ tipo:'integracion', titulo:'Integración ArduSimple + 4G (parejas)', form:'/formularios/integracion.html', cols:[['ard','ArduSimple'],['modem','4G (RTK-)']] },
|
||||||
|
{ tipo:'ensamble', titulo:'Unidades ensambladas — HBOX v5', form:'/formularios/ensamble.html', cols:[['einar','EinarBoard'],['ard','ArduSimple'],['modem','4G (RTK-)']], unidad:true },
|
||||||
|
];
|
||||||
|
const ICON = { apro: Kanban.icono('ok'), falla: Kanban.icono('cerrar'), pend: '·', ens: Kanban.icono('unidad') };
|
||||||
|
const cont = document.getElementById('cont');
|
||||||
|
const meta = document.getElementById('meta');
|
||||||
|
|
||||||
|
function fmt(iso){ return iso ? iso.replace('T',' ').slice(0,16) : ''; }
|
||||||
|
|
||||||
|
function celda(st, sc){
|
||||||
|
const cls = st.estado;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.className = 'cel ' + cls;
|
||||||
|
a.href = (cls==='pend') ? sc.form : ('/formularios/ver.html?tipo='+encodeURIComponent(sc.tipo)+'&serie='+encodeURIComponent(st.serie));
|
||||||
|
a.title = st.serie + (st.tecnico? ' — '+st.tecnico : '');
|
||||||
|
const label = cls==='apro' ? 'Aprobada' : cls==='falla' ? 'Falla' : cls==='ens' ? 'Ensamblada' : 'Pendiente';
|
||||||
|
a.innerHTML = '<span class="s">'+st.serie+'</span>'
|
||||||
|
+ '<span class="st">'+ (ICON[cls]||'·') +' '+label+'</span>'
|
||||||
|
+ '<span class="who">'+ (st.tecnico? st.tecnico : '—') +'</span>';
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bloque(sc, actas){
|
||||||
|
const map = {};
|
||||||
|
actas.filter(a=>a.tipo===sc.tipo).forEach(a=>{ map[a.serie]=a; });
|
||||||
|
let apro=0, falla=0, pend=0, ens=0;
|
||||||
|
const cells=[];
|
||||||
|
const desde = sc.desde||1, pad = sc.pad||2;
|
||||||
|
for(let i=desde;i<desde+sc.n;i++){
|
||||||
|
const serie = sc.prefijo+'-'+String(i).padStart(pad,'0');
|
||||||
|
const a = map[serie];
|
||||||
|
let estado='pend', tecnico='';
|
||||||
|
if(a){ tecnico=a.tecnico||'';
|
||||||
|
estado = a.resultado==='FALLA' ? 'falla' : 'apro';
|
||||||
|
// módulo aprobado y ya montado en una unidad → 'ens' (ensamblado)
|
||||||
|
if(estado==='apro' && sc.usoClave && enUnidad[sc.usoClave] && enUnidad[sc.usoClave][serie]){ estado='ens'; tecnico='en '+enUnidad[sc.usoClave][serie]; }
|
||||||
|
}
|
||||||
|
if(estado==='apro')apro++; else if(estado==='falla')falla++; else if(estado==='ens')ens++; else pend++;
|
||||||
|
cells.push(celda({serie,estado,tecnico,recibido:a?a.recibido:''}, sc));
|
||||||
|
}
|
||||||
|
const hechas = sc.n - pend;
|
||||||
|
const sec = document.createElement('section'); sec.className='bloque';
|
||||||
|
sec.innerHTML = '<h2>'+sc.titulo+'</h2>'
|
||||||
|
+ '<p class="resumen">'+hechas+' de '+sc.n+' registradas · '+apro+' aprobadas'
|
||||||
|
+ (ens ? ' · '+ens+' ya en unidad' : '') + ' · '+falla+' con falla · '+pend+' pendientes</p>';
|
||||||
|
const barra = document.createElement('div'); barra.className='barra';
|
||||||
|
const seg=(c,n)=>{ if(!n)return; const s=document.createElement('span'); s.className=c; s.style.flex=n; barra.append(s); };
|
||||||
|
seg('a',apro); seg('e',ens); seg('f',falla);
|
||||||
|
const rest=document.createElement('span'); rest.style.flex=pend; barra.append(rest);
|
||||||
|
sec.append(barra);
|
||||||
|
const grid=document.createElement('div'); grid.className='grid'; cells.forEach(c=>grid.append(c));
|
||||||
|
sec.append(grid);
|
||||||
|
return sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
let enUnidad = {}; // clave -> { serieModulo: serieUnidad }
|
||||||
|
function tabla(tc, actas){
|
||||||
|
const rows = actas.filter(a=>a.tipo===tc.tipo);
|
||||||
|
const sec=document.createElement('section'); sec.className='bloque';
|
||||||
|
const ok = rows.filter(a=>a.resultado!=='FALLA').length, bad = rows.length-ok;
|
||||||
|
sec.innerHTML='<h2>'+tc.titulo+'</h2><p class="resumen">'+rows.length+' registradas · '+ok+' aprobadas · '+bad+' con falla · <a href="'+tc.form+'">Nueva acta</a></p>';
|
||||||
|
const t=document.createElement('table');
|
||||||
|
t.innerHTML='<tr><th>'+(tc.unidad?'Unidad':'Pareja')+'</th>'+tc.cols.map(c=>'<th>'+c[1]+'</th>').join('')+'<th>Técnico</th><th>Resultado</th><th>Fecha</th></tr>';
|
||||||
|
rows.forEach(a=>{
|
||||||
|
const tr=document.createElement('tr');
|
||||||
|
const p = a.resultado==='FALLA'?'<span class="pill falla">FALLA</span>':'<span class="pill apro">OK</span>';
|
||||||
|
const link='/formularios/ver.html?tipo='+encodeURIComponent(tc.tipo)+'&serie='+encodeURIComponent(a.serie||'');
|
||||||
|
tr.innerHTML='<td><a href="'+link+'"><b>'+(a.serie||'')+'</b></a></td>'
|
||||||
|
+ tc.cols.map(c=>'<td>'+((a.componentes||{})[c[0]]||'—')+'</td>').join('')
|
||||||
|
+ '<td>'+(a.tecnico||'')+'</td><td>'+p+'</td><td>'+fmt(a.recibido)+'</td>';
|
||||||
|
t.append(tr);
|
||||||
|
});
|
||||||
|
sec.append(t); if(!rows.length) sec.innerHTML+='<p class="resumen">Aún no hay registros.</p>';
|
||||||
|
return sec;
|
||||||
|
}
|
||||||
|
function render(actas){
|
||||||
|
cont.innerHTML='';
|
||||||
|
// módulos ya montados en unidades (actas de ensamble reales)
|
||||||
|
enUnidad = {};
|
||||||
|
actas.filter(a=>a.tipo==='ensamble' && a.componentes && a.resultado!=='FALLA').forEach(a=>{
|
||||||
|
for(const k in a.componentes){ enUnidad[k]=enUnidad[k]||{}; enUnidad[k][a.componentes[k]]=a.serie; }
|
||||||
|
});
|
||||||
|
// KPIs
|
||||||
|
const v5 = actas.filter(a=>a.tipo==='prueba-v5');
|
||||||
|
const rtk = actas.filter(a=>a.tipo==='modulo-ard'), g4 = actas.filter(a=>a.tipo==='modulo-4g');
|
||||||
|
const integ = actas.filter(a=>a.tipo==='integracion');
|
||||||
|
const ens = actas.filter(a=>a.tipo==='ensamble');
|
||||||
|
const okN = l=>l.filter(a=>a.resultado!=='FALLA').length;
|
||||||
|
const kpis=document.createElement('div'); kpis.className='kpis';
|
||||||
|
const kpi=(n,l)=>{ const d=document.createElement('div'); d.className='kpi'; d.innerHTML='<div class="n">'+n+'</div><div class="l">'+l+'</div>'; return d; };
|
||||||
|
kpis.append(kpi(okN(v5),'EinarBoard V5 aprobadas'));
|
||||||
|
kpis.append(kpi(okN(rtk),'ArduSimple aprobados'));
|
||||||
|
kpis.append(kpi(okN(g4),'Módems 4G aprobados'));
|
||||||
|
kpis.append(kpi(okN(integ),'Parejas integradas'));
|
||||||
|
kpis.append(kpi(okN(ens),'Unidades HBOX listas'));
|
||||||
|
cont.append(kpis);
|
||||||
|
// unidades e integración primero, luego inventario de módulos
|
||||||
|
TABLAS.slice().reverse().forEach(tc=>cont.append(tabla(tc,actas)));
|
||||||
|
STREAMS.forEach(sc=>cont.append(bloque(sc,actas)));
|
||||||
|
// leyenda
|
||||||
|
const ley=document.createElement('div'); ley.className='leyenda';
|
||||||
|
ley.innerHTML='<span class="st" style="color:var(--apro)">'+ICON.apro+'<span style="color:var(--tinta2)">Aprobada</span></span>'
|
||||||
|
+'<span class="st" style="color:var(--falla)">'+ICON.falla+'<span style="color:var(--tinta2)">Falla</span></span>'
|
||||||
|
+'<span class="st" style="color:var(--ens)">'+ICON.ens+'<span style="color:var(--tinta2)">Ya montado en una unidad</span></span>'
|
||||||
|
+'<span><b>·</b> Pendiente</span>'
|
||||||
|
+'<span>· Toca una tarjeta para ver o llenar su acta.</span>';
|
||||||
|
cont.append(ley);
|
||||||
|
// actividad reciente + por técnico
|
||||||
|
const dos=document.createElement('div'); dos.className='dos';
|
||||||
|
const rec=document.createElement('section'); rec.className='bloque';
|
||||||
|
rec.innerHTML='<h2>Actividad reciente</h2>';
|
||||||
|
const tbl=document.createElement('table');
|
||||||
|
tbl.innerHTML='<tr><th>Serie</th><th>Técnico</th><th>Resultado</th><th>Fecha</th></tr>';
|
||||||
|
actas.slice(0,12).forEach(a=>{
|
||||||
|
const tr=document.createElement('tr');
|
||||||
|
const p = a.resultado==='FALLA'?'<span class="pill falla">FALLA</span>':'<span class="pill apro">OK</span>';
|
||||||
|
tr.innerHTML='<td><b>'+(a.serie||'')+'</b></td><td>'+(a.tecnico||'')+'</td><td>'+p+'</td><td>'+fmt(a.recibido)+'</td>';
|
||||||
|
tbl.append(tr);
|
||||||
|
});
|
||||||
|
rec.append(tbl); if(!actas.length) rec.innerHTML+='<p class="resumen">Aún no hay actas registradas.</p>';
|
||||||
|
const pt=document.createElement('section'); pt.className='bloque';
|
||||||
|
pt.innerHTML='<h2>Por técnico</h2>';
|
||||||
|
const cnt={}; actas.forEach(a=>{ const t=(a.tecnico||'—').trim()||'—'; cnt[t]=(cnt[t]||0)+1; });
|
||||||
|
const t2=document.createElement('table'); t2.innerHTML='<tr><th>Técnico</th><th>Actas</th></tr>';
|
||||||
|
Object.entries(cnt).sort((x,y)=>y[1]-x[1]).forEach(([t,c])=>{ const tr=document.createElement('tr'); tr.innerHTML='<td>'+t+'</td><td>'+c+'</td>'; t2.append(tr); });
|
||||||
|
pt.append(t2); if(!actas.length) pt.innerHTML+='<p class="resumen">—</p>';
|
||||||
|
dos.append(rec,pt); cont.append(dos);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cargar(){
|
||||||
|
meta.textContent='Actualizando…';
|
||||||
|
try{
|
||||||
|
const r=await fetch('/api/actas',{cache:'no-store'});
|
||||||
|
const j=await r.json();
|
||||||
|
if(!j.ok) throw new Error(j.error||'error');
|
||||||
|
render(j.actas||[]);
|
||||||
|
meta.textContent='Actualizado '+new Date().toLocaleTimeString('es-MX',{hour:'2-digit',minute:'2-digit'})+' · '+(j.total||0)+' actas';
|
||||||
|
}catch(e){
|
||||||
|
cont.innerHTML='<div class="err">No se pudo cargar el tablero: '+e.message+'</div>';
|
||||||
|
meta.textContent='Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('refrescar').addEventListener('click',cargar);
|
||||||
|
cargar();
|
||||||
|
setInterval(cargar, 30000);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
146
docs/tableros/campo.md
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
---
|
||||||
|
title: "Visitas de campo"
|
||||||
|
description: "Los diagnósticos en campo y en banco, por terminal y por unidad, con fotos y logs."
|
||||||
|
template: tablero.html
|
||||||
|
hide:
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="tb tb-campo">
|
||||||
|
<header class="tb-cab">
|
||||||
|
<div class="tb-cab__t"><h1>Visitas de campo</h1><span class="meta" id="meta">Cargando…</span></div>
|
||||||
|
<div class="tb-cab__acc"><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>Nueva acta de diagnóstico</a><button type="button" class="sec" id="refrescar"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4"/></svg>Actualizar</button></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="cont"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/formularios/flota.js"></script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const TIPOS = ['diagnostico-campo','diagnostico-banco'];
|
||||||
|
const TIPO = 'diagnostico-campo';
|
||||||
|
const cont = document.getElementById('cont');
|
||||||
|
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=>e.append(c)); return e;};
|
||||||
|
const fecha = s => (s||'').replace('T',' ').slice(0,16);
|
||||||
|
|
||||||
|
function pill(v){
|
||||||
|
const t = String(v.accion||'');
|
||||||
|
if (/^Instalada y reportando/.test(t)) return ['ok','Operando'];
|
||||||
|
if (/^Instalada/.test(t)) return ['falla','Instalada sin resolver'];
|
||||||
|
if (/Querétaro/.test(t)) return ['info','En camino a QRO'];
|
||||||
|
if (/taller/.test(t)) return ['info','En taller'];
|
||||||
|
if (/Reemplazada/.test(t)) return ['ok','Reemplazada'];
|
||||||
|
return ['pend', v.resultado || '—'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detalle(tipo, serie, archivo, caja){
|
||||||
|
caja.textContent = 'Cargando…';
|
||||||
|
try{
|
||||||
|
const r = await fetch(`/api/acta?tipo=${encodeURIComponent(tipo)}&serie=${encodeURIComponent(serie)}&archivo=${encodeURIComponent(archivo)}`);
|
||||||
|
const j = await r.json();
|
||||||
|
if(!j.exists) throw new Error('no encontrada');
|
||||||
|
const t = el('table',{class:'det'});
|
||||||
|
const res = j.acta.respuestas || {};
|
||||||
|
Object.keys(res).forEach(k=>{
|
||||||
|
const v = res[k]; if(v.val===''||v.val===false||v.val==null) return;
|
||||||
|
t.append(el('tr',null, el('td',{class:'k',text:(v.lbl||k).replace(/<[^>]+>/g,'')}),
|
||||||
|
el('td',{text: v.val===true ? 'sí' : String(v.val)})));
|
||||||
|
});
|
||||||
|
caja.innerHTML=''; caja.append(t);
|
||||||
|
}catch(e){ caja.textContent = 'No se pudo cargar el detalle.'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(actas){
|
||||||
|
cont.innerHTML='';
|
||||||
|
const porUnidad = {};
|
||||||
|
actas.filter(a=>TIPOS.includes(a.tipo)).forEach(a=>{ (porUnidad[a.serie]=porUnidad[a.serie]||[]).push(a); });
|
||||||
|
|
||||||
|
const flota = (window.FLOTA||[]).slice();
|
||||||
|
Object.keys(porUnidad).forEach(s=>{ if(!flota.some(u=>u.serie===s)) flota.push({serie:s, etiqueta:s, maquina:'(fuera del registro de flota)', terminal:'Sin clasificar'}); });
|
||||||
|
|
||||||
|
const visitas = actas.filter(a=>TIPOS.includes(a.tipo));
|
||||||
|
const conVisita = Object.keys(porUnidad).length;
|
||||||
|
const operando = flota.filter(u=>{const v=(porUnidad[u.serie]||[])[0]; return v && pill(v)[0]==='ok';}).length;
|
||||||
|
const kp = el('div',{class:'kpis'});
|
||||||
|
[[flota.length,'Unidades en el registro'],[visitas.length,'Diagnósticos recibidos'],
|
||||||
|
[conVisita,'Unidades ya diagnosticadas'],[operando,'Reportadas como operando']]
|
||||||
|
.forEach(([n,l])=>kp.append(el('div',{class:'kpi'}, el('div',{class:'n',text:String(n)}), el('div',{class:'l',text:l}))));
|
||||||
|
cont.append(kp);
|
||||||
|
|
||||||
|
const grupos = {};
|
||||||
|
flota.forEach(u=>{ (grupos[u.terminal||'Sin clasificar']=grupos[u.terminal||'Sin clasificar']||[]).push(u); });
|
||||||
|
|
||||||
|
Object.keys(grupos).sort().forEach(g=>{
|
||||||
|
const b = el('section',{class:'bloque'});
|
||||||
|
b.append(el('h2',{text:g}));
|
||||||
|
b.append(el('p',{class:'resumen',text:grupos[g].length+' unidad(es) registradas en esta terminal.'}));
|
||||||
|
grupos[g].forEach(u=>{
|
||||||
|
const vs = (porUnidad[u.serie]||[]);
|
||||||
|
const box = el('div',{class:'unidad'});
|
||||||
|
const cab = el('div',{class:'cab'});
|
||||||
|
cab.append(el('div',null,
|
||||||
|
el('div',{class:'s'}, el('a',{href:'/tableros/unidad/?serie='+encodeURIComponent(u.serie),
|
||||||
|
style:'color:inherit;text-decoration:none;border-bottom:1px dotted currentColor',
|
||||||
|
title:'Ver el expediente completo',text:u.serie})),
|
||||||
|
el('div',{class:'m',text:(u.maquina||'')+(u.nota?' · '+u.nota:'')})));
|
||||||
|
const [cls,txt] = vs.length ? pill(vs[0]) : ['pend','Sin diagnóstico'];
|
||||||
|
if(DANOS[u.serie]) cab.append(el('span',{class:'pill falla',text:DANOS[u.serie].marca==='BAJA'?'Baja':'Dañada'}));
|
||||||
|
cab.append(el('span',{class:'pill '+cls,text:txt}));
|
||||||
|
box.append(cab);
|
||||||
|
|
||||||
|
if(!vs.length) box.append(el('div',{class:'visita vacio',text:'Todavía no hay ningún diagnóstico registrado para esta unidad.'}));
|
||||||
|
vs.forEach(v=>{
|
||||||
|
const d = el('div',{class:'visita'});
|
||||||
|
d.append(el('div',{class:'f',text:(v.tipo==='diagnostico-banco'?'Banco · ':'Campo · ')+fecha(v.recibido)+' · '+(v.tecnico||'—')+(v.motivo?' · '+v.motivo:'')}));
|
||||||
|
if(v.clasificacion) d.append(el('div',{class:'cl',text:v.clasificacion}));
|
||||||
|
if(v.accion) d.append(el('div',{text:'→ '+v.accion}));
|
||||||
|
if(v.fotos && v.fotos.length){
|
||||||
|
const f = el('div',{class:'fotos'});
|
||||||
|
v.fotos.forEach(r=>{
|
||||||
|
const a = el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(r),target:'_blank'});
|
||||||
|
a.append(el('img',{src:'/api/adjunto?ruta='+encodeURIComponent(r),loading:'lazy'}));
|
||||||
|
f.append(a);
|
||||||
|
});
|
||||||
|
d.append(f);
|
||||||
|
}
|
||||||
|
if(v.archivos && v.archivos.length){
|
||||||
|
const g = el('div',{class:'logs'});
|
||||||
|
v.archivos.forEach(a=>{
|
||||||
|
const x = el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(a.ruta),target:'_blank',
|
||||||
|
text:(a.nombre||'captura')});
|
||||||
|
g.append(x);
|
||||||
|
});
|
||||||
|
d.append(g);
|
||||||
|
}
|
||||||
|
const det = el('details');
|
||||||
|
det.append(el('summary',{text:'Ver todas las respuestas'}));
|
||||||
|
const caja = el('div'); det.append(caja);
|
||||||
|
det.addEventListener('toggle',()=>{ if(det.open && !caja.dataset.listo){ caja.dataset.listo='1'; detalle(v.tipo, v.serie, v.archivo_n, caja); } });
|
||||||
|
d.append(det);
|
||||||
|
box.append(d);
|
||||||
|
});
|
||||||
|
b.append(box);
|
||||||
|
});
|
||||||
|
cont.append(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let DANOS = {};
|
||||||
|
async function cargar(){
|
||||||
|
document.getElementById('meta').textContent = 'Cargando…';
|
||||||
|
try{
|
||||||
|
try{ const d=await (await fetch('/api/danos',{cache:'no-store'})).json(); DANOS={}; (d.series||[]).forEach(x=>DANOS[x.serie]=x); }catch(e){}
|
||||||
|
const r = await fetch('/api/actas',{cache:'no-store'});
|
||||||
|
const j = await r.json();
|
||||||
|
render(j.actas||[]);
|
||||||
|
document.getElementById('meta').textContent = 'Actualizado ' + new Date().toLocaleString('es-MX',{dateStyle:'short',timeStyle:'short'});
|
||||||
|
}catch(e){
|
||||||
|
cont.innerHTML=''; cont.append(el('div',{class:'err',text:'No se pudo leer el servidor de actas: '+e.message}));
|
||||||
|
document.getElementById('meta').textContent = 'Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('refrescar').addEventListener('click',cargar);
|
||||||
|
cargar();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@ -1,226 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
<!-- Desde el 20-oct-2026 este tablero vive dentro del sitio: docs/tableros/equipo.md → /tableros/equipo/
|
||||||
|
Este archivo solo redirige (conserva ?serie=, ?id=, ?pista=… y #…) para que las ligas viejas
|
||||||
|
guardadas en pendientes y actas sigan sirviendo. No se edita: los cambios van en tableros/equipo.md. -->
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Equipo y accesos — HBOX</title>
|
<meta name="robots" content="noindex">
|
||||||
<link rel="stylesheet" href="tablero.css">
|
<title>Equipo - Drogheda</title>
|
||||||
<style>
|
<script>location.replace('/tableros/equipo/' + location.search + location.hash);</script>
|
||||||
table.t td.num{text-align:right;font-variant-numeric:tabular-nums}
|
<meta http-equiv="refresh" content="0; url=/tableros/equipo/">
|
||||||
table.t th.num{text-align:right}
|
<style>body{margin:0;padding:24px;background:#0E1524;color:#E7ECF5;font:16px/1.5 system-ui,sans-serif}a{color:#7FE9D8}</style>
|
||||||
table.t tr.inactivo td{opacity:.55}
|
|
||||||
table.t td .nom{font-weight:700;cursor:pointer;color:var(--naranja)}
|
|
||||||
table.t td .nom:hover{text-decoration:underline}
|
|
||||||
table.t td .id{font-size:11.5px;color:var(--tinta2);display:block}
|
|
||||||
table.t td button{padding:5px 9px;font-size:12.5px;margin:1px 2px 1px 0}
|
|
||||||
.area{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;border-radius:99px;background:var(--pend-bg);color:var(--tinta2)}
|
|
||||||
.pass-una-vez{background:var(--aviso-bg);border:1px solid var(--aviso);border-radius:10px;padding:12px 14px;margin-top:10px;font-size:14.5px;color:#7a4a00}
|
|
||||||
.pass-una-vez code{font-size:17px;font-weight:700;letter-spacing:.04em;user-select:all}
|
|
||||||
.mis-pend .card{cursor:default}
|
|
||||||
@media(max-width:760px){#cont table.t{min-width:760px}}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<p>Este tablero ahora está dentro del sitio: <a href="/tableros/equipo/">Equipo</a>.</p>
|
||||||
<header class="top">
|
|
||||||
<div><h1>Equipo y accesos</h1><div class="meta" id="meta">Cargando…</div></div>
|
|
||||||
<nav>
|
|
||||||
<a href="../">← Documentación</a>
|
|
||||||
<a href="kanban.html">Pendientes</a>
|
|
||||||
<a href="ingenieria.html">Ingeniería</a>
|
|
||||||
<a href="produccion.html">Producción</a>
|
|
||||||
<a class="activo" href="equipo.html">Equipo</a>
|
|
||||||
<a href="../cuenta.html">Mi cuenta</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<details class="como">
|
|
||||||
<summary>Cómo se usa</summary>
|
|
||||||
<ol>
|
|
||||||
<li>Aquí está <b>quién es quién</b>: nombre, rol, área, si tiene acceso al sitio y cuántos pendientes abiertos y vencidos trae cada quien.</li>
|
|
||||||
<li><b>Todo pendiente tiene responsable.</b> Al crearlo en el tablero se elige de esta lista; puede haber más de uno.</li>
|
|
||||||
<li>Un miembro <b>sin acceso al sitio</b> también puede ser responsable (Adriana hoy): el pendiente se le asigna igual y alguien con acceso lo mueve por ella.</li>
|
|
||||||
<li>Toca un nombre para ver sus pendientes abiertos y abrir el tablero filtrado.</li>
|
|
||||||
<li>Los <b>accesos los crea Leo aquí</b> (botón «Nuevo usuario / miembro»), ya no con <code>htpasswd</code> en el servidor. La contraseña inicial se muestra <b>una sola vez</b>: pásala en persona o por mensaje directo; la persona la cambia en <a href="../cuenta.html">Mi cuenta</a>.</li>
|
|
||||||
<li>«Desactivar» oculta al miembro de las listas sin borrar nada; «Quitar acceso» le cierra la entrada al sitio y lo desactiva. Nadie se borra: el historial se queda.</li>
|
|
||||||
<li>Si no tienes acceso o no apareces, pídeselo a Leo.</li>
|
|
||||||
</ol>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div class="msg" id="aviso"></div>
|
|
||||||
<div id="acciones" style="display:none;margin:0 0 12px"><button id="bNuevo">+ Nuevo usuario / miembro</button></div>
|
|
||||||
<div id="cont" class="tblwrap"></div>
|
|
||||||
<section class="bloque mis-pend" id="detalle" style="display:none"></section>
|
|
||||||
</div>
|
|
||||||
<dialog id="dlg"></dialog>
|
|
||||||
<script src="kanban.js"></script>
|
|
||||||
<script>
|
|
||||||
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 AREA = { direccion: 'Dirección', operaciones: 'Operaciones', ingenieria: 'Ingeniería', layout: 'Layout', produccion: 'Producción', pruebas: 'Pruebas', fabricacion: 'Fabricación', software: 'Software', campo: 'Campo', asistente: 'Asistente' };
|
|
||||||
const PISTAS = Kanban.PISTAS, ESTADOS = Object.fromEntries(Kanban.ESTADOS);
|
|
||||||
let MIEMBROS = [], ADMIN = false, USER = '', PASS_MIN = 8, AREAS = Object.keys(AREA), HTPASSWD_OK = true;
|
|
||||||
const dlg = $('dlg'), aviso = $('aviso');
|
|
||||||
function mensaje(t, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.innerHTML = t; clearTimeout(aviso._t); aviso._t = setTimeout(() => aviso.className = 'msg', bien ? 5000 : 10000); }
|
|
||||||
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, ayuda) { const d = el('div'); d.append(el('label', { text: l }), i); if (ayuda) d.append(el('div', { style: 'font-size:12.5px;color:var(--tinta2);margin-top:3px', text: ayuda })); return d; }
|
|
||||||
|
|
||||||
function pintar() {
|
|
||||||
const cont = $('cont'); cont.innerHTML = '';
|
|
||||||
if (!MIEMBROS.length) { cont.append(el('div', { class: 'vacio', text: 'Todavía no hay miembros. Corre la siembra o crea el primero.' })); return; }
|
|
||||||
const t = el('table', { class: 't' });
|
|
||||||
t.innerHTML = '<tr><th>Nombre</th><th>Rol</th><th>Área</th><th>Acceso</th><th>Admin</th><th class="num">Abiertos</th><th class="num">Vencidos</th><th>Activo</th>' + (ADMIN ? '<th></th>' : '') + '</tr>';
|
|
||||||
MIEMBROS.forEach(m => {
|
|
||||||
const tr = el('tr', { class: m.activo === false ? 'inactivo' : '' });
|
|
||||||
const nom = el('span', { class: 'nom', text: m.nombre }); nom.addEventListener('click', () => detalle(m));
|
|
||||||
tr.append(el('td', null, nom, el('span', { class: 'id', text: m.id + (m.correo ? ' · ' + m.correo : '') + (m.telefono ? ' · ' + m.telefono : '') })));
|
|
||||||
tr.append(el('td', { text: m.rol || '—' }));
|
|
||||||
tr.append(el('td', null, el('span', { class: 'area', text: AREA[m.area] || m.area })));
|
|
||||||
tr.append(el('td', { html: m.tiene_login ? '<span class="pill ok">sí</span>' : '<span class="pill pend">no</span>' }));
|
|
||||||
tr.append(el('td', { html: m.es_admin ? '<span class="pill info">admin</span>' : '' }));
|
|
||||||
const p = m.pendientes || {};
|
|
||||||
tr.append(el('td', { class: 'num', html: p.abiertos ? '<a href="kanban.html?responsable=' + encodeURIComponent(m.id) + '"><b>' + p.abiertos + '</b></a>' : '0' }));
|
|
||||||
tr.append(el('td', { class: 'num', html: p.vencidas ? '<span style="color:var(--falla);font-weight:700">' + p.vencidas + '</span>' : '0' }));
|
|
||||||
tr.append(el('td', { html: m.activo === false ? '<span class="pill falla">inactivo</span>' : '<span class="pill ok">activo</span>' }));
|
|
||||||
if (ADMIN) {
|
|
||||||
const acc = el('td', { style: 'white-space:nowrap' });
|
|
||||||
const be = el('button', { class: 'sec', text: 'Editar' }); be.addEventListener('click', () => editar(m)); acc.append(be);
|
|
||||||
if (m.activo !== false) { const bd = el('button', { class: 'sec', text: 'Desactivar' }); bd.addEventListener('click', () => desactivar(m)); acc.append(bd); }
|
|
||||||
else { const ba = el('button', { class: 'sec', text: 'Reactivar' }); ba.addEventListener('click', () => reactivar(m)); acc.append(ba); }
|
|
||||||
if (m.tiene_login && m.id.toLowerCase() !== USER.toLowerCase()) { const bq = el('button', { class: 'peligro', text: 'Quitar acceso' }); bq.addEventListener('click', () => quitarAcceso(m)); acc.append(bq); }
|
|
||||||
if (!m.tiene_login && m.activo !== false) { const bc = el('button', { text: 'Crear acceso' }); bc.addEventListener('click', () => nuevo(m)); acc.append(bc); }
|
|
||||||
tr.append(acc);
|
|
||||||
}
|
|
||||||
t.append(tr);
|
|
||||||
});
|
|
||||||
cont.append(t);
|
|
||||||
}
|
|
||||||
async function cargar() {
|
|
||||||
$('meta').textContent = 'Actualizando…';
|
|
||||||
try {
|
|
||||||
const j = await api('/api/equipo?todos=1');
|
|
||||||
MIEMBROS = j.miembros || []; ADMIN = !!j.admin; USER = j.user || ''; PASS_MIN = j.pass_min || 8; AREAS = j.areas || AREAS; HTPASSWD_OK = j.htpasswd_ok !== false;
|
|
||||||
Kanban.usarEquipo(Object.fromEntries(MIEMBROS.map(m => [m.id, { nombre: m.nombre, activo: m.activo !== false, area: m.area }])));
|
|
||||||
pintar();
|
|
||||||
$('acciones').style.display = ADMIN ? '' : 'none';
|
|
||||||
const act = MIEMBROS.filter(m => m.activo !== false), con = act.filter(m => m.tiene_login).length;
|
|
||||||
$('meta').textContent = act.length + ' miembros activos · ' + con + ' con acceso al sitio · ' + (USER ? USER + (ADMIN ? ' (admin)' : '') : '');
|
|
||||||
if (ADMIN && !HTPASSWD_OK) mensaje('El servicio no encuentra el archivo de usuarios (HTPASSWD_FILE): se pueden editar miembros, pero no crear ni quitar accesos. Avisa a Leo.', false);
|
|
||||||
if (ADMIN && (j.logins_sin_miembro || []).length) mensaje('Cuentas con acceso que no están en el equipo: <b>' + j.logins_sin_miembro.map(esc).join(', ') + '</b>. Dales de alta como miembro (Nuevo usuario / miembro, sin marcar «crear acceso») para que puedan ser responsables.', false);
|
|
||||||
const q = new URLSearchParams(location.search).get('id'); if (q) { const m = MIEMBROS.find(x => x.id === q); if (m) detalle(m); }
|
|
||||||
} catch (e) { $('cont').innerHTML = ''; $('cont').append(el('div', { class: 'err', text: 'No se pudo cargar el equipo: ' + e.message })); $('meta').textContent = 'Error'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- pendientes de un miembro ----------
|
|
||||||
async function detalle(m) {
|
|
||||||
const sec = $('detalle'); sec.style.display = ''; sec.innerHTML = '';
|
|
||||||
sec.append(el('h2', { text: 'Pendientes de ' + m.nombre }), el('p', { class: 'sub', html: esc(m.rol || '') + ' · ' + esc(AREA[m.area] || m.area) + ' · <a href="kanban.html?responsable=' + encodeURIComponent(m.id) + '">Abrir en el tablero →</a>' }));
|
|
||||||
const lista = el('div', { text: 'Cargando…' }); sec.append(lista);
|
|
||||||
sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
try {
|
|
||||||
const j = await api('/api/tareas?responsable=' + encodeURIComponent(m.id));
|
|
||||||
const abiertas = (j.tareas || []).filter(t => t.estado !== 'hecho');
|
|
||||||
lista.innerHTML = '';
|
|
||||||
if (!abiertas.length) { lista.append(el('div', { class: 'vacio', text: 'Sin pendientes abiertos.' })); return; }
|
|
||||||
abiertas.forEach(t => {
|
|
||||||
const c = el('div', { class: 'card p-' + t.pista + (t.vencida ? ' vencida' : '') });
|
|
||||||
c.append(el('div', { class: 'tit', html: '<a href="kanban.html?id=' + encodeURIComponent(t.id) + '" style="color:inherit;text-decoration:none">' + esc(t.titulo) + '</a>' }));
|
|
||||||
c.append(el('div', { class: 'meta', html: '<span class="chip p-' + t.pista + '">' + esc(PISTAS[t.pista] || t.pista) + '</span><span class="chip">' + esc(ESTADOS[t.estado] || t.estado) + '</span>'
|
|
||||||
+ (t.fecha_limite ? '<span class="fecha' + (t.vencida ? ' mal' : '') + '">' + (t.vencida ? '⚠ venció ' : '📅 ') + esc(t.fecha_limite) + '</span>' : '')
|
|
||||||
+ ((t.responsable_ids || []).length > 1 ? '<span>👥 ' + esc(Kanban.nombresResp(t).join(', ')) + '</span>' : '') }));
|
|
||||||
lista.append(c);
|
|
||||||
});
|
|
||||||
} catch (e) { lista.textContent = 'No se pudieron leer los pendientes: ' + e.message; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- diálogos (admin) ----------
|
|
||||||
function abrirDialogo(titulo, cuerpoFn, botones) {
|
|
||||||
dlg.innerHTML = '';
|
|
||||||
const cab = el('div', { class: 'dcab' }); cab.append(el('h2', { text: titulo }));
|
|
||||||
const x = el('button', { type: 'button', text: '✕' }); x.addEventListener('click', () => dlg.close()); cab.append(x); dlg.append(cab);
|
|
||||||
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
|
||||||
const m = el('div', { class: 'msg' });
|
|
||||||
cuerpoFn(cuerpo, m); cuerpo.append(m);
|
|
||||||
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
|
||||||
const cancelar = el('button', { type: 'button', class: 'sec', text: botones.length ? 'Cancelar' : 'Cerrar' }); cancelar.addEventListener('click', () => dlg.close()); pie.append(cancelar);
|
|
||||||
botones.forEach(([txt, fn]) => { const b = el('button', { type: 'button', text: txt }); b.addEventListener('click', async () => { b.disabled = true; try { await fn(m, b, pie); } catch (e) { m.className = 'msg err'; m.textContent = e.message; } b.disabled = false; }); pie.append(b); });
|
|
||||||
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute('open', '');
|
|
||||||
}
|
|
||||||
function formMiembro(c, m, conAcceso) {
|
|
||||||
m = m || {}; const f = {};
|
|
||||||
f.id = el('input', { type: 'text', maxlength: '30', value: m.id || '', placeholder: 'adriana (minúsculas, sin acentos; será el usuario del sitio)', autocapitalize: 'none' }); if (m.id) f.id.disabled = true;
|
|
||||||
f.nombre = el('input', { type: 'text', maxlength: '80', value: m.nombre || '', placeholder: 'Nombre y apellido' });
|
|
||||||
f.rol = el('input', { type: 'text', maxlength: '120', value: m.rol || '', placeholder: 'Técnico de pruebas · Layout de PCB · Operaciones' });
|
|
||||||
f.area = el('select'); AREAS.forEach(a => f.area.append(el('option', { value: a, text: AREA[a] || a }))); f.area.value = m.area || 'produccion';
|
|
||||||
f.correo = el('input', { type: 'email', maxlength: '120', value: m.correo || '' });
|
|
||||||
f.telefono = el('input', { type: 'tel', maxlength: '40', value: m.telefono || '' });
|
|
||||||
const g = el('div', { class: 'dos-col' }); g.append(campo('Usuario (id)', f.id), campo('Nombre', f.nombre), campo('Rol', f.rol), campo('Área', f.area), campo('Correo', f.correo), campo('Teléfono', f.telefono)); c.append(g);
|
|
||||||
if (conAcceso) {
|
|
||||||
f.acceso = el('input', { type: 'checkbox', style: 'width:auto' }); f.acceso.checked = !m.tiene_login && HTPASSWD_OK; f.acceso.disabled = !HTPASSWD_OK;
|
|
||||||
const l = el('label', { class: 'chip-sel', style: 'margin-top:12px' }); l.append(f.acceso, document.createTextNode(' Crear acceso al sitio (usuario y contraseña)')); c.append(l);
|
|
||||||
f.pass = el('input', { type: 'text', autocomplete: 'off', placeholder: 'mínimo ' + PASS_MIN + ' caracteres; se muestra una sola vez' });
|
|
||||||
const cp = campo('Contraseña inicial', f.pass, 'La persona la cambia en Mi cuenta la primera vez que entre.'); c.append(cp);
|
|
||||||
const gen = el('button', { type: 'button', class: 'sec', text: 'Generar una', style: 'padding:6px 10px;font-size:13px' });
|
|
||||||
gen.addEventListener('click', () => { const A = 'abcdefghjkmnpqrstuvwxyz23456789'; let p = ''; const r = new Uint32Array(12); crypto.getRandomValues(r); r.forEach(v => p += A[v % A.length]); f.pass.value = p.slice(0, 4) + '-' + p.slice(4, 8) + '-' + p.slice(8, 12); });
|
|
||||||
cp.append(gen);
|
|
||||||
const sync = () => { cp.style.display = f.acceso.checked ? '' : 'none'; }; f.acceso.addEventListener('change', sync); sync();
|
|
||||||
if (!HTPASSWD_OK) c.append(el('div', { class: 'err', text: 'No se pueden crear accesos: el servicio no encuentra el archivo de usuarios.' }));
|
|
||||||
}
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
const leerMiembro = f => ({ nombre: f.nombre.value.trim(), rol: f.rol.value.trim(), area: f.area.value, correo: f.correo.value.trim(), telefono: f.telefono.value.trim() });
|
|
||||||
function nuevo(base) {
|
|
||||||
let f;
|
|
||||||
abrirDialogo(base ? 'Crear acceso para ' + base.nombre : 'Nuevo usuario / miembro', c => { f = formMiembro(c, base, true); if (base) { f.acceso.checked = true; f.acceso.dispatchEvent(new Event('change')); } },
|
|
||||||
[['Guardar', async (m, b, pie) => {
|
|
||||||
const id = Kanban.norm(f.id.value); const d = leerMiembro(f);
|
|
||||||
if (!id) throw new Error('Escribe el usuario (id).');
|
|
||||||
if (!d.nombre) throw new Error('Escribe el nombre.');
|
|
||||||
if (f.acceso.checked) {
|
|
||||||
if (f.pass.value.length < PASS_MIN) throw new Error('La contraseña inicial debe tener al menos ' + PASS_MIN + ' caracteres.');
|
|
||||||
const j = await api('/api/usuarios', json('POST', Object.assign({ usuario: id, password_inicial: f.pass.value }, d)));
|
|
||||||
const pw = f.pass.value;
|
|
||||||
dlg.querySelector('.dcuerpo').innerHTML = '';
|
|
||||||
dlg.querySelector('.dcuerpo').append(el('div', { class: 'pass-una-vez', html: '✓ Acceso creado para <b>' + esc(j.usuario) + '</b>.<br>Contraseña inicial (no se vuelve a mostrar): <code>' + esc(pw) + '</code><br><small>Pásasela en persona o por mensaje directo y dile que la cambie en Mi cuenta.</small>' }));
|
|
||||||
pie.innerHTML = ''; const ok = el('button', { type: 'button', text: 'Listo' }); ok.addEventListener('click', () => dlg.close()); pie.append(ok);
|
|
||||||
cargar(); return;
|
|
||||||
}
|
|
||||||
if (base) throw new Error('Marca «Crear acceso al sitio» para darle acceso.');
|
|
||||||
await api('/api/equipo', json('POST', Object.assign({ id }, d)));
|
|
||||||
dlg.close(); mensaje('Miembro ' + esc(d.nombre) + ' creado (sin acceso al sitio).', true); cargar();
|
|
||||||
}]]);
|
|
||||||
}
|
|
||||||
function editar(m) {
|
|
||||||
let f;
|
|
||||||
abrirDialogo('Editar ' + m.nombre, c => {
|
|
||||||
f = formMiembro(c, m, false);
|
|
||||||
const h = el('div', { class: 'hist' });
|
|
||||||
h.append(el('div', { html: '<b>Creado</b> por ' + esc(m.creado_por) + ' el ' + esc(fmt(m.creado_en)) + (m.tiene_login ? ' · tiene acceso al sitio' : ' · sin acceso al sitio') }));
|
|
||||||
(m.historial || []).slice().reverse().slice(0, 10).forEach(x => h.append(el('div', { html: esc(fmt(x.cuando)) + ' · <b>' + esc(x.quien) + '</b> · ' + esc(x.cambio) })));
|
|
||||||
c.append(h);
|
|
||||||
}, [['Guardar', async () => {
|
|
||||||
const d = leerMiembro(f); const cambios = {};
|
|
||||||
Object.keys(d).forEach(k => { if (d[k] !== (m[k] || '')) cambios[k] = d[k]; });
|
|
||||||
if (Object.keys(cambios).length) await api('/api/equipo/miembro?id=' + encodeURIComponent(m.id), json('PATCH', cambios));
|
|
||||||
dlg.close(); mensaje('Guardado.', true); cargar();
|
|
||||||
}]]);
|
|
||||||
}
|
|
||||||
async function desactivar(m) {
|
|
||||||
if (!confirm('¿Desactivar a ' + m.nombre + '? Deja de aparecer en las listas de responsables; sus pendientes y su historial se quedan.' + (m.tiene_login ? ' Su acceso al sitio NO se quita (usa «Quitar acceso» para eso).' : ''))) return;
|
|
||||||
try { await api('/api/equipo/miembro?id=' + encodeURIComponent(m.id), json('PATCH', { activo: false })); mensaje(m.nombre + ' desactivado.', true); cargar(); } catch (e) { mensaje(e.message, false); }
|
|
||||||
}
|
|
||||||
async function reactivar(m) {
|
|
||||||
try { await api('/api/equipo/miembro?id=' + encodeURIComponent(m.id), json('PATCH', { activo: true })); mensaje(m.nombre + ' reactivado.', true); cargar(); } catch (e) { mensaje(e.message, false); }
|
|
||||||
}
|
|
||||||
async function quitarAcceso(m) {
|
|
||||||
if (!confirm('¿Quitar el acceso al sitio a ' + m.nombre + ' (' + m.id + ')? Ya no podrá entrar y quedará como inactivo. Sus actas y pendientes se conservan.')) return;
|
|
||||||
try { await api('/api/usuario?u=' + encodeURIComponent(m.id), { method: 'DELETE' }); mensaje('Acceso de ' + m.id + ' retirado.', true); cargar(); } catch (e) { mensaje(e.message, false); }
|
|
||||||
}
|
|
||||||
$('bNuevo').addEventListener('click', () => nuevo(null));
|
|
||||||
cargar();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
205
docs/tableros/equipo.md
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
---
|
||||||
|
title: "Equipo"
|
||||||
|
description: "Quién es quién: miembros, área, acceso al sitio y pendientes de cada quien."
|
||||||
|
template: tablero.html
|
||||||
|
hide:
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="tb tb-equipo">
|
||||||
|
<header class="tb-cab">
|
||||||
|
<div class="tb-cab__t"><h1>Equipo</h1><span class="meta" id="meta">Cargando…</span></div>
|
||||||
|
<div class="tb-cab__acc"><a class="boton sec" href="/cuenta.html"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="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"/></svg>Mi cuenta</a></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<details class="info tb-como">
|
||||||
|
<summary>Cómo se usa</summary>
|
||||||
|
<ol>
|
||||||
|
<li>Aquí está <b>quién es quién</b>: nombre, rol, área, si tiene acceso al sitio y cuántos pendientes abiertos y vencidos trae cada quien.</li>
|
||||||
|
<li><b>Todo pendiente tiene responsable.</b> Al crearlo en el tablero se elige de esta lista; puede haber más de uno.</li>
|
||||||
|
<li>Un miembro <b>sin acceso al sitio</b> también puede ser responsable (Adriana hoy): el pendiente se le asigna igual y alguien con acceso lo mueve por ella.</li>
|
||||||
|
<li>Toca un nombre para ver sus pendientes abiertos y abrir el tablero filtrado.</li>
|
||||||
|
<li>Los <b>accesos los crea Leo aquí</b> (botón «Nuevo usuario / miembro»), ya no con <code>htpasswd</code> en el servidor. La contraseña inicial se muestra <b>una sola vez</b>: pásala en persona o por mensaje directo; la persona la cambia en <a href="/cuenta.html">Mi cuenta</a>.</li>
|
||||||
|
<li>«Desactivar» oculta al miembro de las listas sin borrar nada; «Quitar acceso» le cierra la entrada al sitio y lo desactiva. Nadie se borra: el historial se queda.</li>
|
||||||
|
<li>Si no tienes acceso o no apareces, pídeselo a Leo.</li>
|
||||||
|
</ol>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="msg" id="aviso"></div>
|
||||||
|
<div id="acciones" style="display:none;margin:0 0 12px"><button id="bNuevo"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Nuevo usuario o miembro</button></div>
|
||||||
|
<div id="cont" class="tblwrap"></div>
|
||||||
|
<section class="bloque mis-pend" id="detalle" style="display:none"></section>
|
||||||
|
|
||||||
|
<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 AREA = { direccion: 'Dirección', operaciones: 'Operaciones', ingenieria: 'Ingeniería', layout: 'Layout', produccion: 'Producción', pruebas: 'Pruebas', fabricacion: 'Fabricación', software: 'Software', campo: 'Campo', asistente: 'Asistente' };
|
||||||
|
const PISTAS = Kanban.PISTAS, ESTADOS = Object.fromEntries(Kanban.ESTADOS);
|
||||||
|
let MIEMBROS = [], ADMIN = false, USER = '', PASS_MIN = 8, AREAS = Object.keys(AREA), HTPASSWD_OK = true;
|
||||||
|
const dlg = $('dlg'), aviso = $('aviso');
|
||||||
|
function mensaje(t, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.innerHTML = t; clearTimeout(aviso._t); aviso._t = setTimeout(() => aviso.className = 'msg', bien ? 5000 : 10000); }
|
||||||
|
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, ayuda) { const d = el('div'); d.append(el('label', { text: l }), i); if (ayuda) d.append(el('div', { style: 'font-size:12.5px;color:var(--tinta2);margin-top:3px', text: ayuda })); return d; }
|
||||||
|
|
||||||
|
function pintar() {
|
||||||
|
const cont = $('cont'); cont.innerHTML = '';
|
||||||
|
if (!MIEMBROS.length) { cont.append(el('div', { class: 'vacio', text: 'Todavía no hay miembros. Corre la siembra o crea el primero.' })); return; }
|
||||||
|
const t = el('table', { class: 't' });
|
||||||
|
t.innerHTML = '<tr><th>Nombre</th><th>Rol</th><th>Área</th><th>Acceso</th><th>Admin</th><th class="num">Abiertos</th><th class="num">Vencidos</th><th>Activo</th>' + (ADMIN ? '<th></th>' : '') + '</tr>';
|
||||||
|
MIEMBROS.forEach(m => {
|
||||||
|
const tr = el('tr', { class: m.activo === false ? 'inactivo' : '' });
|
||||||
|
const nom = el('span', { class: 'nom' }, Kanban.avatar(m.id), document.createTextNode(m.nombre)); nom.addEventListener('click', () => detalle(m));
|
||||||
|
tr.append(el('td', null, nom, el('span', { class: 'id', text: m.id + (m.correo ? ' · ' + m.correo : '') + (m.telefono ? ' · ' + m.telefono : '') })));
|
||||||
|
tr.append(el('td', { text: m.rol || '—' }));
|
||||||
|
tr.append(el('td', null, el('span', { class: 'area', text: AREA[m.area] || m.area })));
|
||||||
|
tr.append(el('td', { html: m.tiene_login ? '<span class="pill ok">sí</span>' : '<span class="pill pend">no</span>' }));
|
||||||
|
tr.append(el('td', { html: m.es_admin ? '<span class="pill info">admin</span>' : '' }));
|
||||||
|
const p = m.pendientes || {};
|
||||||
|
tr.append(el('td', { class: 'num', html: p.abiertos ? '<a href="/tableros/pendientes/?responsable=' + encodeURIComponent(m.id) + '"><b>' + p.abiertos + '</b></a>' : '0' }));
|
||||||
|
tr.append(el('td', { class: 'num', html: p.vencidas ? '<span style="color:var(--falla);font-weight:700">' + p.vencidas + '</span>' : '0' }));
|
||||||
|
tr.append(el('td', { html: m.activo === false ? '<span class="pill falla">inactivo</span>' : '<span class="pill ok">activo</span>' }));
|
||||||
|
if (ADMIN) {
|
||||||
|
const acc = el('td', { style: 'white-space:nowrap' });
|
||||||
|
const be = el('button', { class: 'sec', text: 'Editar' }); be.addEventListener('click', () => editar(m)); acc.append(be);
|
||||||
|
if (m.activo !== false) { const bd = el('button', { class: 'sec', text: 'Desactivar' }); bd.addEventListener('click', () => desactivar(m)); acc.append(bd); }
|
||||||
|
else { const ba = el('button', { class: 'sec', text: 'Reactivar' }); ba.addEventListener('click', () => reactivar(m)); acc.append(ba); }
|
||||||
|
if (m.tiene_login && m.id.toLowerCase() !== USER.toLowerCase()) { const bq = el('button', { class: 'peligro', text: 'Quitar acceso' }); bq.addEventListener('click', () => quitarAcceso(m)); acc.append(bq); }
|
||||||
|
if (!m.tiene_login && m.activo !== false) { const bc = el('button', { class: 'sec', text: 'Crear acceso' }); bc.addEventListener('click', () => nuevo(m)); acc.append(bc); }
|
||||||
|
tr.append(acc);
|
||||||
|
}
|
||||||
|
t.append(tr);
|
||||||
|
});
|
||||||
|
cont.append(t);
|
||||||
|
}
|
||||||
|
async function cargar() {
|
||||||
|
$('meta').textContent = 'Actualizando…';
|
||||||
|
try {
|
||||||
|
const j = await api('/api/equipo?todos=1');
|
||||||
|
MIEMBROS = j.miembros || []; ADMIN = !!j.admin; USER = j.user || ''; PASS_MIN = j.pass_min || 8; AREAS = j.areas || AREAS; HTPASSWD_OK = j.htpasswd_ok !== false;
|
||||||
|
Kanban.usarEquipo(Object.fromEntries(MIEMBROS.map(m => [m.id, { nombre: m.nombre, activo: m.activo !== false, area: m.area }])));
|
||||||
|
pintar();
|
||||||
|
$('acciones').style.display = ADMIN ? '' : 'none';
|
||||||
|
const act = MIEMBROS.filter(m => m.activo !== false), con = act.filter(m => m.tiene_login).length;
|
||||||
|
$('meta').textContent = act.length + ' miembros activos · ' + con + ' con acceso al sitio · ' + (USER ? USER + (ADMIN ? ' (admin)' : '') : '');
|
||||||
|
if (ADMIN && !HTPASSWD_OK) mensaje('El servicio no encuentra el archivo de usuarios (HTPASSWD_FILE): se pueden editar miembros, pero no crear ni quitar accesos. Avisa a Leo.', false);
|
||||||
|
if (ADMIN && (j.logins_sin_miembro || []).length) mensaje('Cuentas con acceso que no están en el equipo: <b>' + j.logins_sin_miembro.map(esc).join(', ') + '</b>. Dales de alta como miembro (Nuevo usuario / miembro, sin marcar «crear acceso») para que puedan ser responsables.', false);
|
||||||
|
const q = new URLSearchParams(location.search).get('id'); if (q) { const m = MIEMBROS.find(x => x.id === q); if (m) detalle(m); }
|
||||||
|
} catch (e) { $('cont').innerHTML = ''; $('cont').append(el('div', { class: 'err', text: 'No se pudo cargar el equipo: ' + e.message })); $('meta').textContent = 'Error'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- pendientes de un miembro ----------
|
||||||
|
async function detalle(m) {
|
||||||
|
const sec = $('detalle'); sec.style.display = ''; sec.innerHTML = '';
|
||||||
|
sec.append(el('h2', { text: 'Pendientes de ' + m.nombre }), el('p', { class: 'sub', html: esc(m.rol || '') + ' · ' + esc(AREA[m.area] || m.area) + ' · <a href="/tableros/pendientes/?responsable=' + encodeURIComponent(m.id) + '">Abrir en el tablero →</a>' }));
|
||||||
|
const lista = el('div', { text: 'Cargando…' }); sec.append(lista);
|
||||||
|
sec.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
try {
|
||||||
|
const j = await api('/api/tareas?responsable=' + encodeURIComponent(m.id));
|
||||||
|
const abiertas = (j.tareas || []).filter(t => t.estado !== 'hecho');
|
||||||
|
lista.innerHTML = '';
|
||||||
|
if (!abiertas.length) { lista.append(el('div', { class: 'vacio', text: 'Sin pendientes abiertos.' })); return; }
|
||||||
|
abiertas.forEach(t => {
|
||||||
|
const c = el('div', { class: 'card p-' + t.pista + (t.vencida ? ' vencida' : '') });
|
||||||
|
c.append(el('div', { class: 'tit', html: '<a href="/tableros/pendientes/?id=' + encodeURIComponent(t.id) + '" style="color:inherit;text-decoration:none">' + esc(t.titulo) + '</a>' }));
|
||||||
|
c.append(el('div', { class: 'meta', html: '<span class="chip p-' + t.pista + '">' + esc(PISTAS[t.pista] || t.pista) + '</span><span class="chip">' + esc(ESTADOS[t.estado] || t.estado) + '</span>'
|
||||||
|
+ (t.fecha_limite ? '<span class="fecha' + (t.vencida ? ' mal' : '') + '">' + (t.vencida ? 'Venció ' : '') + esc(t.fecha_limite) + '</span>' : '')
|
||||||
|
+ ((t.responsable_ids || []).length > 1 ? '<span>Con ' + esc(Kanban.nombresResp(t).join(', ')) + '</span>' : '') }));
|
||||||
|
lista.append(c);
|
||||||
|
});
|
||||||
|
} catch (e) { lista.textContent = 'No se pudieron leer los pendientes: ' + e.message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- diálogos (admin) ----------
|
||||||
|
function abrirDialogo(titulo, cuerpoFn, botones) {
|
||||||
|
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 m = el('div', { class: 'msg' });
|
||||||
|
cuerpoFn(cuerpo, m); cuerpo.append(m);
|
||||||
|
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
||||||
|
const cancelar = el('button', { type: 'button', class: 'sec', text: botones.length ? 'Cancelar' : 'Cerrar' }); cancelar.addEventListener('click', () => dlg.close()); pie.append(cancelar);
|
||||||
|
botones.forEach(([txt, fn]) => { const b = el('button', { type: 'button', text: txt }); b.addEventListener('click', async () => { b.disabled = true; try { await fn(m, b, pie); } catch (e) { m.className = 'msg err'; m.textContent = e.message; } b.disabled = false; }); pie.append(b); });
|
||||||
|
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute('open', '');
|
||||||
|
}
|
||||||
|
function formMiembro(c, m, conAcceso) {
|
||||||
|
m = m || {}; const f = {};
|
||||||
|
f.id = el('input', { type: 'text', maxlength: '30', value: m.id || '', placeholder: 'adriana (minúsculas, sin acentos; será el usuario del sitio)', autocapitalize: 'none' }); if (m.id) f.id.disabled = true;
|
||||||
|
f.nombre = el('input', { type: 'text', maxlength: '80', value: m.nombre || '', placeholder: 'Nombre y apellido' });
|
||||||
|
f.rol = el('input', { type: 'text', maxlength: '120', value: m.rol || '', placeholder: 'Técnico de pruebas · Layout de PCB · Operaciones' });
|
||||||
|
f.area = el('select'); AREAS.forEach(a => f.area.append(el('option', { value: a, text: AREA[a] || a }))); f.area.value = m.area || 'produccion';
|
||||||
|
f.correo = el('input', { type: 'email', maxlength: '120', value: m.correo || '' });
|
||||||
|
f.telefono = el('input', { type: 'tel', maxlength: '40', value: m.telefono || '' });
|
||||||
|
const g = el('div', { class: 'dos-col' }); g.append(campo('Usuario (id)', f.id), campo('Nombre', f.nombre), campo('Rol', f.rol), campo('Área', f.area), campo('Correo', f.correo), campo('Teléfono', f.telefono)); c.append(g);
|
||||||
|
if (conAcceso) {
|
||||||
|
f.acceso = el('input', { type: 'checkbox', style: 'width:auto' }); f.acceso.checked = !m.tiene_login && HTPASSWD_OK; f.acceso.disabled = !HTPASSWD_OK;
|
||||||
|
const l = el('label', { class: 'chip-sel', style: 'margin-top:12px' }); l.append(f.acceso, document.createTextNode(' Crear acceso al sitio (usuario y contraseña)')); c.append(l);
|
||||||
|
f.pass = el('input', { type: 'text', autocomplete: 'off', placeholder: 'mínimo ' + PASS_MIN + ' caracteres; se muestra una sola vez' });
|
||||||
|
const cp = campo('Contraseña inicial', f.pass, 'La persona la cambia en Mi cuenta la primera vez que entre.'); c.append(cp);
|
||||||
|
const gen = el('button', { type: 'button', class: 'sec', text: 'Generar una', style: 'padding:6px 10px;font-size:13px' });
|
||||||
|
gen.addEventListener('click', () => { const A = 'abcdefghjkmnpqrstuvwxyz23456789'; let p = ''; const r = new Uint32Array(12); crypto.getRandomValues(r); r.forEach(v => p += A[v % A.length]); f.pass.value = p.slice(0, 4) + '-' + p.slice(4, 8) + '-' + p.slice(8, 12); });
|
||||||
|
cp.append(gen);
|
||||||
|
const sync = () => { cp.style.display = f.acceso.checked ? '' : 'none'; }; f.acceso.addEventListener('change', sync); sync();
|
||||||
|
if (!HTPASSWD_OK) c.append(el('div', { class: 'err', text: 'No se pueden crear accesos: el servicio no encuentra el archivo de usuarios.' }));
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
const leerMiembro = f => ({ nombre: f.nombre.value.trim(), rol: f.rol.value.trim(), area: f.area.value, correo: f.correo.value.trim(), telefono: f.telefono.value.trim() });
|
||||||
|
function nuevo(base) {
|
||||||
|
let f;
|
||||||
|
abrirDialogo(base ? 'Crear acceso para ' + base.nombre : 'Nuevo usuario / miembro', c => { f = formMiembro(c, base, true); if (base) { f.acceso.checked = true; f.acceso.dispatchEvent(new Event('change')); } },
|
||||||
|
[['Guardar', async (m, b, pie) => {
|
||||||
|
const id = Kanban.norm(f.id.value); const d = leerMiembro(f);
|
||||||
|
if (!id) throw new Error('Escribe el usuario (id).');
|
||||||
|
if (!d.nombre) throw new Error('Escribe el nombre.');
|
||||||
|
if (f.acceso.checked) {
|
||||||
|
if (f.pass.value.length < PASS_MIN) throw new Error('La contraseña inicial debe tener al menos ' + PASS_MIN + ' caracteres.');
|
||||||
|
const j = await api('/api/usuarios', json('POST', Object.assign({ usuario: id, password_inicial: f.pass.value }, d)));
|
||||||
|
const pw = f.pass.value;
|
||||||
|
dlg.querySelector('.dcuerpo').innerHTML = '';
|
||||||
|
dlg.querySelector('.dcuerpo').append(el('div', { class: 'pass-una-vez', html: 'Acceso creado para <b>' + esc(j.usuario) + '</b>.<br>Contraseña inicial (no se vuelve a mostrar): <code>' + esc(pw) + '</code><br><small>Pásasela en persona o por mensaje directo y dile que la cambie en Mi cuenta.</small>' }));
|
||||||
|
pie.innerHTML = ''; const ok = el('button', { type: 'button', text: 'Listo' }); ok.addEventListener('click', () => dlg.close()); pie.append(ok);
|
||||||
|
cargar(); return;
|
||||||
|
}
|
||||||
|
if (base) throw new Error('Marca «Crear acceso al sitio» para darle acceso.');
|
||||||
|
await api('/api/equipo', json('POST', Object.assign({ id }, d)));
|
||||||
|
dlg.close(); mensaje('Miembro ' + esc(d.nombre) + ' creado (sin acceso al sitio).', true); cargar();
|
||||||
|
}]]);
|
||||||
|
}
|
||||||
|
function editar(m) {
|
||||||
|
let f;
|
||||||
|
abrirDialogo('Editar ' + m.nombre, c => {
|
||||||
|
f = formMiembro(c, m, false);
|
||||||
|
const h = el('div', { class: 'hist' });
|
||||||
|
h.append(el('div', { html: '<b>Creado</b> por ' + esc(m.creado_por) + ' el ' + esc(fmt(m.creado_en)) + (m.tiene_login ? ' · tiene acceso al sitio' : ' · sin acceso al sitio') }));
|
||||||
|
(m.historial || []).slice().reverse().slice(0, 10).forEach(x => h.append(el('div', { html: esc(fmt(x.cuando)) + ' · <b>' + esc(x.quien) + '</b> · ' + esc(x.cambio) })));
|
||||||
|
c.append(h);
|
||||||
|
}, [['Guardar', async () => {
|
||||||
|
const d = leerMiembro(f); const cambios = {};
|
||||||
|
Object.keys(d).forEach(k => { if (d[k] !== (m[k] || '')) cambios[k] = d[k]; });
|
||||||
|
if (Object.keys(cambios).length) await api('/api/equipo/miembro?id=' + encodeURIComponent(m.id), json('PATCH', cambios));
|
||||||
|
dlg.close(); mensaje('Guardado.', true); cargar();
|
||||||
|
}]]);
|
||||||
|
}
|
||||||
|
async function desactivar(m) {
|
||||||
|
if (!confirm('¿Desactivar a ' + m.nombre + '? Deja de aparecer en las listas de responsables; sus pendientes y su historial se quedan.' + (m.tiene_login ? ' Su acceso al sitio NO se quita (usa «Quitar acceso» para eso).' : ''))) return;
|
||||||
|
try { await api('/api/equipo/miembro?id=' + encodeURIComponent(m.id), json('PATCH', { activo: false })); mensaje(m.nombre + ' desactivado.', true); cargar(); } catch (e) { mensaje(e.message, false); }
|
||||||
|
}
|
||||||
|
async function reactivar(m) {
|
||||||
|
try { await api('/api/equipo/miembro?id=' + encodeURIComponent(m.id), json('PATCH', { activo: true })); mensaje(m.nombre + ' reactivado.', true); cargar(); } catch (e) { mensaje(e.message, false); }
|
||||||
|
}
|
||||||
|
async function quitarAcceso(m) {
|
||||||
|
if (!confirm('¿Quitar el acceso al sitio a ' + m.nombre + ' (' + m.id + ')? Ya no podrá entrar y quedará como inactivo. Sus actas y pendientes se conservan.')) return;
|
||||||
|
try { await api('/api/usuario?u=' + encodeURIComponent(m.id), { method: 'DELETE' }); mensaje('Acceso de ' + m.id + ' retirado.', true); cargar(); } catch (e) { mensaje(e.message, false); }
|
||||||
|
}
|
||||||
|
$('bNuevo').addEventListener('click', () => nuevo(null));
|
||||||
|
cargar();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@ -4,36 +4,35 @@ title: "Tableros"
|
|||||||
|
|
||||||
# Tableros
|
# Tableros
|
||||||
|
|
||||||
Las páginas que abres para saber **dónde está el proyecto hoy**: qué tareas hay y de quién son, en qué estado va cada versión de tarjeta, cuántas unidades llevamos contra el plan de 570 y qué pasó con cada serie. Se alimentan solas de las actas y de los registros del servidor (`/api/…`); no hay nada que copiar a mano.
|
Las páginas que abres para saber **dónde está el proyecto hoy**: qué pendientes hay y de quién son, en qué estado va cada versión de tarjeta, cuántas unidades llevamos contra el plan de 570 y qué pasó con cada serie. Se alimentan solas de las actas y de los registros del servidor (`/api/…`); no hay nada que copiar a mano. Todos viven en el menú de la izquierda, bajo **Tableros**.
|
||||||
|
|
||||||
## ¿Qué quiero ver?
|
## ¿Qué quiero ver?
|
||||||
|
|
||||||
| Quiero… | Tablero | Qué muestra |
|
| Quiero… | Tablero | Qué muestra |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Las **tareas** del equipo y quién las tiene | [Kanban](kanban.html) | Columnas *Por hacer · En curso · Bloqueado · Hecho*; pistas Ingeniería / Producción / Mecánica / Compras / Campo; responsable, fecha límite, ligas a documentos y actas; historial de movimientos |
|
| Los **pendientes** del equipo y quién los tiene | [Pendientes](pendientes.md) | Columnas *Por hacer · En curso · Bloqueado · Hecho*; pistas Ingeniería, Producción, Mecánica, Compras y Campo; responsable, fecha límite, ligas a documentos y actas; historial de cada cambio. «Solo los míos» deja solo los tuyos |
|
||||||
| **Dónde está el hardware** (V5.5 → V5.6 → V5.7 → v6) | [Ingeniería](ingenieria.html) | Una tarjeta por versión con su estado, pendientes con responsable y fecha, últimas decisiones y filosofía de diseño; las tareas de la pista Ingeniería |
|
| **Dónde está el hardware** (V5.5 → V5.6 → V5.7 → v6) | [Ingeniería](ingenieria.md) | Una tarjeta por versión con su estado, siguiente paso, responsables y documentos; los pendientes de la pista Ingeniería; últimas decisiones y filosofía de diseño |
|
||||||
| **Cuántas unidades** llevamos y cuántas faltan | [Producción](produccion.html) | Plan 113 V5.6 · 327 V5.7 · 130 v6 contra las unidades reales por etapa (tarjeta → módulos → ensamble → envío → instalación); embudo por paso; bloqueos (caja plástica / cable Ethernet) |
|
| **Cuántas unidades** llevamos y cuántas faltan | [Producción](produccion.md) | Plan 113 V5.6 · 327 V5.7 · 130 v6 contra las unidades reales por etapa; embudo por paso; bloqueos; alertas de inventario |
|
||||||
| Qué **tarjeta, módulo o unidad** pasó cada acta | [Estado de actas](../tablero.html) | Series V5-NN, ARD-NNN, RTK-NNNN, V4-NN; parejas integradas; unidades ensambladas. Pendiente = liga al acta |
|
| Qué **tarjeta, módulo o unidad** pasó cada acta | [Actas](actas.md) | Series V5-NN, ARD-NNN, RTK-NNNN y V4-NN; parejas integradas; unidades ensambladas. Pendiente = liga al acta |
|
||||||
| Las **visitas de diagnóstico** en los puertos | [Tablero de campo](../diagnostico/tablero-campo.html) | Todas las actas de diagnóstico en campo, con fotos |
|
| Qué **material** hay y qué falta | [Inventario](inventario.md) | Existencias, mínimos y cada movimiento con quién y por qué |
|
||||||
| **Todo sobre una serie** | [Expediente de una unidad](../unidad.html) | Cada acta, foto y log de una unidad en una línea de tiempo, de la tarjeta al sitio |
|
| Qué hay **en cada puerto** | [Sitios](sitio.md) | Unidades instaladas, envíos, diagnósticos, daños y contactos de ICAVE, LZC, EIT y TIMSA |
|
||||||
|
| **Quién es quién** | [Equipo](equipo.md) | Miembros, área, acceso al sitio y pendientes abiertos y vencidos de cada quien. El administrador crea aquí los accesos |
|
||||||
|
| **Todo sobre una serie** | [Expediente de unidad](unidad.md) | Cada acta, foto y log de una unidad en una línea de tiempo, de la tarjeta al sitio |
|
||||||
|
| Las **visitas de diagnóstico** | [Visitas de campo](campo.md) | Todas las actas de diagnóstico en campo y en banco, con fotos |
|
||||||
|
|
||||||
## Cómo se usan
|
## Cómo se usan
|
||||||
|
|
||||||
1. Las tareas se mueven arrastrando entre columnas. Cualquier usuario autenticado crea y mueve; **borrar y reabrir** es de administradores (`ACTAS_ADMINS`).
|
1. Los pendientes se mueven arrastrando entre columnas (en el teléfono, con «Mover a…»). Cualquier usuario autenticado crea y mueve; **borrar y reabrir** es de administradores (`ACTAS_ADMINS`).
|
||||||
2. Cada tarjeta del Kanban lleva **pista**, **responsable** (leo, michelle, aldair, jesús, alan, daniel, claude), **fecha límite** y ligas. Si una tarea depende de otra, escríbelo en las notas y déjala en *Bloqueado*.
|
2. Cada pendiente lleva **pista**, **responsable** (uno o varios del [Equipo](equipo.md)), **fecha límite** y ligas. Si depende de otro, márcalo en «Espera a que terminen» y, si no puedes avanzar, déjalo en *Bloqueado*.
|
||||||
3. Los tableros de Ingeniería y Producción son **vistas** del mismo Kanban filtradas por pista, más sus bloques fijos (versiones, cantidades).
|
3. Los tableros de Ingeniería y Producción son **vistas** de los mismos pendientes filtrados por pista, más sus bloques fijos (versiones, cantidades).
|
||||||
4. Las cantidades del plan de producción (versión → objetivo → fecha) las edita un administrador en el propio tablero; los conteos reales salen de las actas.
|
4. Las cantidades del plan de producción (versión → objetivo → fecha) las edita un administrador en el propio tablero; los conteos reales salen de las actas.
|
||||||
5. El Kanban sustituye al *Task Tracker* de Notion, que queda como histórico (sus tareas se importaron una vez).
|
5. Los pendientes sustituyen al *Task Tracker* de Notion, que queda como histórico (sus tareas se importaron una vez).
|
||||||
|
|
||||||
## [Equipo](equipo.html)
|
Las direcciones viejas (`tableros/kanban.html`, `unidad.html?serie=…`, `sitios/sitio.html?id=…` y las demás) siguen sirviendo: llevan solas a la página nueva.
|
||||||
|
|
||||||
Quién es quién y qué tiene cada uno: miembros, área, si tienen acceso al sitio y sus pendientes abiertos y vencidos. El administrador crea aquí los accesos (ya no por `htpasswd` en el servidor).
|
|
||||||
|
|
||||||
## Relacionados
|
## Relacionados
|
||||||
|
|
||||||
- [Detalle por tarjeta y módulo](../tablero.html) — qué serie pasó cada acta del lote actual.
|
- [Política de inventario](../inventario/index.md): qué se cuenta, mínimos y cómo se registra.
|
||||||
- [Inventario](../inventario/index.md) — stock, mínimos y movimientos.
|
- [Sitios y puertos](../sitios/index.md): la lista de sitios y cómo se da de alta uno.
|
||||||
- [Sitios](../sitios/index.md) — unidades instaladas por puerto.
|
- [Bitácora](../bitacora.md): los documentos por fecha.
|
||||||
- [Expediente de una unidad](../unidad.html) — todo lo registrado para una serie.
|
- [Historia del proyecto](../historia/index.md): jun → oct 2026 con ligas.
|
||||||
- [Historia del proyecto](../historia/index.md) — jun → oct 2026 con ligas.
|
|
||||||
|
|
||||||
|
|||||||
@ -1,125 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
<!-- Desde el 20-oct-2026 este tablero vive dentro del sitio: docs/tableros/ingenieria.md → /tableros/ingenieria/
|
||||||
|
Este archivo solo redirige (conserva ?serie=, ?id=, ?pista=… y #…) para que las ligas viejas
|
||||||
|
guardadas en pendientes y actas sigan sirviendo. No se edita: los cambios van en tableros/ingenieria.md. -->
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Tablero de ingeniería — EinarBoard</title>
|
<meta name="robots" content="noindex">
|
||||||
<link rel="stylesheet" href="tablero.css">
|
<title>Ingeniería - Drogheda</title>
|
||||||
|
<script>location.replace('/tableros/ingenieria/' + location.search + location.hash);</script>
|
||||||
|
<meta http-equiv="refresh" content="0; url=/tableros/ingenieria/">
|
||||||
|
<style>body{margin:0;padding:24px;background:#0E1524;color:#E7ECF5;font:16px/1.5 system-ui,sans-serif}a{color:#7FE9D8}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<p>Este tablero ahora está dentro del sitio: <a href="/tableros/ingenieria/">Ingeniería</a>.</p>
|
||||||
<header class="top">
|
|
||||||
<div><h1>Tablero de ingeniería — ¿dónde está el hardware?</h1><div class="meta" id="meta">V5.5 · V5.6 · V5.7 · v6 — estado al 19-oct-2026</div></div>
|
|
||||||
<nav>
|
|
||||||
<a href="../">← Documentación</a>
|
|
||||||
<a href="kanban.html">Pendientes</a>
|
|
||||||
<a class="activo" href="ingenieria.html">Ingeniería</a>
|
|
||||||
<a href="produccion.html">Producción</a>
|
|
||||||
<a href="../hardware/">Tarjetas y hardware</a>
|
|
||||||
<a href="equipo.html">👥 Equipo</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<details class="como">
|
|
||||||
<summary>Cómo se usa</summary>
|
|
||||||
<ol>
|
|
||||||
<li>Arriba, una tarjeta por versión de EinarBoard: estado, siguiente paso, responsable y sus documentos. Es texto fijo: cuando cambie una decisión, pide a Leo o a Claude que lo actualice aquí.</li>
|
|
||||||
<li>En medio, los <b>pendientes de la pista Ingeniería</b>: son los mismos del <a href="kanban.html">tablero de pendientes</a>, filtrados. Puedes moverlos desde aquí.</li>
|
|
||||||
<li>Abajo, las últimas decisiones con fecha y la filosofía de diseño. Antes de proponer un cambio, revisa que no contradiga una decisión ya tomada.</li>
|
|
||||||
<li>Para una tarjeta nueva o un cambio de pedido, primero el pendiente, luego el trabajo: así queda el rastro de quién y cuándo.</li>
|
|
||||||
</ol>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div class="versiones" id="versiones"></div>
|
|
||||||
|
|
||||||
<section class="bloque">
|
|
||||||
<h2>Pendientes de ingeniería</h2>
|
|
||||||
<p class="sub">Pista Ingeniería del tablero. Arrastra o usa «Mover a…»; el historial anota quién lo hizo. <a href="kanban.html">Ver todas las pistas →</a></p>
|
|
||||||
<div id="kb"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="bloque">
|
|
||||||
<h2>Últimas decisiones</h2>
|
|
||||||
<p class="sub">Lo decidido manda hasta que otra decisión con fecha posterior lo cambie. Fuente: notas del proyecto; detalle en la sección de hardware.</p>
|
|
||||||
<ul class="decisiones">
|
|
||||||
<li><b>19-oct (19:30)</b> — <b>V5.6: qué es crítico y qué se puede quedar</b> (archivo de trabajo de las 18:59, sobre T30). <b>Un solo crítico, nuevo:</b> U6 (LTC4040) no tiene capacitor en sus pines VSYS; el más cercano, C34, está a 11.5 mm (viene desde la V5.5). La hoja de datos lo pone por encima de todo en el layout. Se agrega <b>C129</b> (10 µF 0603, C19702) junto a los pines 1/24, sin mover piezas. Regresar 2 vías de <code>/MAIN5V</code> y 2 de <code>/3V3_MODEM</code> que se perdieron al guardar. L7 lejos de U7 y lo heredado de la V5.5 (L6, L8, Dbk1) se quedan para el piloto, y se miden en el banco. <a href="../hardware/v56-clasificacion-19oct/">Clasificación</a></li>
|
|
||||||
<li><b>19-oct (17:10)</b> — <b>Re-auditoría V5.6 / V5.7 / v6</b> sobre la pasada 3 de Michelle (HBox_PCB <code>4f4681f</code>). <b>V5.6 casi lista</b>: <code>/BAT_PLUS</code> fuera de capas internas, <code>/3V3_MODEM</code> a 1.0 mm, 4 vías en el EP de U7, sin cobre suelto; faltan T30 + ≈ 1 h (L7/Dbk2, salida VSYS de U6, 3 vías, exclusiones del DRC). <b>V5.7 NO LISTA</b>: ya replica la V5.6, pero <code>/SYS5V</code> va 10.9 mm por In2 a 0.5 mm bajo K1 (bloqueante) y el lazo de entrada de U25 mide 52 mm². <b>v6 sin avance</b> en sus 8 bloqueantes (solo 15 vías GND): plan recorrido una semana, piloto de 20 el lun 2-nov; pre-órdenes de JLC ya. <b>Script T30</b> probado en copias en la Mac (DRC sin infracciones nuevas, 0 sin conectar). Merge del lunes (<code>40decf8</code>) arreglado por Leo (<code>240e2c3</code> + <code>merge=binary</code>) y verificado objeto por objeto. Decisiones para Leo: D-19-1 puertos (si van, entran al piloto), D-19-2 Dbk2, D-19-3 sacar KiCad del auto-sync de las 18:00, D-19-4 clase HV24. <a href="../hardware/re-auditoria-19oct/">Re-auditoría</a> · <a href="../hardware/v56-v57-layout-pasada-4/">Pasada 4</a> · <a href="../hardware/v6-plan-fines-de-semana/">Plan v6 rev. 1.1</a></li>
|
|
||||||
<li><b>16-oct (17:30)</b> — <b>Auditoría de producción de la v6: NO LISTA.</b> Ruteo completo (<code>2612891</code>, DRC 0 errores), pero: U25 con el código del AP64350, sin barrenos, 5 V por In1/In2 a 0.5 mm, cargador con pistas de 0.5 mm y 1 vía en el EP, RF del módem por In1, 81 vías de tierra, stackup genérico. Scripts listos: T25 <code>--set v6</code> y <b>T28</b> (probados con DRC). <b>Plan de fines de semana</b>: layout el 17–18 y el 24–25 oct, pedido del piloto de 20 el 26-oct, firmware y banco hasta ≈ 15-nov. P7/P8 se quitan (decisión del 9-oct). <a href="../hardware/v6-auditoria-produccion-16oct/">Auditoría v6</a> · <a href="../hardware/v6-plan-fines-de-semana/">Plan</a></li>
|
|
||||||
<li><b>16-oct (16:00)</b> — <b>T27 aplicado</b> en V5.6 y V5.7 (<code>f65c830</code>). Figuras de los pasos 3, 4 y 5 en la pasada 3; corregido el dato de A4 (son dos vías de <code>/MAIN5V</code>, no una). <b>Tablero de pendientes al día</b>: pasada 2 y fuente de la V5.7 en Hecho; 9 pendientes nuevos (pasada 3 parte A, medición con TP1, decisiones de Leo, firmware 0.7.0, piloto y banco V5.6, prototipos V5.7, barrenos de la v6). <a href="../tableros/kanban.html">Kanban</a></li>
|
|
||||||
<li><b>16-oct (tarde)</b> — Decisión de Leo: <b>MH11 se quita</b> de V5.6 y V5.7. Pasos 1 y 2 de la pasada 3 por script (T27): MH11 fuera, <code>/CHRG_STAT</code> lejos de MH10 (V5.6) y <b>sensado Kelvin de RS1</b> (RS1 girado 180°; el LTC4040 deja de ver ≈ 27 mΩ y vuelve a limitar en 2.5 A). Probado en copias con DRC: 0 infracciones nuevas, 0 sin conectar. Prueba de banco nueva con TP1 (CLPROG). <a href="../hardware/v56-v57-layout-pasada-3/">Pasada 3</a> · <a href="../hardware/auditoria-produccion-16oct/#44-la-correccion-rs1-girado-180-y-sensado-kelvin-script-t27">Qué mide RS1</a></li>
|
|
||||||
<li><b>16-oct</b> — Auditoría de producción V5.6 / V5.7 / v6: <b>ninguna lista para pedir</b>. Corte de 10 s en batería: V5.6/V5.7 lo permiten en hardware, falta el firmware 0.7.0 y el banco. V5.7 sin SD-15B: entrada <b>9–32 V</b> (recomendado D10 → SMBJ33CA). v6: PCB sin actualizar y sin el firmware NTRIP que sustituye al 4G NTRIP Master. Puertos de V5.6/V5.7 con defectos ya resueltos en el esquemático v6 (portarlos: decide Leo). Acelerómetro en las tres. Barrenos de la V5.5 a V5.6/V5.7 con el script T25 (<b>MH4 en 185.2, no 193.2</b>). Hallazgo: límite de entrada del LTC4040 ≈ 1 A en V5.5/V5.6/V5.7 por el sensado de RS1. Recomendado: piloto de 5 V5.6 antes de las 113. <a href="../hardware/auditoria-produccion-16oct/">Auditoría</a> · <a href="../hardware/v56-v57-layout-pasada-3/">Pasada 3</a></li>
|
|
||||||
<li><b>13-oct</b> — Pedido de <b>570 HBOX</b>: 113 × V5.6 (con SD-15B), 327 × V5.7, 130 × v6; todos los reemplazos futuros en v6. Paquetes de fabricación V5.6 / V5.7 / v6 a <b>Aldair</b> (fabricación/ensamble, rol por confirmar). Caja plástica HBOX: el cable Ethernet no cabe — cambio por definir (pista Mecánica).</li>
|
|
||||||
<li><b>13-oct</b> — Auditoría del layout V5.6 de Michelle (d71098d): <b>NO LISTA</b> — /BAT_PLUS por In1 (bloqueante), MAIN5V/SYS5V/SW3 por capas internas; las 18 piezas movidas se quedan (opción A). Esquemático V5.7 aplicado con ERC 0 (F2 SMD 3 A, Q10/D10 reusados, fuera U2 y P2, JP1 puenteado). Reparto: <b>Leo</b> coloca la fuente en V5.7; <b>Michelle</b> corrige V5.6 y después toma V5.7. <a href="../hardware/v56-auditoria-layout-13oct/">Auditoría</a> · <a href="../hardware/v56-layout-pasada-2/">Pasada 2</a></li>
|
|
||||||
<li><b>12-oct</b> — <b>V5.7 = V5.6 + fuente conmutada 24 V → 5 V a bordo</b> (U25 TPS54360B, bloque de la v6), sin SD-15B externa. Proyecto KiCad separado <code>hbox-watchdog-v57</code>; V5.6 y V5.7 conviven. <a href="../hardware/v57-eco-fuente/">ECO V5.7</a></li>
|
|
||||||
<li><b>09-oct</b> — <b>P12 (sonda NTC) eliminada</b> de V5.6 con R42/C24: redundante y se necesita el espacio. Sin lectura de temperatura de celda en batería (aceptado). Aplica a P7/P8 de la v6.</li>
|
|
||||||
<li><b>08-oct</b> — Acelerómetro <b>U26 SC7A20TR</b> (I²C 0x18, INT1 → PB9) añadido a V5.6. <b>Regla DFM LTC4040 (U6)</b>: pasta del EP ≤ 50 %, vías tapadas (epoxy filled & capped) o fuera de las ventanas de pasta, X-ray/AOI en U6, lupa en cada tarjeta antes de energizar. Vale para toda revisión, incluida la v6.</li>
|
|
||||||
<li><b>05-oct</b> — Revisión del bus I²C V5.6 / v6 (direcciones, pull-ups, TMP102 U18 en 0x49).</li>
|
|
||||||
<li><b>30-sep → 1-oct</b> — V5.6: revisión de modo batería y layout (U4 junto a L6, DzG1 corregido); estado hacia producción.</li>
|
|
||||||
<li><b>28-sep</b> — v6 auditada con las lecciones de V5.5 y plan de corrección. <b>«No habrá v6.1»</b>: lo que falte se corrige antes de pedir. <a href="../hardware/v6-plan-correccion/">Plan de corrección</a> · <a href="../hardware/v6-proximos-pasos/">Próximos pasos</a></li>
|
|
||||||
<li><b>27-sep</b> — Requisito V5.6: ciclo de 10 min en batería y mejor manejo térmico. ECO V5.6: LFP de alta temperatura, JP2 2-3, R55 5.6 k → flotación 3.50 V, RS1 10 mΩ, TMP102 U18 0x49. Regla de campo V5.5/V4 con 0.6.0: respaldo continuo en batería, nunca ciclos de etapa en batería. <a href="../hardware/v56-eco/">ECO V5.6</a></li>
|
|
||||||
<li><b>26-sep</b> — Decisión V5.6: los 500 ArduSimple ya pedidos se quedan (arquitectura con zócalos); nada de rework en tarjetas existentes; buck del módem AP64350 desde SYS5V, MODEM_EN/RST/LED al MCU, lista EMC. Hallazgo: el módem no arrancaba en batería porque el MP1584 (4.5 V mín.) caía en UVLO sobre el riel de 4.6–4.79 V del boost.</li>
|
|
||||||
<li><b>19/21-sep</b> — Un solo firmware por tarjeta; la versión del firmware no indica la tarjeta: V3 → <code>blackpillrtkcode_v4</code>, V4 → <code>hbox-tarjetaV4_x.y.z.bin</code>, V5 → <code>hbox-v5-bringup_x.y.z.bin</code>. V5.6 necesita 0.7.0 (<code>board_v56.h</code>).</li>
|
|
||||||
<li><b>22-ago</b> — V5.5 ordenada, 15 pzas (JLC). Auditoría EMC: /SW_BAT 0.3 mm y /BAT_PLUS 0.3–0.5 mm = críticos para la siguiente corrida (origen de la regla «nada de potencia por capas internas»).</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="bloque">
|
|
||||||
<h2>Filosofía de diseño</h2>
|
|
||||||
<p class="sub">Cinco reglas que explican casi todas las decisiones anteriores. Detalle y documentos en <a href="../hardware/">Tarjetas y hardware</a>.</p>
|
|
||||||
<ul class="filosofia">
|
|
||||||
<li><b>Primero la seguridad en campo.</b> La unidad vive en un puerto a más de 45 °C y en batería: en V5.5/V4, respaldo continuo, nunca ciclos de etapa en batería; desde V5.6 el corte de 10 s en batería es requisito, y se prueba en banco antes de usarlo en campo; se mide antes de energizar; una tarjeta con marca de daño no entra a un ensamble.</li>
|
|
||||||
<li><b>Una etapa probada vale más que una nueva.</b> Los zócalos para ArduSimple y módem se quedan (500 ya pedidos); nada de rework en tarjetas existentes; la fuente a bordo de V5.7 reutiliza el bloque ya auditado de la v6.</li>
|
|
||||||
<li><b>Redundancia solo cuando un modo de falla la justifica.</b> Por eso salió P12 (NTC redundante, estorbaba) y entró el acelerómetro U26 (detecta golpes y movimiento, que sí nos han fallado).</li>
|
|
||||||
<li><b>Las reglas de fabricación son órdenes permanentes.</b> LTC4040: pasta ≤ 50 %, vías tapadas, X-ray; ninguna red de potencia por capas internas (lección de V5.5); lupa en U6 antes de energizar. Se aplican a toda revisión, sin excepciones por prisa.</li>
|
|
||||||
<li><b>Cambios con prueba y rastro.</b> Esquemático por etiquetas y ediciones con script verificadas contra la netlist (ERC 0, paridad 100 %); git en dos pasos (submódulo KiCad y luego Drogheda); una persona por PCB a la vez y scripts solo con KiCad cerrado (git no mezcla archivos de KiCad: <code>merge=binary</code>); una tarjeta por proyecto KiCad (V5.7 separado); «no habrá v6.1»: se corrige antes de pedir, no después.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="kanban.js"></script>
|
|
||||||
<script>
|
|
||||||
const VERSIONES = [
|
|
||||||
{ v: 'V5.5', cls: 'v55', badge: 'badge-v55', estado: 'En campo — 15 pzas (lote 22-ago), fw 0.6.x',
|
|
||||||
siguiente: 'Mantener la flota en 0.6.x con respaldo continuo en batería. Sin rework: lo aprendido va a V5.6. Inspeccionar U6 con lupa en cada tarjeta. Medir en una unidad el límite de entrada del cargador (≈ 1 A, auditoría 16-oct §4.3).',
|
|
||||||
quien: 'Jesús (pruebas) · Alan y Daniel (campo)',
|
|
||||||
docs: [['Manual V5', '../manuales/manual-tarjeta-V5/'], ['Auditoría 16-oct §4', '../hardware/auditoria-produccion-16oct/'], ['Lecciones V5.5 → v6', '../hardware/v6-revision-lecciones-v55/'], ['Acta de tarjeta V5', '../formularios/v5.html']] },
|
|
||||||
{ v: 'V5.6', cls: 'v56', badge: 'badge-v56', estado: 'Casi lista: T30 aplicado (7650b96); falta la pasada 4, parte A, con un crítico nuevo: C129 en VSYS de U6 (19-oct, tarde)',
|
|
||||||
siguiente: 'Pasada 4, parte A: primero C129 junto a VSYS de U6 (crítico); regresar 4 vías perdidas; quitar la rama de Dbk2; exclusiones del DRC → Claude re-audita y genera Gerber/BOM/CPL → pedido del piloto de 5 (≈ 21–22-oct, con los 5 de V5.7) → banco con firmware 0.7.0 (incluye medir VSYS, el riel del módem y el GNSS) → las 108 restantes (con SD-15B). Antes del pedido: decisión D-19-1 (puertos).',
|
|
||||||
quien: 'Michelle (layout) · Claude (auditoría) · Leo (T30, pedido, firmware) · Jesús (banco)',
|
|
||||||
docs: [['▶ Qué es crítico (19-oct, tarde)', '../hardware/v56-clasificacion-19oct/'], ['▶ Pasada 4 — V5.6 y V5.7', '../hardware/v56-v57-layout-pasada-4/'], ['Re-auditoría 19-oct', '../hardware/re-auditoria-19oct/'], ['Auditoría 16-oct (requisitos)', '../hardware/auditoria-produccion-16oct/'], ['ECO V5.6', '../hardware/v56-eco/'], ['Hoja de datos', '../hardware/v56-datasheet/'], ['Pinout', '../hardware/v56-pinout/']] },
|
|
||||||
{ v: 'V5.7', cls: 'v57', badge: 'badge-v57', estado: 'Re-auditada 19-oct — NO LISTA: pasada 3 hecha (f3f35f4), ya replica la V5.6; bloqueante /SYS5V por In2 bajo K1; lazo de entrada de U25 de 52 mm²',
|
|
||||||
siguiente: 'Lun 19: T30 (Leo). Mar 20 – mié 21: Michelle pasada 4, parte B (≈ medio día: /SYS5V fuera de In2, C149/C150 y L22 junto a U25, lo mismo que V5.6) → re-auditoría → 5 prototipos con el piloto de V5.6 (ECO §6: 9/12/24/32 V, polaridad, temperatura) → pedido 327. Entrada 9–32 V.',
|
|
||||||
quien: 'Michelle (layout) · Claude (auditoría) · Leo (T30, pedido)',
|
|
||||||
docs: [['▶ Pasada 4 — parte B', '../hardware/v56-v57-layout-pasada-4/#parte-b-v57-hbox-watchdog-v57-medio-dia'], ['Re-auditoría 19-oct §4', '../hardware/re-auditoria-19oct/#4-v57-punto-por-punto'], ['ECO V5.7 — fuente a bordo', '../hardware/v57-eco-fuente/']] },
|
|
||||||
{ v: 'v6', cls: 'v6', badge: 'badge-v6', estado: 'Re-auditada 19-oct: NO LISTA, sin cambio — solo 15 vías de tierra nuevas (00a9af4); siguen los 8 bloqueantes del 16-oct',
|
|
||||||
siguiente: 'Lun 19 – mar 20: pre-órdenes JLC (THVD1410DR, TPS7A2033PDBVR, J14). Sáb 24 8:00: Leo corre T25 --set v6 + T28. Sáb 24: Michelle bloques A y B (potencia) + MH4/MH6. Dom 25: bloques C, D y E (RF del módem, U20, GNSS, U12). Sáb 31 – dom 1-nov: pares, puertos, In1, T29 (vías GND), serigrafía, DRC final. Lun 2-nov: piloto de 20. Firmware v6 los fines de semana 7–8 y 14–15 nov; banco 21–22 nov (G2). No habrá v6.1.',
|
|
||||||
quien: 'Michelle (layout) · Leo · Claude · Adriana (pre-órdenes)',
|
|
||||||
docs: [['▶ Plan de fines de semana (rev. 1.1)', '../hardware/v6-plan-fines-de-semana/'], ['Auditoría v6 (16-oct)', '../hardware/v6-auditoria-produccion-16oct/'], ['Re-auditoría 19-oct §8', '../hardware/re-auditoria-19oct/#8-v6'], ['Plan de corrección (28-sep)', '../hardware/v6-plan-correccion/'], ['Manual v6', '../manuales/manual-tarjeta-v6/'], ['Plan de cobre', '../hardware/EinarBoard-v6_Plan-de-Cobre_ES.html']] },
|
|
||||||
];
|
|
||||||
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 cont = document.getElementById('versiones');
|
|
||||||
const contadores = {};
|
|
||||||
VERSIONES.forEach(x => {
|
|
||||||
const c = el('div', { class: 'ver ' + x.cls });
|
|
||||||
c.append(el('h3', null, el('span', { text: 'EinarBoard ' + x.v }), el('span', { class: x.badge, text: x.v })));
|
|
||||||
c.append(el('div', { class: 'est', text: x.estado }));
|
|
||||||
c.append(el('div', { class: 'sig', html: '<b>Siguiente:</b> ' + x.siguiente }));
|
|
||||||
c.append(el('div', { class: 'quien', text: '👤 ' + x.quien }));
|
|
||||||
const d = el('div', { class: 'docs' }); x.docs.forEach(([t, h]) => d.append(el('a', { href: h, text: t }))); c.append(d);
|
|
||||||
const cnt = el('div', { class: 'cnt', text: '…' }); contadores[x.v] = cnt; c.append(cnt);
|
|
||||||
cont.append(c);
|
|
||||||
});
|
|
||||||
Kanban.montar(document.getElementById('kb'), {
|
|
||||||
pista: 'ingenieria', filtros: true,
|
|
||||||
onCargado: tareas => {
|
|
||||||
VERSIONES.forEach(x => {
|
|
||||||
const mios = tareas.filter(t => t.version_tarjeta === x.v && t.estado !== 'hecho');
|
|
||||||
const bloq = mios.filter(t => t.estado === 'bloqueado').length, venc = mios.filter(t => t.vencida).length;
|
|
||||||
const a = el('a', { href: 'kanban.html?pista=ingenieria', text: mios.length + ' pendiente(s) abierto(s)' + (bloq ? ' · ' + bloq + ' bloqueado(s)' : '') + (venc ? ' · ' + venc + ' vencido(s)' : '') });
|
|
||||||
contadores[x.v].innerHTML = ''; contadores[x.v].append(a);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
115
docs/tableros/ingenieria.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
title: "Ingeniería"
|
||||||
|
description: "Dónde está el hardware: una tarjeta por versión de EinarBoard, sus pendientes y las decisiones."
|
||||||
|
template: tablero.html
|
||||||
|
hide:
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="tb tb-ingenieria">
|
||||||
|
<header class="tb-cab">
|
||||||
|
<div class="tb-cab__t"><h1>Ingeniería</h1><span class="meta" id="meta">V5.5 · V5.6 · V5.7 · v6 — estado al 19-oct-2026</span></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<details class="info tb-como">
|
||||||
|
<summary>Cómo se usa</summary>
|
||||||
|
<ol>
|
||||||
|
<li>Arriba, una tarjeta por versión de EinarBoard: estado, siguiente paso, responsable y sus documentos. Es texto fijo: cuando cambie una decisión, pide a Leo o a Claude que lo actualice aquí.</li>
|
||||||
|
<li>En medio, los <b>pendientes de la pista Ingeniería</b>: son los mismos del <a href="/tableros/pendientes/">tablero de pendientes</a>, filtrados. Puedes moverlos desde aquí.</li>
|
||||||
|
<li>Abajo, las últimas decisiones con fecha y la filosofía de diseño. Antes de proponer un cambio, revisa que no contradiga una decisión ya tomada.</li>
|
||||||
|
<li>Para una tarjeta nueva o un cambio de pedido, primero el pendiente, luego el trabajo: así queda el rastro de quién y cuándo.</li>
|
||||||
|
</ol>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="versiones" id="versiones"></div>
|
||||||
|
|
||||||
|
<section class="bloque">
|
||||||
|
<h2>Pendientes de ingeniería</h2>
|
||||||
|
<p class="sub">Pista Ingeniería del tablero. Arrastra o usa «Mover a…»; el historial anota quién lo hizo. <a href="/tableros/pendientes/">Ver todas las pistas →</a></p>
|
||||||
|
<div id="kb"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bloque">
|
||||||
|
<h2>Últimas decisiones</h2>
|
||||||
|
<p class="sub">Lo decidido manda hasta que otra decisión con fecha posterior lo cambie. Fuente: notas del proyecto; detalle en la sección de hardware.</p>
|
||||||
|
<ul class="decisiones">
|
||||||
|
<li><b>19-oct (19:30)</b> — <b>V5.6: qué es crítico y qué se puede quedar</b> (archivo de trabajo de las 18:59, sobre T30). <b>Un solo crítico, nuevo:</b> U6 (LTC4040) no tiene capacitor en sus pines VSYS; el más cercano, C34, está a 11.5 mm (viene desde la V5.5). La hoja de datos lo pone por encima de todo en el layout. Se agrega <b>C129</b> (10 µF 0603, C19702) junto a los pines 1/24, sin mover piezas. Regresar 2 vías de <code>/MAIN5V</code> y 2 de <code>/3V3_MODEM</code> que se perdieron al guardar. L7 lejos de U7 y lo heredado de la V5.5 (L6, L8, Dbk1) se quedan para el piloto, y se miden en el banco. <a href="/hardware/v56-clasificacion-19oct/">Clasificación</a></li>
|
||||||
|
<li><b>19-oct (17:10)</b> — <b>Re-auditoría V5.6 / V5.7 / v6</b> sobre la pasada 3 de Michelle (HBox_PCB <code>4f4681f</code>). <b>V5.6 casi lista</b>: <code>/BAT_PLUS</code> fuera de capas internas, <code>/3V3_MODEM</code> a 1.0 mm, 4 vías en el EP de U7, sin cobre suelto; faltan T30 + ≈ 1 h (L7/Dbk2, salida VSYS de U6, 3 vías, exclusiones del DRC). <b>V5.7 NO LISTA</b>: ya replica la V5.6, pero <code>/SYS5V</code> va 10.9 mm por In2 a 0.5 mm bajo K1 (bloqueante) y el lazo de entrada de U25 mide 52 mm². <b>v6 sin avance</b> en sus 8 bloqueantes (solo 15 vías GND): plan recorrido una semana, piloto de 20 el lun 2-nov; pre-órdenes de JLC ya. <b>Script T30</b> probado en copias en la Mac (DRC sin infracciones nuevas, 0 sin conectar). Merge del lunes (<code>40decf8</code>) arreglado por Leo (<code>240e2c3</code> + <code>merge=binary</code>) y verificado objeto por objeto. Decisiones para Leo: D-19-1 puertos (si van, entran al piloto), D-19-2 Dbk2, D-19-3 sacar KiCad del auto-sync de las 18:00, D-19-4 clase HV24. <a href="/hardware/re-auditoria-19oct/">Re-auditoría</a> · <a href="/hardware/v56-v57-layout-pasada-4/">Pasada 4</a> · <a href="/hardware/v6-plan-fines-de-semana/">Plan v6 rev. 1.1</a></li>
|
||||||
|
<li><b>16-oct (17:30)</b> — <b>Auditoría de producción de la v6: NO LISTA.</b> Ruteo completo (<code>2612891</code>, DRC 0 errores), pero: U25 con el código del AP64350, sin barrenos, 5 V por In1/In2 a 0.5 mm, cargador con pistas de 0.5 mm y 1 vía en el EP, RF del módem por In1, 81 vías de tierra, stackup genérico. Scripts listos: T25 <code>--set v6</code> y <b>T28</b> (probados con DRC). <b>Plan de fines de semana</b>: layout el 17–18 y el 24–25 oct, pedido del piloto de 20 el 26-oct, firmware y banco hasta ≈ 15-nov. P7/P8 se quitan (decisión del 9-oct). <a href="/hardware/v6-auditoria-produccion-16oct/">Auditoría v6</a> · <a href="/hardware/v6-plan-fines-de-semana/">Plan</a></li>
|
||||||
|
<li><b>16-oct (16:00)</b> — <b>T27 aplicado</b> en V5.6 y V5.7 (<code>f65c830</code>). Figuras de los pasos 3, 4 y 5 en la pasada 3; corregido el dato de A4 (son dos vías de <code>/MAIN5V</code>, no una). <b>Tablero de pendientes al día</b>: pasada 2 y fuente de la V5.7 en Hecho; 9 pendientes nuevos (pasada 3 parte A, medición con TP1, decisiones de Leo, firmware 0.7.0, piloto y banco V5.6, prototipos V5.7, barrenos de la v6). <a href="/tableros/pendientes/">Kanban</a></li>
|
||||||
|
<li><b>16-oct (tarde)</b> — Decisión de Leo: <b>MH11 se quita</b> de V5.6 y V5.7. Pasos 1 y 2 de la pasada 3 por script (T27): MH11 fuera, <code>/CHRG_STAT</code> lejos de MH10 (V5.6) y <b>sensado Kelvin de RS1</b> (RS1 girado 180°; el LTC4040 deja de ver ≈ 27 mΩ y vuelve a limitar en 2.5 A). Probado en copias con DRC: 0 infracciones nuevas, 0 sin conectar. Prueba de banco nueva con TP1 (CLPROG). <a href="/hardware/v56-v57-layout-pasada-3/">Pasada 3</a> · <a href="/hardware/auditoria-produccion-16oct/#44-la-correccion-rs1-girado-180-y-sensado-kelvin-script-t27">Qué mide RS1</a></li>
|
||||||
|
<li><b>16-oct</b> — Auditoría de producción V5.6 / V5.7 / v6: <b>ninguna lista para pedir</b>. Corte de 10 s en batería: V5.6/V5.7 lo permiten en hardware, falta el firmware 0.7.0 y el banco. V5.7 sin SD-15B: entrada <b>9–32 V</b> (recomendado D10 → SMBJ33CA). v6: PCB sin actualizar y sin el firmware NTRIP que sustituye al 4G NTRIP Master. Puertos de V5.6/V5.7 con defectos ya resueltos en el esquemático v6 (portarlos: decide Leo). Acelerómetro en las tres. Barrenos de la V5.5 a V5.6/V5.7 con el script T25 (<b>MH4 en 185.2, no 193.2</b>). Hallazgo: límite de entrada del LTC4040 ≈ 1 A en V5.5/V5.6/V5.7 por el sensado de RS1. Recomendado: piloto de 5 V5.6 antes de las 113. <a href="/hardware/auditoria-produccion-16oct/">Auditoría</a> · <a href="/hardware/v56-v57-layout-pasada-3/">Pasada 3</a></li>
|
||||||
|
<li><b>13-oct</b> — Pedido de <b>570 HBOX</b>: 113 × V5.6 (con SD-15B), 327 × V5.7, 130 × v6; todos los reemplazos futuros en v6. Paquetes de fabricación V5.6 / V5.7 / v6 a <b>Aldair</b> (fabricación/ensamble, rol por confirmar). Caja plástica HBOX: el cable Ethernet no cabe — cambio por definir (pista Mecánica).</li>
|
||||||
|
<li><b>13-oct</b> — Auditoría del layout V5.6 de Michelle (d71098d): <b>NO LISTA</b> — /BAT_PLUS por In1 (bloqueante), MAIN5V/SYS5V/SW3 por capas internas; las 18 piezas movidas se quedan (opción A). Esquemático V5.7 aplicado con ERC 0 (F2 SMD 3 A, Q10/D10 reusados, fuera U2 y P2, JP1 puenteado). Reparto: <b>Leo</b> coloca la fuente en V5.7; <b>Michelle</b> corrige V5.6 y después toma V5.7. <a href="/hardware/v56-auditoria-layout-13oct/">Auditoría</a> · <a href="/hardware/v56-layout-pasada-2/">Pasada 2</a></li>
|
||||||
|
<li><b>12-oct</b> — <b>V5.7 = V5.6 + fuente conmutada 24 V → 5 V a bordo</b> (U25 TPS54360B, bloque de la v6), sin SD-15B externa. Proyecto KiCad separado <code>hbox-watchdog-v57</code>; V5.6 y V5.7 conviven. <a href="/hardware/v57-eco-fuente/">ECO V5.7</a></li>
|
||||||
|
<li><b>09-oct</b> — <b>P12 (sonda NTC) eliminada</b> de V5.6 con R42/C24: redundante y se necesita el espacio. Sin lectura de temperatura de celda en batería (aceptado). Aplica a P7/P8 de la v6.</li>
|
||||||
|
<li><b>08-oct</b> — Acelerómetro <b>U26 SC7A20TR</b> (I²C 0x18, INT1 → PB9) añadido a V5.6. <b>Regla DFM LTC4040 (U6)</b>: pasta del EP ≤ 50 %, vías tapadas (epoxy filled & capped) o fuera de las ventanas de pasta, X-ray/AOI en U6, lupa en cada tarjeta antes de energizar. Vale para toda revisión, incluida la v6.</li>
|
||||||
|
<li><b>05-oct</b> — Revisión del bus I²C V5.6 / v6 (direcciones, pull-ups, TMP102 U18 en 0x49).</li>
|
||||||
|
<li><b>30-sep → 1-oct</b> — V5.6: revisión de modo batería y layout (U4 junto a L6, DzG1 corregido); estado hacia producción.</li>
|
||||||
|
<li><b>28-sep</b> — v6 auditada con las lecciones de V5.5 y plan de corrección. <b>«No habrá v6.1»</b>: lo que falte se corrige antes de pedir. <a href="/hardware/v6-plan-correccion/">Plan de corrección</a> · <a href="/hardware/v6-proximos-pasos/">Próximos pasos</a></li>
|
||||||
|
<li><b>27-sep</b> — Requisito V5.6: ciclo de 10 min en batería y mejor manejo térmico. ECO V5.6: LFP de alta temperatura, JP2 2-3, R55 5.6 k → flotación 3.50 V, RS1 10 mΩ, TMP102 U18 0x49. Regla de campo V5.5/V4 con 0.6.0: respaldo continuo en batería, nunca ciclos de etapa en batería. <a href="/hardware/v56-eco/">ECO V5.6</a></li>
|
||||||
|
<li><b>26-sep</b> — Decisión V5.6: los 500 ArduSimple ya pedidos se quedan (arquitectura con zócalos); nada de rework en tarjetas existentes; buck del módem AP64350 desde SYS5V, MODEM_EN/RST/LED al MCU, lista EMC. Hallazgo: el módem no arrancaba en batería porque el MP1584 (4.5 V mín.) caía en UVLO sobre el riel de 4.6–4.79 V del boost.</li>
|
||||||
|
<li><b>19/21-sep</b> — Un solo firmware por tarjeta; la versión del firmware no indica la tarjeta: V3 → <code>blackpillrtkcode_v4</code>, V4 → <code>hbox-tarjetaV4_x.y.z.bin</code>, V5 → <code>hbox-v5-bringup_x.y.z.bin</code>. V5.6 necesita 0.7.0 (<code>board_v56.h</code>).</li>
|
||||||
|
<li><b>22-ago</b> — V5.5 ordenada, 15 pzas (JLC). Auditoría EMC: /SW_BAT 0.3 mm y /BAT_PLUS 0.3–0.5 mm = críticos para la siguiente corrida (origen de la regla «nada de potencia por capas internas»).</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="bloque">
|
||||||
|
<h2>Filosofía de diseño</h2>
|
||||||
|
<p class="sub">Cinco reglas que explican casi todas las decisiones anteriores. Detalle y documentos en <a href="/hardware/">Tarjetas y hardware</a>.</p>
|
||||||
|
<ul class="filosofia">
|
||||||
|
<li><b>Primero la seguridad en campo.</b> La unidad vive en un puerto a más de 45 °C y en batería: en V5.5/V4, respaldo continuo, nunca ciclos de etapa en batería; desde V5.6 el corte de 10 s en batería es requisito, y se prueba en banco antes de usarlo en campo; se mide antes de energizar; una tarjeta con marca de daño no entra a un ensamble.</li>
|
||||||
|
<li><b>Una etapa probada vale más que una nueva.</b> Los zócalos para ArduSimple y módem se quedan (500 ya pedidos); nada de rework en tarjetas existentes; la fuente a bordo de V5.7 reutiliza el bloque ya auditado de la v6.</li>
|
||||||
|
<li><b>Redundancia solo cuando un modo de falla la justifica.</b> Por eso salió P12 (NTC redundante, estorbaba) y entró el acelerómetro U26 (detecta golpes y movimiento, que sí nos han fallado).</li>
|
||||||
|
<li><b>Las reglas de fabricación son órdenes permanentes.</b> LTC4040: pasta ≤ 50 %, vías tapadas, X-ray; ninguna red de potencia por capas internas (lección de V5.5); lupa en U6 antes de energizar. Se aplican a toda revisión, sin excepciones por prisa.</li>
|
||||||
|
<li><b>Cambios con prueba y rastro.</b> Esquemático por etiquetas y ediciones con script verificadas contra la netlist (ERC 0, paridad 100 %); git en dos pasos (submódulo KiCad y luego Drogheda); una persona por PCB a la vez y scripts solo con KiCad cerrado (git no mezcla archivos de KiCad: <code>merge=binary</code>); una tarjeta por proyecto KiCad (V5.7 separado); «no habrá v6.1»: se corrige antes de pedir, no después.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const VERSIONES = [
|
||||||
|
{ v: 'V5.5', cls: 'v55', badge: 'badge-v55', estado: 'En campo — 15 pzas (lote 22-ago), fw 0.6.x',
|
||||||
|
siguiente: 'Mantener la flota en 0.6.x con respaldo continuo en batería. Sin rework: lo aprendido va a V5.6. Inspeccionar U6 con lupa en cada tarjeta. Medir en una unidad el límite de entrada del cargador (≈ 1 A, auditoría 16-oct §4.3).',
|
||||||
|
quien: 'Jesús (pruebas) · Alan y Daniel (campo)',
|
||||||
|
docs: [['Manual V5', '/manuales/manual-tarjeta-V5/'], ['Auditoría 16-oct §4', '/hardware/auditoria-produccion-16oct/'], ['Lecciones V5.5 → v6', '/hardware/v6-revision-lecciones-v55/'], ['Acta de tarjeta V5', '/formularios/v5.html']] },
|
||||||
|
{ v: 'V5.6', cls: 'v56', badge: 'badge-v56', estado: 'Casi lista: T30 aplicado (7650b96); falta la pasada 4, parte A, con un crítico nuevo: C129 en VSYS de U6 (19-oct, tarde)',
|
||||||
|
siguiente: 'Pasada 4, parte A: primero C129 junto a VSYS de U6 (crítico); regresar 4 vías perdidas; quitar la rama de Dbk2; exclusiones del DRC → Claude re-audita y genera Gerber/BOM/CPL → pedido del piloto de 5 (≈ 21–22-oct, con los 5 de V5.7) → banco con firmware 0.7.0 (incluye medir VSYS, el riel del módem y el GNSS) → las 108 restantes (con SD-15B). Antes del pedido: decisión D-19-1 (puertos).',
|
||||||
|
quien: 'Michelle (layout) · Claude (auditoría) · Leo (T30, pedido, firmware) · Jesús (banco)',
|
||||||
|
docs: [['Qué es crítico (19-oct, tarde)', '/hardware/v56-clasificacion-19oct/'], ['Pasada 4 — V5.6 y V5.7', '/hardware/v56-v57-layout-pasada-4/'], ['Re-auditoría 19-oct', '/hardware/re-auditoria-19oct/'], ['Auditoría 16-oct (requisitos)', '/hardware/auditoria-produccion-16oct/'], ['ECO V5.6', '/hardware/v56-eco/'], ['Hoja de datos', '/hardware/v56-datasheet/'], ['Pinout', '/hardware/v56-pinout/']] },
|
||||||
|
{ v: 'V5.7', cls: 'v57', badge: 'badge-v57', estado: 'Re-auditada 19-oct — NO LISTA: pasada 3 hecha (f3f35f4), ya replica la V5.6; bloqueante /SYS5V por In2 bajo K1; lazo de entrada de U25 de 52 mm²',
|
||||||
|
siguiente: 'Lun 19: T30 (Leo). Mar 20 – mié 21: Michelle pasada 4, parte B (≈ medio día: /SYS5V fuera de In2, C149/C150 y L22 junto a U25, lo mismo que V5.6) → re-auditoría → 5 prototipos con el piloto de V5.6 (ECO §6: 9/12/24/32 V, polaridad, temperatura) → pedido 327. Entrada 9–32 V.',
|
||||||
|
quien: 'Michelle (layout) · Claude (auditoría) · Leo (T30, pedido)',
|
||||||
|
docs: [['Pasada 4 — parte B', '/hardware/v56-v57-layout-pasada-4/#parte-b-v57-hbox-watchdog-v57-medio-dia'], ['Re-auditoría 19-oct §4', '/hardware/re-auditoria-19oct/#4-v57-punto-por-punto'], ['ECO V5.7 — fuente a bordo', '/hardware/v57-eco-fuente/']] },
|
||||||
|
{ v: 'v6', cls: 'v6', badge: 'badge-v6', estado: 'Re-auditada 19-oct: NO LISTA, sin cambio — solo 15 vías de tierra nuevas (00a9af4); siguen los 8 bloqueantes del 16-oct',
|
||||||
|
siguiente: 'Lun 19 – mar 20: pre-órdenes JLC (THVD1410DR, TPS7A2033PDBVR, J14). Sáb 24 8:00: Leo corre T25 --set v6 + T28. Sáb 24: Michelle bloques A y B (potencia) + MH4/MH6. Dom 25: bloques C, D y E (RF del módem, U20, GNSS, U12). Sáb 31 – dom 1-nov: pares, puertos, In1, T29 (vías GND), serigrafía, DRC final. Lun 2-nov: piloto de 20. Firmware v6 los fines de semana 7–8 y 14–15 nov; banco 21–22 nov (G2). No habrá v6.1.',
|
||||||
|
quien: 'Michelle (layout) · Leo · Claude · Adriana (pre-órdenes)',
|
||||||
|
docs: [['Plan de fines de semana (rev. 1.1)', '/hardware/v6-plan-fines-de-semana/'], ['Auditoría v6 (16-oct)', '/hardware/v6-auditoria-produccion-16oct/'], ['Re-auditoría 19-oct §8', '/hardware/re-auditoria-19oct/#8-v6'], ['Plan de corrección (28-sep)', '/hardware/v6-plan-correccion/'], ['Manual v6', '/manuales/manual-tarjeta-v6/'], ['Plan de cobre', '/hardware/EinarBoard-v6_Plan-de-Cobre_ES.html']] },
|
||||||
|
];
|
||||||
|
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 cont = document.getElementById('versiones');
|
||||||
|
const contadores = {};
|
||||||
|
VERSIONES.forEach(x => {
|
||||||
|
const c = el('div', { class: 'ver ' + x.cls });
|
||||||
|
c.append(el('h3', null, el('span', { text: 'EinarBoard ' + x.v }), el('span', { class: x.badge, text: x.v })));
|
||||||
|
c.append(el('div', { class: 'est', text: x.estado }));
|
||||||
|
c.append(el('div', { class: 'sig', html: '<b>Siguiente:</b> ' + x.siguiente }));
|
||||||
|
const qd = el('div', { class: 'quien', html: Kanban.icono('equipo') }); qd.append(el('span', { text: x.quien })); c.append(qd);
|
||||||
|
const d = el('div', { class: 'docs' }); x.docs.forEach(([t, h]) => d.append(el('a', { href: h, text: t }))); c.append(d);
|
||||||
|
const cnt = el('div', { class: 'cnt', text: '…' }); contadores[x.v] = cnt; c.append(cnt);
|
||||||
|
cont.append(c);
|
||||||
|
});
|
||||||
|
Kanban.montar(document.getElementById('kb'), {
|
||||||
|
pista: 'ingenieria', filtros: true,
|
||||||
|
onCargado: tareas => {
|
||||||
|
VERSIONES.forEach(x => {
|
||||||
|
const mios = tareas.filter(t => t.version_tarjeta === x.v && t.estado !== 'hecho');
|
||||||
|
const bloq = mios.filter(t => t.estado === 'bloqueado').length, venc = mios.filter(t => t.vencida).length;
|
||||||
|
const a = el('a', { href: '/tableros/pendientes/?pista=ingenieria', text: mios.length + ' pendiente(s) abierto(s)' + (bloq ? ' · ' + bloq + ' bloqueado(s)' : '') + (venc ? ' · ' + venc + ' vencido(s)' : '') });
|
||||||
|
contadores[x.v].innerHTML = ''; contadores[x.v].append(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
219
docs/tableros/inventario.md
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
---
|
||||||
|
title: "Inventario"
|
||||||
|
description: "Existencias y movimientos de materiales: entradas, salidas, ajustes y alertas de mínimo."
|
||||||
|
template: tablero.html
|
||||||
|
hide:
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="tb tb-inventario">
|
||||||
|
<header class="tb-cab">
|
||||||
|
<div class="tb-cab__t"><h1>Inventario</h1><span class="meta" id="meta">Cargando…</span></div>
|
||||||
|
<div class="tb-cab__acc"><a class="boton sec" href="/inventario/"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4.5h11a3 3 0 0 1 3 3v12.5H8a3 3 0 0 1-3-3zM5 17a3 3 0 0 1 3-3h11"/></svg>Política de inventario</a></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<details class="info tb-como">
|
||||||
|
<summary>Cómo se usa</summary>
|
||||||
|
<ol>
|
||||||
|
<li>El <b>stock no se escribe a mano</b>: es la suma de los movimientos. Llegó material → <b>Entrada</b>. Se usó en un ensamble o se envió → <b>Salida</b> con la unidad o el acta como referencia.</li>
|
||||||
|
<li>Contaste y no cuadra → <b>Ajuste</b> con el conteo físico; el sistema calcula la diferencia y la deja registrada con tu nombre.</li>
|
||||||
|
<li>Toca el <b>código</b> para ver los movimientos de ese ítem. Cada uno dice quién, cuándo, cuánto y por qué.</li>
|
||||||
|
<li><b>Alerta</b> (rojo) = stock por debajo del mínimo. El mínimo lo pone quien compra; el tablero de producción lo repite.</li>
|
||||||
|
<li><b>Nuevo ítem</b> solo para cosas que se cuentan: tarjetas, módulos, gabinetes, cajas, fuentes, packs, antenas, cables y los componentes de entrega larga. Nada de resistencias.</li>
|
||||||
|
<li><b>Exportar CSV</b> baja la tabla para Excel. Nada se borra: si un ítem ya no aplica, ponle «(obsoleto)» en la descripción.</li>
|
||||||
|
</ol>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="acciones">
|
||||||
|
<select id="fCat"><option value="">Todas las categorías</option></select>
|
||||||
|
<select id="fVer"><option value="">Aplica a: todas</option></select>
|
||||||
|
<label><input type="checkbox" id="fAlerta" style="width:auto"> solo alertas</label>
|
||||||
|
<input type="search" id="fBuscar" placeholder="Buscar código o descripción…" style="flex:1;min-width:150px">
|
||||||
|
<button id="bMov"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Movimiento</button>
|
||||||
|
<button id="bNuevo" class="sec"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Nuevo ítem</button>
|
||||||
|
<a href="/api/inventario/export.csv" class="boton sec"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7.5 10.5 12 15l4.5-4.5M5 20h14"/></svg>Exportar CSV</a>
|
||||||
|
<span class="cnt" id="cnt"></span>
|
||||||
|
</div>
|
||||||
|
<div class="msg" id="aviso"></div>
|
||||||
|
<div id="cont" class="tblwrap"></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 CAT = { tarjeta: 'Tarjeta', modulo: 'Módulo', gabinete: 'Gabinete', caja: 'Caja', fuente: 'Fuente', bateria: 'Batería', antena: 'Antena', cable: 'Cable', conector: 'Conector', 'componente-critico': 'Componente crítico', otro: 'Otro' };
|
||||||
|
const VERS = ['V5.5', 'V5.6', 'V5.7', 'v6'];
|
||||||
|
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
|
||||||
|
let ITEMS = [], ADMIN = false, USER = '';
|
||||||
|
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, ayuda) { const d = el('div'); d.append(el('label', { text: l }), i); if (ayuda) d.append(el('div', { class: 'ayuda', text: ayuda })); return d; }
|
||||||
|
|
||||||
|
Object.entries(CAT).forEach(([k, v]) => $('fCat').append(el('option', { value: k, text: v })));
|
||||||
|
VERS.forEach(v => $('fVer').append(el('option', { value: v, text: 'Aplica a: ' + v })));
|
||||||
|
|
||||||
|
function visibles() {
|
||||||
|
const cat = $('fCat').value, ver = $('fVer').value, solo = $('fAlerta').checked, q = $('fBuscar').value.trim().toLowerCase();
|
||||||
|
return ITEMS.filter(i => (!cat || i.categoria === cat) && (!ver || (i.aplica_a || []).includes(ver)) && (!solo || i.alerta)
|
||||||
|
&& (!q || (i.codigo + ' ' + i.descripcion + ' ' + (i.proveedor || '') + ' ' + (i.ubicacion || '')).toLowerCase().includes(q)));
|
||||||
|
}
|
||||||
|
function pintar() {
|
||||||
|
const cont = $('cont'); cont.innerHTML = '';
|
||||||
|
const vis = visibles();
|
||||||
|
$('cnt').textContent = vis.length + ' ítem(s)' + (vis.length !== ITEMS.length ? ' de ' + ITEMS.length : '') + ' · ' + vis.filter(i => i.alerta).length + ' en alerta';
|
||||||
|
if (!vis.length) { cont.append(el('div', { class: 'vacio', text: ITEMS.length ? 'Nada coincide con el filtro.' : 'Todavía no hay ítems. Crea el primero con «Nuevo ítem» o corre la siembra.' })); return; }
|
||||||
|
const t = el('table', { class: 't' });
|
||||||
|
t.innerHTML = '<tr><th>Código</th><th>Descripción</th><th>Aplica a</th><th class="num">Stock</th><th class="num">Mínimo</th><th>Ubicación</th><th>Proveedor</th><th></th></tr>';
|
||||||
|
vis.forEach(i => {
|
||||||
|
const tr = el('tr', { class: i.alerta ? 'alerta' : '' });
|
||||||
|
const cod = el('span', { class: 'cod', text: i.codigo, title: 'Ver movimientos' }); cod.addEventListener('click', () => verMovimientos(i));
|
||||||
|
tr.append(el('td', null, cod, el('span', { class: 'cat', text: CAT[i.categoria] || i.categoria })));
|
||||||
|
tr.append(el('td', null, document.createTextNode(i.descripcion), i.notas ? el('div', { class: 'mini', text: i.notas.length > 90 ? i.notas.slice(0, 89) + '…' : i.notas }) : null));
|
||||||
|
const ap = el('td'); (i.aplica_a || []).forEach(v => ap.append(el('span', { class: BADGE[v] || 'pill pend', text: v, style: 'margin-right:3px' }))); tr.append(ap);
|
||||||
|
tr.append(el('td', { class: 'num stock', html: esc(i.stock) + ' <span class="mini">' + esc(i.unidad) + '</span>' + (i.alerta ? ' <span class="pill falla">alerta</span>' : '') }));
|
||||||
|
tr.append(el('td', { class: 'num', text: i.minimo || '—' }));
|
||||||
|
tr.append(el('td', { text: i.ubicacion || '—' }));
|
||||||
|
tr.append(el('td', { html: esc(i.proveedor || '—') + (i.codigo_proveedor ? '<div class="mini">' + esc(i.codigo_proveedor) + '</div>' : '') }));
|
||||||
|
const acc = el('td', { style: 'white-space:nowrap' });
|
||||||
|
const bm = el('button', { text: 'Movimiento' }); bm.addEventListener('click', () => movimiento(i));
|
||||||
|
const be = el('button', { class: 'sec', text: 'Editar' }); be.addEventListener('click', () => editar(i));
|
||||||
|
acc.append(bm, be); tr.append(acc);
|
||||||
|
t.append(tr);
|
||||||
|
});
|
||||||
|
cont.append(t);
|
||||||
|
}
|
||||||
|
async function cargar() {
|
||||||
|
$('meta').textContent = 'Actualizando…';
|
||||||
|
try {
|
||||||
|
const j = await api('/api/inventario');
|
||||||
|
ITEMS = j.items || []; ADMIN = !!j.admin; USER = j.user || '';
|
||||||
|
pintar();
|
||||||
|
$('meta').textContent = ITEMS.length + ' ítems · ' + (j.alertas || 0) + ' en alerta · actualizado ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }) + (USER ? ' · ' + USER : '');
|
||||||
|
} catch (e) { $('cont').innerHTML = ''; $('cont').append(el('div', { class: 'err', text: 'No se pudo cargar el inventario: ' + e.message })); $('meta').textContent = 'Error'; }
|
||||||
|
}
|
||||||
|
['fCat', 'fVer', 'fAlerta'].forEach(id => $(id).addEventListener('change', pintar));
|
||||||
|
$('fBuscar').addEventListener('input', pintar);
|
||||||
|
|
||||||
|
// ---------- diálogos ----------
|
||||||
|
function abrirDialogo(titulo, cuerpoFn, botones) {
|
||||||
|
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 m = el('div', { class: 'msg' });
|
||||||
|
cuerpoFn(cuerpo, m); cuerpo.append(m);
|
||||||
|
const pie = el('div', { class: 'dpie' }); dlg.append(pie);
|
||||||
|
const cancelar = el('button', { type: 'button', class: 'sec', text: botones ? 'Cancelar' : 'Cerrar' }); cancelar.addEventListener('click', () => dlg.close()); pie.append(cancelar);
|
||||||
|
(botones || []).forEach(([txt, fn]) => { const b = el('button', { type: 'button', text: txt }); b.addEventListener('click', async () => { b.disabled = true; try { await fn(m); } catch (e) { m.className = 'msg err'; m.textContent = e.message; } b.disabled = false; }); pie.append(b); });
|
||||||
|
if (dlg.showModal) dlg.showModal(); else dlg.setAttribute('open', '');
|
||||||
|
}
|
||||||
|
function selItems(sel) {
|
||||||
|
const s = el('select'); s.append(el('option', { value: '', text: '— elegir ítem —' }));
|
||||||
|
ITEMS.forEach(i => s.append(el('option', { value: i.id, text: i.codigo + ' — ' + i.descripcion + ' (' + i.stock + ' ' + i.unidad + ')' })));
|
||||||
|
if (sel) s.value = sel.id; return s;
|
||||||
|
}
|
||||||
|
function movimiento(item) {
|
||||||
|
abrirDialogo('Registrar movimiento', (c) => {
|
||||||
|
const fItem = selItems(item);
|
||||||
|
const fTipo = el('select'); [['entrada', 'Entrada — llegó material (+)'], ['salida', 'Salida — se usó o se envió (−)'], ['ajuste', 'Ajuste — conteo físico (fija el stock)']].forEach(([v, t]) => fTipo.append(el('option', { value: v, text: t })));
|
||||||
|
const fCant = el('input', { type: 'number', min: '0', step: '1', inputmode: 'numeric', placeholder: '0' });
|
||||||
|
const fMot = el('input', { type: 'text', maxlength: '200', placeholder: 'llegó pedido Mouser · ensamble · envío a ICAVE · conteo físico' });
|
||||||
|
const fRef = el('input', { type: 'text', maxlength: '120', placeholder: 'HBOX5-E07-A010-R0006 · OC-0042 · acta de envío' });
|
||||||
|
const ayudaCant = el('div', { class: 'ayuda', text: 'Cantidad que entra.' });
|
||||||
|
fTipo.addEventListener('change', () => { ayudaCant.textContent = fTipo.value === 'entrada' ? 'Cantidad que entra.' : fTipo.value === 'salida' ? 'Cantidad que sale. No puede dejar el stock en negativo.' : 'Lo que contaste físicamente: el stock quedará exactamente en ese número.'; });
|
||||||
|
c.append(campo('Ítem', fItem), campo('Tipo', fTipo));
|
||||||
|
const g = el('div', { class: 'dos-col' }); const cc = campo('Cantidad', fCant); cc.append(ayudaCant); g.append(cc, campo('Referencia (unidad, acta u orden)', fRef)); c.append(g);
|
||||||
|
c.append(campo('Motivo (obligatorio)', fMot));
|
||||||
|
c._leer = () => ({ item_id: fItem.value, tipo: fTipo.value, cantidad: fCant.value === '' ? null : +fCant.value, motivo: fMot.value.trim(), referencia: fRef.value.trim() });
|
||||||
|
setTimeout(() => (item ? fCant : fItem).focus(), 50);
|
||||||
|
}, [['Registrar', async (m) => {
|
||||||
|
const d = dlg.querySelector('.dcuerpo')._leer();
|
||||||
|
if (!d.item_id) throw new Error('Elige el ítem.');
|
||||||
|
if (d.cantidad == null || isNaN(d.cantidad)) throw new Error('Escribe la cantidad.');
|
||||||
|
if (!d.motivo) throw new Error('Escribe el motivo.');
|
||||||
|
const j = await api('/api/inventario/movimiento', json('POST', d));
|
||||||
|
dlg.close(); mensaje('Movimiento registrado: ' + j.item.codigo + ' ahora tiene ' + j.item.stock + ' ' + j.item.unidad + '.', true); cargar();
|
||||||
|
}]]);
|
||||||
|
}
|
||||||
|
function formItem(c, i) {
|
||||||
|
i = i || {};
|
||||||
|
const f = {};
|
||||||
|
f.codigo = el('input', { type: 'text', maxlength: '40', value: i.codigo || '', placeholder: 'EB-V56 · SD-15B-05', style: 'text-transform:uppercase' });
|
||||||
|
f.descripcion = el('input', { type: 'text', maxlength: '200', value: i.descripcion || '' });
|
||||||
|
f.categoria = el('select'); Object.entries(CAT).forEach(([k, v]) => f.categoria.append(el('option', { value: k, text: v }))); f.categoria.value = i.categoria || 'otro';
|
||||||
|
f.unidad = el('select'); [['pza', 'pza (piezas)'], ['m', 'm (metros)']].forEach(([v, t]) => f.unidad.append(el('option', { value: v, text: t }))); f.unidad.value = i.unidad || 'pza';
|
||||||
|
f.aplica = el('div', { class: 'lista-chk', style: 'max-height:none;display:flex;gap:14px;flex-wrap:wrap' });
|
||||||
|
VERS.forEach(v => { const l = el('label', { style: 'margin:0' }); const ck = el('input', { type: 'checkbox', value: v }); ck.checked = (i.aplica_a || []).includes(v); l.append(ck, document.createTextNode(v)); f.aplica.append(l); });
|
||||||
|
f.minimo = el('input', { type: 'number', min: '0', step: '1', value: i.minimo || 0 });
|
||||||
|
f.ubicacion = el('input', { type: 'text', maxlength: '120', value: i.ubicacion || '', placeholder: 'Anaquel A2 · taller Veracruz' });
|
||||||
|
f.proveedor = el('input', { type: 'text', maxlength: '120', value: i.proveedor || '' });
|
||||||
|
f.codigo_proveedor = el('input', { type: 'text', maxlength: '60', value: i.codigo_proveedor || '', placeholder: 'LCSC C2057152 · Mouser 709-SD15B05' });
|
||||||
|
f.notas = el('textarea', { rows: '2' }); f.notas.value = i.notas || '';
|
||||||
|
const g = el('div', { class: 'dos-col' });
|
||||||
|
g.append(campo('Código (único)', f.codigo), campo('Categoría', f.categoria));
|
||||||
|
c.append(g, campo('Descripción', f.descripcion), campo('Aplica a (versión de tarjeta)', f.aplica));
|
||||||
|
const g2 = el('div', { class: 'dos-col' }); g2.append(campo('Unidad', f.unidad), campo('Mínimo (0 = sin alerta)', f.minimo), campo('Ubicación', f.ubicacion), campo('Proveedor', f.proveedor), campo('Código del proveedor', f.codigo_proveedor)); c.append(g2);
|
||||||
|
if (!i.id) { f.stock_inicial = el('input', { type: 'number', min: '0', step: '1', value: '0' }); c.append(campo('Stock inicial (queda registrado como entrada)', f.stock_inicial)); }
|
||||||
|
c.append(campo('Notas', f.notas));
|
||||||
|
return () => {
|
||||||
|
const d = { codigo: f.codigo.value.trim().toUpperCase(), descripcion: f.descripcion.value.trim(), categoria: f.categoria.value, unidad: f.unidad.value,
|
||||||
|
aplica_a: [...f.aplica.querySelectorAll('input:checked')].map(x => x.value), minimo: +f.minimo.value || 0, ubicacion: f.ubicacion.value.trim(),
|
||||||
|
proveedor: f.proveedor.value.trim(), codigo_proveedor: f.codigo_proveedor.value.trim(), notas: f.notas.value.trim() };
|
||||||
|
if (f.stock_inicial) d.stock_inicial = +f.stock_inicial.value || 0;
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function nuevo() {
|
||||||
|
let leer;
|
||||||
|
abrirDialogo('Nuevo ítem', c => { leer = formItem(c); }, [['Crear ítem', async () => {
|
||||||
|
const d = leer(); if (!d.codigo) throw new Error('Escribe el código.'); if (!d.descripcion) throw new Error('Escribe la descripción.');
|
||||||
|
await api('/api/inventario', json('POST', d)); dlg.close(); mensaje('Ítem ' + d.codigo + ' creado.', true); cargar();
|
||||||
|
}]]);
|
||||||
|
}
|
||||||
|
function editar(i) {
|
||||||
|
let leer;
|
||||||
|
abrirDialogo('Editar ' + i.codigo, c => {
|
||||||
|
leer = formItem(c, i);
|
||||||
|
const h = el('div', { class: 'hist' });
|
||||||
|
h.append(el('div', { html: '<b>Creado</b> por ' + esc(i.creado_por) + ' el ' + esc(fmt(i.creado_en)) + ' · stock actual <b>' + esc(i.stock) + ' ' + esc(i.unidad) + '</b> (se cambia con movimientos, no aquí)' }));
|
||||||
|
(i.historial || []).slice().reverse().slice(0, 12).forEach(x => h.append(el('div', { html: esc(fmt(x.cuando)) + ' · <b>' + esc(x.quien) + '</b> · ' + esc(x.cambio) })));
|
||||||
|
c.append(h);
|
||||||
|
}, [['Guardar', async () => {
|
||||||
|
const d = leer(); const cambios = {};
|
||||||
|
Object.keys(d).forEach(k => { if (JSON.stringify(d[k]) !== JSON.stringify(i[k] == null ? (Array.isArray(d[k]) ? [] : (typeof d[k] === 'number' ? 0 : '')) : i[k])) cambios[k] = d[k]; });
|
||||||
|
if (Object.keys(cambios).length) await api('/api/inventario/item?id=' + encodeURIComponent(i.id), json('PATCH', cambios));
|
||||||
|
dlg.close(); mensaje('Ítem guardado.', true); cargar();
|
||||||
|
}]]);
|
||||||
|
}
|
||||||
|
async function verMovimientos(i) {
|
||||||
|
abrirDialogo('Movimientos de ' + i.codigo, async c => {
|
||||||
|
c.append(el('p', { class: 'sub', html: esc(i.descripcion) + ' · stock <b>' + esc(i.stock) + ' ' + esc(i.unidad) + '</b>' + (i.minimo ? ' · mínimo ' + esc(i.minimo) : '') }));
|
||||||
|
const lista = el('div', { text: 'Cargando…' }); c.append(lista);
|
||||||
|
const b = el('button', { html: Kanban.icono('mas') + 'Movimiento', style: 'margin-top:10px' }); b.addEventListener('click', () => movimiento(i)); c.append(b);
|
||||||
|
try {
|
||||||
|
const j = await api('/api/inventario/movimientos?item_id=' + encodeURIComponent(i.id));
|
||||||
|
lista.innerHTML = '';
|
||||||
|
if (!j.total) { lista.append(el('div', { class: 'vacio', text: 'Sin movimientos. El stock es 0 hasta que registres una entrada o un ajuste.' })); return; }
|
||||||
|
lista.append(el('div', { class: 'mov-fila', style: 'font-weight:700;color:var(--tinta2);font-size:11.5px;text-transform:uppercase' }, el('span', { text: 'Cuándo · quién' }), el('span', { text: 'Tipo' }), el('span', { text: 'Δ · stock' }), el('span', { text: 'Motivo · referencia' })));
|
||||||
|
j.movimientos.forEach(m => {
|
||||||
|
lista.append(el('div', { class: 'mov-fila' },
|
||||||
|
el('span', { html: esc(fmt(m.cuando)) + '<br><b>' + esc(m.quien) + '</b>' }),
|
||||||
|
el('span', { text: m.tipo + (m.tipo === 'ajuste' ? ' → ' + m.cantidad : '') }),
|
||||||
|
el('span', { class: 'd', html: '<span class="' + (m.delta >= 0 ? 'pos' : 'neg') + '">' + (m.delta >= 0 ? '+' : '') + esc(m.delta) + '</span> · ' + esc(m.stock_despues) }),
|
||||||
|
el('span', { html: esc(m.motivo) + (m.referencia ? '<br><span class="mini">ref. ' + esc(m.referencia) + '</span>' : '') })));
|
||||||
|
});
|
||||||
|
} catch (e) { lista.textContent = 'No se pudieron leer los movimientos: ' + e.message; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$('bMov').addEventListener('click', () => movimiento(null));
|
||||||
|
$('bNuevo').addEventListener('click', nuevo);
|
||||||
|
cargar();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@ -1,54 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
<!-- Desde el 20-oct-2026 este tablero vive dentro del sitio: docs/tableros/pendientes.md → /tableros/pendientes/
|
||||||
|
Este archivo solo redirige (conserva ?serie=, ?id=, ?pista=… y #…) para que las ligas viejas
|
||||||
|
guardadas en pendientes y actas sigan sirviendo. No se edita: los cambios van en tableros/pendientes.md. -->
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Tablero de pendientes (kanban) — HBOX</title>
|
<meta name="robots" content="noindex">
|
||||||
<link rel="stylesheet" href="tablero.css">
|
<title>Pendientes - Drogheda</title>
|
||||||
|
<script>location.replace('/tableros/pendientes/' + location.search + location.hash);</script>
|
||||||
|
<meta http-equiv="refresh" content="0; url=/tableros/pendientes/">
|
||||||
|
<style>body{margin:0;padding:24px;background:#0E1524;color:#E7ECF5;font:16px/1.5 system-ui,sans-serif}a{color:#7FE9D8}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<p>Este tablero ahora está dentro del sitio: <a href="/tableros/pendientes/">Pendientes</a>.</p>
|
||||||
<header class="top">
|
|
||||||
<div><h1>Tablero de pendientes</h1><div class="meta" id="meta">Ingeniería · Producción · Mecánica · Compras · Campo</div></div>
|
|
||||||
<nav>
|
|
||||||
<a href="../">← Documentación</a>
|
|
||||||
<a class="activo" href="kanban.html">Pendientes</a>
|
|
||||||
<a href="ingenieria.html">Ingeniería</a>
|
|
||||||
<a href="produccion.html">Producción</a>
|
|
||||||
<a href="../inventario/inventario.html">Inventario</a>
|
|
||||||
<a href="../sitios/">Sitios</a>
|
|
||||||
<a href="equipo.html">👥 Equipo</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<details class="como">
|
|
||||||
<summary>Cómo se usa</summary>
|
|
||||||
<ol>
|
|
||||||
<li>Cada tarjeta del tablero es un <b>pendiente</b> (no confundir con la tarjeta EinarBoard). Léelo antes de empezar el trabajo.</li>
|
|
||||||
<li>Arrastra el pendiente a la columna que corresponda; en el teléfono usa <b>Mover a…</b>. El tablero anota quién lo movió y cuándo.</li>
|
|
||||||
<li>Toca el título para ver el detalle: descripción, enlaces, de qué depende y su historial. Ahí mismo edita y guarda.</li>
|
|
||||||
<li><b>Nuevo pendiente</b>: título corto, pista, responsable y fecha límite. Si depende de otro, márcalo en «Bloqueado por».</li>
|
|
||||||
<li>Columna <b>Bloqueado</b>: solo si no puedes avanzar. Escribe en la descripción qué falta y quién lo destraba.</li>
|
|
||||||
<li>Al terminar, muévelo a <b>Hecho</b>. No lo borres: solo el administrador borra, y el historial sirve de bitácora.</li>
|
|
||||||
<li>Rojo = vencido. Filtra por pista, responsable o versión de tarjeta; los filtros no cambian nada en el servidor.</li>
|
|
||||||
</ol>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div id="kb"></div>
|
|
||||||
</div>
|
|
||||||
<script src="kanban.js"></script>
|
|
||||||
<script>
|
|
||||||
Kanban.montar(document.getElementById('kb'), {
|
|
||||||
filtros: true,
|
|
||||||
onCargado: (tareas, meta) => {
|
|
||||||
const abiertas = tareas.filter(t => t.estado !== 'hecho').length;
|
|
||||||
const venc = tareas.filter(t => t.vencida).length;
|
|
||||||
const bloq = tareas.filter(t => t.estado === 'bloqueado').length;
|
|
||||||
document.getElementById('meta').textContent = abiertas + ' abiertos · ' + bloq + ' bloqueados · ' + venc + ' vencidos'
|
|
||||||
+ ' · actualizado ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' })
|
|
||||||
+ (meta.user ? ' · ' + meta.user + (meta.admin ? ' (admin)' : '') : '');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
/* Tablero kanban HBOX — módulo compartido (kanban.html, ingenieria.html, produccion.html).
|
/* Tablero de pendientes HBOX — módulo compartido (tableros/pendientes, ingenieria, produccion, equipo).
|
||||||
Vocabulario: la tarjeta del tablero se llama "pendiente" en pantalla para no confundirla con la
|
Vocabulario: la tarjeta del tablero se llama "pendiente" en pantalla para no confundirla con la
|
||||||
tarjeta EinarBoard. Columnas fijas: Por hacer · En curso · Bloqueado · Hecho.
|
tarjeta EinarBoard. Columnas fijas: Por hacer · En curso · Bloqueado · Hecho.
|
||||||
|
|
||||||
Uso: const kb = Kanban.montar(document.getElementById('kb'), {
|
Uso: const kb = Kanban.montar(document.getElementById('kb'), {
|
||||||
pista: 'ingenieria', // opcional: fija la pista (oculta su filtro y la precarga en "Nuevo")
|
pista: 'ingenieria', // opcional: fija la pista (oculta su filtro y la precarga en "Nuevo")
|
||||||
filtros: true, // barra de filtros (responsable / versión / buscar / pista)
|
filtros: true, // barra de filtros (responsable / versión / buscar / pista / solo los míos)
|
||||||
compacto: false, // tarjetas más pequeñas (tableros que solo muestran un extracto)
|
compacto: false, // tarjetas más pequeñas (tableros que solo muestran un extracto)
|
||||||
|
botonNuevoEn: elemento, // opcional: dónde va el botón «Nuevo pendiente» (p. ej. el encabezado)
|
||||||
onCargado: (tareas, meta) => {} // callback después de cada carga
|
onCargado: (tareas, meta) => {} // callback después de cada carga
|
||||||
});
|
});
|
||||||
kb.recargar();
|
kb.recargar();
|
||||||
@ -14,7 +15,9 @@
|
|||||||
DELETE /api/tarea?id= (solo admin). Cada cambio guarda quién y cuándo en el historial.
|
DELETE /api/tarea?id= (solo admin). Cada cambio guarda quién y cuándo en el historial.
|
||||||
Equipo (14-oct): los responsables son miembros de /api/equipo (GET /api/tareas ya trae el mapa
|
Equipo (14-oct): los responsables son miembros de /api/equipo (GET /api/tareas ya trae el mapa
|
||||||
id → nombre). El pendiente exige responsable; se puede escribir texto libre si la persona aún no
|
id → nombre). El pendiente exige responsable; se puede escribir texto libre si la persona aún no
|
||||||
está dada de alta (el servidor avisa). Kanban.nombresResp(t, equipo) sirve a otras páginas. */
|
está dada de alta (el servidor avisa). Kanban.nombresResp(t, equipo) sirve a otras páginas.
|
||||||
|
Estilos: tableros/tablero.css (todo bajo .tb). Sin emoji: íconos de línea (Kanban.icono) y
|
||||||
|
avatares con iniciales (Kanban.avatar), con el mismo color por persona en todo el sitio. */
|
||||||
(function () {
|
(function () {
|
||||||
const PISTAS = { ingenieria: 'Ingeniería', produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', campo: 'Campo' };
|
const PISTAS = { ingenieria: 'Ingeniería', produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', campo: 'Campo' };
|
||||||
const ESTADOS = [['por-hacer', 'Por hacer'], ['en-curso', 'En curso'], ['bloqueado', 'Bloqueado'], ['hecho', 'Hecho']];
|
const ESTADOS = [['por-hacer', 'Por hacer'], ['en-curso', 'En curso'], ['bloqueado', 'Bloqueado'], ['hecho', 'Hecho']];
|
||||||
@ -22,9 +25,45 @@
|
|||||||
// Lista de respaldo si /api/equipo no responde; lo normal es que el equipo venga del servidor.
|
// Lista de respaldo si /api/equipo no responde; lo normal es que el equipo venga del servidor.
|
||||||
const RESPONSABLES = ['leo', 'adriana', 'michelle', 'jesus', 'alan', 'daniel', 'aldair', 'gibran', 'claude'];
|
const RESPONSABLES = ['leo', 'adriana', 'michelle', 'jesus', 'alan', 'daniel', 'aldair', 'gibran', 'claude'];
|
||||||
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
|
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
|
||||||
|
const MESES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'];
|
||||||
|
|
||||||
|
// Un color por persona, el mismo en todo el sitio (sistema visual del 20-oct).
|
||||||
|
const AVATAR = {
|
||||||
|
leo: ['LR', '#3A5A9A'], adriana: ['AD', '#8A4F7D'], michelle: ['MM', '#2E7D74'], jesus: ['JS', '#9A6A2E'],
|
||||||
|
aldair: ['AL', '#5B5FA8'], alan: ['AB', '#57682B'], daniel: ['DM', '#A0503F'], gibran: ['GB', '#36708E'],
|
||||||
|
produccion: ['PR', '#5A6680'], claude: ['CL', '#4A5468']
|
||||||
|
};
|
||||||
|
const TONOS = ['#3A5A9A', '#8A4F7D', '#2E7D74', '#9A6A2E', '#5B5FA8', '#57682B', '#A0503F', '#36708E', '#5A6680', '#6B4F8A'];
|
||||||
|
|
||||||
|
// Íconos de línea (24×24, trazo redondo).
|
||||||
|
const ICONOS = {
|
||||||
|
liga: 'M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1',
|
||||||
|
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',
|
||||||
|
mas: 'M12 5v14M5 12h14',
|
||||||
|
cerrar: 'M6 6l12 12M18 6 6 18',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
espera: 'M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM12 7.5V12l3 2',
|
||||||
|
ok: 'M5 12.5l4.5 4.5L19 7.5',
|
||||||
|
equipo: 'M9 5.3a3.2 3.2 0 1 0 0 6.4 3.2 3.2 0 0 0 0-6.4zM3.5 19.5c.6-3.2 2.8-5 5.5-5s4.9 1.8 5.5 5M16.8 7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM15.8 14.6c2.3 0 4.2 1.6 4.7 4.4',
|
||||||
|
descargar: 'M12 4v11M7.5 10.5 12 15l4.5-4.5M5 20h14',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
actualizar: 'M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4',
|
||||||
|
imprimir: 'M7 8.5V4h10v4.5M7 17H5a1.5 1.5 0 0 1-1.5-1.5v-5A1.5 1.5 0 0 1 5 9h14a1.5 1.5 0 0 1 1.5 1.5v5A1.5 1.5 0 0 1 19 17h-2M7 14h10v6H7z'
|
||||||
|
};
|
||||||
|
const icono = (n, cls) => '<svg class="i' + (cls ? ' ' + cls : '') + '" viewBox="0 0 24 24" aria-hidden="true"><path d="' + (ICONOS[n] || '') + '"/></svg>';
|
||||||
|
// Íconos de estado de las columnas (con relleno; colores de los tokens).
|
||||||
|
const ESTADO_SVG = {
|
||||||
|
'por-hacer': '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16z" style="fill:none;stroke:var(--hg-texto-3);stroke-width:2;stroke-dasharray:2.6 2.6"/></svg>',
|
||||||
|
'en-curso': '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16z" style="fill:none;stroke:var(--hg-curso);stroke-width:2"/><path d="M12 4a8 8 0 0 1 0 16z" style="fill:var(--hg-curso)"/></svg>',
|
||||||
|
'bloqueado': '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16z" style="fill:none;stroke:var(--hg-bloqueado);stroke-width:2"/><path d="M6.8 17.2 17.2 6.8" style="fill:none;stroke:var(--hg-bloqueado);stroke-width:2.2;stroke-linecap:round"/></svg>',
|
||||||
|
'hecho': '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16z" style="fill:var(--hg-hecho-icono);stroke:var(--hg-hecho-icono);stroke-width:2"/><path d="M8.3 12.3l2.5 2.5 4.9-5.1" style="fill:none;stroke:var(--hg-fondo);stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round"/></svg>'
|
||||||
|
};
|
||||||
|
|
||||||
// ---- equipo: id → {nombre, activo, area}. 'Jesús' ≡ 'jesus' (sin acentos, minúsculas).
|
// ---- equipo: id → {nombre, activo, area}. 'Jesús' ≡ 'jesus' (sin acentos, minúsculas).
|
||||||
const norm = s => String(s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim().replace(/[^a-z0-9_\-.]/g, '');
|
const norm = s => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim().replace(/[^a-z0-9_\-.]/g, '');
|
||||||
let EQUIPO = {};
|
let EQUIPO = {};
|
||||||
let equipoProm = null;
|
let equipoProm = null;
|
||||||
function cargarEquipo() {
|
function cargarEquipo() {
|
||||||
@ -48,11 +87,45 @@
|
|||||||
}
|
}
|
||||||
const libresResp = (t, eq) => { eq = eq || EQUIPO; return partesResp(t).filter(p => !eq[norm(p.replace(/\s+/g, ''))] && !eq[norm(p)]); };
|
const libresResp = (t, eq) => { eq = eq || EQUIPO; return partesResp(t).filter(p => !eq[norm(p.replace(/\s+/g, ''))] && !eq[norm(p)]); };
|
||||||
|
|
||||||
|
// ---- avatares: iniciales sobre el color de la persona
|
||||||
|
function iniciales(nombre) {
|
||||||
|
const w = String(nombre || '?').trim().split(/\s+/).filter(Boolean);
|
||||||
|
if (w.length >= 2) return (w[0][0] + w[1][0]).toUpperCase();
|
||||||
|
return (w[0] || '?').slice(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
function tono(id) { let h = 0; for (const c of String(id)) h = (h * 31 + c.charCodeAt(0)) >>> 0; return TONOS[h % TONOS.length]; }
|
||||||
|
// avatar(id) para un miembro; avatar(null, 'texto libre') para alguien que no está en Equipo
|
||||||
|
function avatar(id, libre) {
|
||||||
|
const s = document.createElement('span');
|
||||||
|
if (id) {
|
||||||
|
const m = EQUIPO[id] || {};
|
||||||
|
const a = AVATAR[id] || [iniciales(m.nombre || id), tono(id)];
|
||||||
|
s.className = 'av'; s.textContent = a[0]; s.style.background = a[1]; s.title = m.nombre || id;
|
||||||
|
} else {
|
||||||
|
s.className = 'av libre'; s.textContent = iniciales(libre); s.title = (libre || '') + ' (sin registrar en Equipo)';
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
function avatares(t) {
|
||||||
|
const g = document.createElement('span'); g.className = 'avs';
|
||||||
|
idsResp(t).forEach(id => g.append(avatar(id)));
|
||||||
|
libresResp(t).forEach(x => g.append(avatar(null, x)));
|
||||||
|
g.title = nombresResp(t).join(', ');
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
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 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 esc = s => { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; };
|
||||||
const hoyISO = () => { const d = new Date(); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); };
|
const hoyISO = () => { const d = new Date(); d.setMinutes(d.getMinutes() - d.getTimezoneOffset()); return d.toISOString().slice(0, 10); };
|
||||||
const fmt = iso => (iso || '').replace('T', ' ').slice(0, 16);
|
const fmt = iso => (iso || '').replace('T', ' ').slice(0, 16);
|
||||||
const fechaCorta = f => { if (!f) return ''; const [y, m, d] = f.split('-'); return d + '-' + ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'][+m - 1] + (y !== hoyISO().slice(0, 4) ? '-' + y.slice(2) : ''); };
|
const fechaCorta = f => { if (!f) return ''; const [y, m, d] = f.split('-'); return (+d) + ' ' + MESES[+m - 1] + (y !== hoyISO().slice(0, 4) ? ' ' + y : ''); };
|
||||||
|
// texto y clase de la fecha límite de un pendiente
|
||||||
|
function fechaDe(t) {
|
||||||
|
if (!t.fecha_limite) return null;
|
||||||
|
if (t.vencida) return { texto: 'Venció ' + fechaCorta(t.fecha_limite), cls: 'mal' };
|
||||||
|
if (t.estado !== 'hecho' && t.fecha_limite === hoyISO()) return { texto: 'Hoy', cls: 'hoy' };
|
||||||
|
return { texto: fechaCorta(t.fecha_limite), cls: '' };
|
||||||
|
}
|
||||||
|
|
||||||
async function api(url, opts) {
|
async function api(url, opts) {
|
||||||
const r = await fetch(url, Object.assign({ cache: 'no-store' }, opts || {}));
|
const r = await fetch(url, Object.assign({ cache: 'no-store' }, opts || {}));
|
||||||
@ -65,27 +138,31 @@
|
|||||||
function montar(root, opts) {
|
function montar(root, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
let tareas = [], admin = false, user = '', porId = {};
|
let tareas = [], admin = false, user = '', porId = {};
|
||||||
const filtro = { pista: opts.pista || '', responsable: '', version: '', buscar: '', hechas: 'recientes' };
|
const q0 = new URLSearchParams(location.search);
|
||||||
|
const filtro = { pista: opts.pista || '', responsable: '', version: '', buscar: '', hechas: 'recientes', mios: q0.get('mios') === '1' };
|
||||||
root.classList.add('kanban-root');
|
root.classList.add('kanban-root');
|
||||||
|
const contenedor = root.closest('.tb') || root;
|
||||||
|
|
||||||
// ---------- barra de filtros ----------
|
// ---------- barra de filtros ----------
|
||||||
const barra = el('div', { class: 'kb-barra' });
|
const barra = el('div', { class: 'kb-barra' });
|
||||||
const selPista = el('select', { title: 'Pista' }); selPista.append(el('option', { value: '', text: 'Todas las pistas' }));
|
const selPista = el('select', { title: 'Pista', 'aria-label': 'Pista' }); selPista.append(el('option', { value: '', text: 'Pista: todas' }));
|
||||||
Object.entries(PISTAS).forEach(([k, v]) => selPista.append(el('option', { value: k, text: v })));
|
Object.entries(PISTAS).forEach(([k, v]) => selPista.append(el('option', { value: k, text: v })));
|
||||||
const selResp = el('select', { title: 'Responsable' }); selResp.append(el('option', { value: '', text: 'Todos los responsables' }));
|
const selResp = el('select', { title: 'Responsable', 'aria-label': 'Responsable' }); selResp.append(el('option', { value: '', text: 'Responsable: todos' }));
|
||||||
const selVer = el('select', { title: 'Versión de tarjeta' }); selVer.append(el('option', { value: '', text: 'Todas las versiones' }));
|
const selVer = el('select', { title: 'Versión de tarjeta', 'aria-label': 'Versión de tarjeta' }); selVer.append(el('option', { value: '', text: 'Tarjeta: todas' }));
|
||||||
VERSIONES.filter(v => v !== '—').forEach(v => selVer.append(el('option', { value: v, text: v })));
|
VERSIONES.filter(v => v !== '—').forEach(v => selVer.append(el('option', { value: v, text: v })));
|
||||||
const selHechas = el('select', { title: 'Hechas' });
|
const selHechas = el('select', { title: 'Hechos', 'aria-label': 'Hechos' });
|
||||||
[['recientes', 'Hechas: últimos 14 días'], ['todas', 'Hechas: todas'], ['ninguna', 'Hechas: ocultar']].forEach(([v, t]) => selHechas.append(el('option', { value: v, text: t })));
|
[['recientes', 'Hechos: últimos 14 días'], ['todas', 'Hechos: todos'], ['ninguna', 'Hechos: ocultar']].forEach(([v, t]) => selHechas.append(el('option', { value: v, text: t })));
|
||||||
const buscar = el('input', { type: 'search', placeholder: 'Buscar…', style: 'min-width:140px;flex:1' });
|
const buscar = el('input', { type: 'search', placeholder: 'Buscar en pendientes', 'aria-label': 'Buscar en pendientes' });
|
||||||
const btnNuevo = el('button', { text: '+ Nuevo pendiente' });
|
const btnMios = el('button', { type: 'button', class: 'kb-solo', 'aria-pressed': String(filtro.mios), html: icono('persona') + 'Solo los míos' });
|
||||||
const cnt = el('span', { class: 'cnt' });
|
const btnNuevo = el('button', { type: 'button', class: 'kb-nuevo', html: icono('mas') + 'Nuevo pendiente' });
|
||||||
if (opts.filtros !== false) {
|
if (opts.filtros !== false) {
|
||||||
|
barra.append(btnMios);
|
||||||
if (!opts.pista) barra.append(selPista);
|
if (!opts.pista) barra.append(selPista);
|
||||||
barra.append(selResp, selVer, selHechas, buscar, btnNuevo, cnt);
|
barra.append(selResp, selVer, selHechas, buscar);
|
||||||
|
if (opts.botonNuevoEn) opts.botonNuevoEn.append(btnNuevo); else barra.append(btnNuevo);
|
||||||
root.append(barra);
|
root.append(barra);
|
||||||
}
|
}
|
||||||
const aviso = el('div', { class: 'msg' }); root.append(aviso);
|
const aviso = el('div', { class: 'msg', role: 'status' }); root.append(aviso);
|
||||||
function mensaje(txt, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.textContent = txt; clearTimeout(aviso._t); aviso._t = setTimeout(() => { aviso.className = 'msg'; }, bien ? 3500 : 8000); }
|
function mensaje(txt, bien) { aviso.className = 'msg ' + (bien ? 'ok' : 'err'); aviso.textContent = txt; clearTimeout(aviso._t); aviso._t = setTimeout(() => { aviso.className = 'msg'; }, bien ? 3500 : 8000); }
|
||||||
|
|
||||||
// ---------- columnas ----------
|
// ---------- columnas ----------
|
||||||
@ -94,7 +171,12 @@
|
|||||||
const cols = {};
|
const cols = {};
|
||||||
ESTADOS.forEach(([k, nombre]) => {
|
ESTADOS.forEach(([k, nombre]) => {
|
||||||
const c = el('div', { class: 'col e-' + k, 'data-estado': k });
|
const c = el('div', { class: 'col e-' + k, 'data-estado': k });
|
||||||
c.append(el('h3', null, el('span', { text: nombre }), el('span', { class: 'n', text: '0' })));
|
c.append(el('h3', { html: ESTADO_SVG[k] + '<span>' + nombre + '</span><span class="n">0</span>' }));
|
||||||
|
if (k !== 'hecho') {
|
||||||
|
const mas = el('button', { type: 'button', class: 'kb-mas', html: icono('mas') + 'Nuevo' });
|
||||||
|
mas.addEventListener('click', () => abrir(null, k));
|
||||||
|
c.append(mas);
|
||||||
|
}
|
||||||
c.addEventListener('dragover', e => { e.preventDefault(); c.classList.add('sobre'); });
|
c.addEventListener('dragover', e => { e.preventDefault(); c.classList.add('sobre'); });
|
||||||
c.addEventListener('dragleave', () => c.classList.remove('sobre'));
|
c.addEventListener('dragleave', () => c.classList.remove('sobre'));
|
||||||
c.addEventListener('drop', e => {
|
c.addEventListener('drop', e => {
|
||||||
@ -114,8 +196,10 @@
|
|||||||
function visibles() {
|
function visibles() {
|
||||||
const q = filtro.buscar.trim().toLowerCase();
|
const q = filtro.buscar.trim().toLowerCase();
|
||||||
const corte = new Date(Date.now() - 14 * 86400000).toISOString().slice(0, 10);
|
const corte = new Date(Date.now() - 14 * 86400000).toISOString().slice(0, 10);
|
||||||
|
const yo = norm(user);
|
||||||
return tareas.filter(t => {
|
return tareas.filter(t => {
|
||||||
if (filtro.pista && t.pista !== filtro.pista) return false;
|
if (filtro.pista && t.pista !== filtro.pista) return false;
|
||||||
|
if (filtro.mios && yo && !idsResp(t).includes(yo) && !String(t.responsable || '').toLowerCase().includes(yo)) return false;
|
||||||
if (filtro.responsable) {
|
if (filtro.responsable) {
|
||||||
const f = filtro.responsable;
|
const f = filtro.responsable;
|
||||||
if (EQUIPO[f]) { if (!idsResp(t).includes(f)) return false; }
|
if (EQUIPO[f]) { if (!idsResp(t).includes(f)) return false; }
|
||||||
@ -126,34 +210,47 @@
|
|||||||
if (filtro.hechas === 'ninguna') return false;
|
if (filtro.hechas === 'ninguna') return false;
|
||||||
if (filtro.hechas === 'recientes' && (t.modificado_en || '').slice(0, 10) < corte) return false;
|
if (filtro.hechas === 'recientes' && (t.modificado_en || '').slice(0, 10) < corte) return false;
|
||||||
}
|
}
|
||||||
if (q) { const txt = [t.titulo, t.descripcion, t.responsable, (t.etiquetas || []).join(' '), t.id].join(' ').toLowerCase(); if (!txt.includes(q)) return false; }
|
if (q) { const txt = [t.titulo, t.descripcion, t.responsable, nombresResp(t).join(' '), (t.etiquetas || []).join(' '), t.id].join(' ').toLowerCase(); if (!txt.includes(q)) return false; }
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function bloqueosAbiertos(t) { return (t.bloqueada_por || []).map(id => porId[id]).filter(b => b && b.estado !== 'hecho'); }
|
function bloqueosAbiertos(t) { return (t.bloqueada_por || []).map(id => porId[id]).filter(b => b && b.estado !== 'hecho'); }
|
||||||
|
const corto = (s, n) => s.length > n ? s.slice(0, n - 1) + '…' : s;
|
||||||
|
|
||||||
function tarjeta(t) {
|
function tarjeta(t) {
|
||||||
const c = el('div', { class: 'card p-' + t.pista + (t.vencida ? ' vencida' : ''), draggable: 'true', 'data-id': t.id });
|
const c = el('div', { class: 'card p-' + t.pista + ' e-' + t.estado + (t.vencida ? ' vencida' : ''), draggable: 'true', 'data-id': t.id });
|
||||||
c.addEventListener('dragstart', e => { e.dataTransfer.setData('text/plain', t.id); e.dataTransfer.effectAllowed = 'move'; c.classList.add('arrastrando'); });
|
c.addEventListener('dragstart', e => { e.dataTransfer.setData('text/plain', t.id); e.dataTransfer.effectAllowed = 'move'; c.classList.add('arrastrando'); });
|
||||||
c.addEventListener('dragend', () => c.classList.remove('arrastrando'));
|
c.addEventListener('dragend', () => c.classList.remove('arrastrando'));
|
||||||
const tit = el('div', { class: 'tit', text: t.titulo }); tit.addEventListener('click', () => abrir(t)); c.append(tit);
|
// pista y versión de tarjeta
|
||||||
const meta = el('div', { class: 'meta' });
|
const sup = el('div', { class: 'sup' });
|
||||||
if (!opts.pista) meta.append(el('span', { class: 'chip p-' + t.pista, text: PISTAS[t.pista] || t.pista }));
|
if (!opts.pista) sup.append(el('span', { class: 'pista-p' }), el('span', { text: PISTAS[t.pista] || t.pista }));
|
||||||
if (t.version_tarjeta && t.version_tarjeta !== '—') meta.append(el('span', { class: BADGE[t.version_tarjeta] || 'chip', text: t.version_tarjeta }));
|
if (t.version_tarjeta && t.version_tarjeta !== '—') sup.append(el('span', { class: (BADGE[t.version_tarjeta] || 'chip') + ' ver-t', text: t.version_tarjeta }));
|
||||||
if (t.responsable) meta.append(el('span', { text: '👤 ' + nombresResp(t).join(', '), title: t.responsable }));
|
if (sup.childNodes.length) c.append(sup);
|
||||||
if (t.fecha_limite) meta.append(el('span', { class: 'fecha' + (t.vencida ? ' mal' : ''), text: (t.vencida ? '⚠ venció ' : '📅 ') + fechaCorta(t.fecha_limite) }));
|
// título (abre el detalle)
|
||||||
(t.etiquetas || []).slice(0, 3).forEach(x => meta.append(el('span', { class: 'chip', text: x })));
|
const tit = el('div', { class: 'tit', text: t.titulo, role: 'button', tabindex: '0' });
|
||||||
c.append(meta);
|
tit.addEventListener('click', () => abrir(t));
|
||||||
|
tit.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); abrir(t); } });
|
||||||
|
c.append(tit);
|
||||||
|
// de qué depende
|
||||||
const bl = bloqueosAbiertos(t);
|
const bl = bloqueosAbiertos(t);
|
||||||
if (bl.length) c.append(el('div', { class: 'bloq', text: '⛓ espera: ' + bl.map(b => b.titulo.length > 38 ? b.titulo.slice(0, 37) + '…' : b.titulo).join(' · ') }));
|
if (bl.length) c.append(el('div', { class: 'nota', html: icono('aviso') + '<span>Espera: ' + esc(bl.map(b => corto(b.titulo.split(':')[0], 42)).join(' · ')) + '</span>' }));
|
||||||
|
// ligas
|
||||||
if ((t.enlaces || []).length) {
|
if ((t.enlaces || []).length) {
|
||||||
const e = el('div', { class: 'enl' });
|
const e = el('div', { class: 'enl' });
|
||||||
t.enlaces.slice(0, 3).forEach(x => e.append(el('a', { href: x.url, text: '🔗 ' + (x.titulo || x.url) })));
|
t.enlaces.slice(0, 2).forEach(x => { const a = el('a', { href: x.url, html: icono('liga') + '<span>' + esc(x.titulo || x.url) + '</span>' }); a.addEventListener('click', ev => ev.stopPropagation()); e.append(a); });
|
||||||
|
if (t.enlaces.length > 2) e.append(el('span', { class: 'vacio', text: '+' + (t.enlaces.length - 2) + ' más' }));
|
||||||
c.append(e);
|
c.append(e);
|
||||||
}
|
}
|
||||||
|
if ((t.etiquetas || []).length) { const g = el('div', { class: 'etq' }); t.etiquetas.slice(0, 3).forEach(x => g.append(el('span', { class: 'chip', text: x }))); c.append(g); }
|
||||||
|
// responsables y fecha
|
||||||
|
const pie = el('div', { class: 'pie' });
|
||||||
|
pie.append(t.responsable ? avatares(t) : el('span', { class: 'vacio', text: 'Sin responsable' }));
|
||||||
|
const f = fechaDe(t);
|
||||||
|
if (f) pie.append(el('span', { class: 'fecha ' + f.cls, text: f.texto, title: 'Fecha límite ' + t.fecha_limite }));
|
||||||
|
c.append(pie);
|
||||||
// "Mover a…": teléfonos y quien no arrastra
|
// "Mover a…": teléfonos y quien no arrastra
|
||||||
const mv = el('div', { class: 'mover' });
|
const mv = el('div', { class: 'mover' });
|
||||||
const sel = el('select');
|
const sel = el('select', { 'aria-label': 'Mover a otra columna' });
|
||||||
sel.append(el('option', { value: '', text: 'Mover a…' }));
|
sel.append(el('option', { value: '', text: 'Mover a…' }));
|
||||||
ESTADOS.filter(([k]) => k !== t.estado).forEach(([k, n]) => sel.append(el('option', { value: k, text: n })));
|
ESTADOS.filter(([k]) => k !== t.estado).forEach(([k, n]) => sel.append(el('option', { value: k, text: n })));
|
||||||
sel.addEventListener('change', () => { if (sel.value) mover(t.id, sel.value, 0); });
|
sel.addEventListener('change', () => { if (sel.value) mover(t.id, sel.value, 0); });
|
||||||
@ -168,7 +265,8 @@
|
|||||||
const c = cols[k]; c.querySelectorAll('.card').forEach(x => x.remove());
|
const c = cols[k]; c.querySelectorAll('.card').forEach(x => x.remove());
|
||||||
const lst = vis.filter(t => t.estado === k).sort((a, b) => (a.orden - b.orden) || (a.creado_en < b.creado_en ? -1 : 1));
|
const lst = vis.filter(t => t.estado === k).sort((a, b) => (a.orden - b.orden) || (a.creado_en < b.creado_en ? -1 : 1));
|
||||||
c.querySelector('.n').textContent = lst.length;
|
c.querySelector('.n').textContent = lst.length;
|
||||||
lst.forEach(t => c.append(tarjeta(t)));
|
const mas = c.querySelector('.kb-mas');
|
||||||
|
lst.forEach(t => mas ? c.insertBefore(tarjeta(t), mas) : c.append(tarjeta(t)));
|
||||||
});
|
});
|
||||||
// responsables presentes en los datos (más los conocidos)
|
// responsables presentes en los datos (más los conocidos)
|
||||||
selResp.querySelectorAll('option:not(:first-child)').forEach(o => o.remove());
|
selResp.querySelectorAll('option:not(:first-child)').forEach(o => o.remove());
|
||||||
@ -177,8 +275,9 @@
|
|||||||
const libres = new Set(); tareas.forEach(t => libresResp(t).forEach(x => libres.add(x.toLowerCase())));
|
const libres = new Set(); tareas.forEach(t => libresResp(t).forEach(x => libres.add(x.toLowerCase())));
|
||||||
[...libres].sort().forEach(x => selResp.append(el('option', { value: x, text: x + ' (sin registrar)' })));
|
[...libres].sort().forEach(x => selResp.append(el('option', { value: x, text: x + ' (sin registrar)' })));
|
||||||
selResp.value = filtro.responsable;
|
selResp.value = filtro.responsable;
|
||||||
const venc = vis.filter(t => t.vencida).length;
|
btnMios.setAttribute('aria-pressed', String(filtro.mios));
|
||||||
cnt.textContent = vis.length + ' pendiente(s)' + (venc ? ' · ' + venc + ' vencido(s)' : '') + (vis.length !== tareas.length ? ' · de ' + tareas.length : '');
|
btnMios.disabled = !user;
|
||||||
|
btnMios.title = user ? 'Solo los pendientes de ' + ((EQUIPO[norm(user)] || {}).nombre || user) : 'No se sabe quién eres (falta X-Remote-User)';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recargar() {
|
async function recargar() {
|
||||||
@ -204,22 +303,23 @@
|
|||||||
try {
|
try {
|
||||||
if (!mismaCol) await api('/api/tarea?id=' + encodeURIComponent(id), json('PATCH', { estado, orden: ids.indexOf(id) }));
|
if (!mismaCol) await api('/api/tarea?id=' + encodeURIComponent(id), json('PATCH', { estado, orden: ids.indexOf(id) }));
|
||||||
await api('/api/tareas/orden', json('POST', { estado, ids }));
|
await api('/api/tareas/orden', json('POST', { estado, ids }));
|
||||||
if (!mismaCol) mensaje('«' + t.titulo.slice(0, 40) + (t.titulo.length > 40 ? '…' : '') + '» → ' + ESTADOS.find(e => e[0] === estado)[1], true);
|
if (!mismaCol) mensaje('«' + corto(t.titulo, 41) + '» pasó a ' + ESTADOS.find(e => e[0] === estado)[1] + '.', true);
|
||||||
await recargar();
|
await recargar();
|
||||||
} catch (e) { t.estado = prev.estado; t.orden = prev.orden; pintar(); mensaje('No se pudo mover: ' + e.message, false); }
|
} catch (e) { t.estado = prev.estado; t.orden = prev.orden; pintar(); mensaje('No se pudo mover: ' + e.message, false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- diálogo: alta y detalle ----------
|
// ---------- diálogo: alta y detalle ----------
|
||||||
const dlg = el('dialog');
|
const dlg = el('dialog', { class: 'kb-dlg', 'aria-label': 'Pendiente' });
|
||||||
document.body.append(dlg);
|
contenedor.append(dlg);
|
||||||
function campo(lbl, input) { const d = el('div'); d.append(el('label', { text: lbl }), input); return d; }
|
function campo(lbl, input) { const d = el('div'); d.append(el('label', { text: lbl }), input); return d; }
|
||||||
function abrir(t) {
|
function abrir(t, estadoInicial) {
|
||||||
const nuevo = !t;
|
const nuevo = !t;
|
||||||
t = t || { titulo: '', descripcion: '', pista: opts.pista || 'produccion', estado: 'por-hacer', responsable: '', fecha_limite: null, enlaces: [], bloqueada_por: [], etiquetas: [], version_tarjeta: '—' };
|
t = t || { titulo: '', descripcion: '', pista: opts.pista || 'produccion', estado: estadoInicial || 'por-hacer', responsable: '', fecha_limite: null, enlaces: [], bloqueada_por: [], etiquetas: [], version_tarjeta: '—' };
|
||||||
dlg.innerHTML = '';
|
dlg.innerHTML = '';
|
||||||
const cab = el('div', { class: 'dcab' });
|
const cab = el('div', { class: 'dcab' });
|
||||||
cab.append(el('h2', { text: nuevo ? 'Nuevo pendiente' : 'Pendiente ' + t.id }));
|
cab.append(el('h2', { text: nuevo ? 'Nuevo pendiente' : 'Pendiente' }));
|
||||||
const cerrar = el('button', { type: 'button', text: '✕', title: 'Cerrar' }); cerrar.addEventListener('click', () => dlg.close()); cab.append(cerrar);
|
if (!nuevo) cab.append(el('span', { class: 'dcab-id', text: t.id }));
|
||||||
|
const cerrar = el('button', { type: 'button', title: 'Cerrar', 'aria-label': 'Cerrar', html: icono('cerrar') }); cerrar.addEventListener('click', () => dlg.close()); cab.append(cerrar);
|
||||||
dlg.append(cab);
|
dlg.append(cab);
|
||||||
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
||||||
|
|
||||||
@ -234,12 +334,12 @@
|
|||||||
const l = el('label', { class: 'chip-sel' + (yaIds.includes(id) ? ' on' : '') });
|
const l = el('label', { class: 'chip-sel' + (yaIds.includes(id) ? ' on' : '') });
|
||||||
const ck = el('input', { type: 'checkbox', value: id }); ck.checked = yaIds.includes(id);
|
const ck = el('input', { type: 'checkbox', value: id }); ck.checked = yaIds.includes(id);
|
||||||
ck.addEventListener('change', () => l.classList.toggle('on', ck.checked));
|
ck.addEventListener('change', () => l.classList.toggle('on', ck.checked));
|
||||||
l.append(ck, document.createTextNode(m.nombre + (m.activo === false ? ' (inactivo)' : ''))); fResp.append(l);
|
l.append(ck, avatar(id), document.createTextNode(m.nombre + (m.activo === false ? ' (inactivo)' : ''))); fResp.append(l);
|
||||||
});
|
});
|
||||||
const fLibre = el('input', { type: 'text', value: libresResp(t).join(', '), placeholder: 'Otro (texto libre, solo si la persona aún no está en Equipo)', style: 'margin-top:6px' });
|
const fLibre = el('input', { type: 'text', value: libresResp(t).join(', '), placeholder: 'Otro (texto libre, solo si la persona aún no está en Equipo)', style: 'margin-top:8px' });
|
||||||
const leerResp = () => [...fResp.querySelectorAll('input:checked')].map(c => c.value).concat(fLibre.value.split(',').map(x => x.trim()).filter(Boolean)).join(', ');
|
const leerResp = () => [...fResp.querySelectorAll('input:checked')].map(c => c.value).concat(fLibre.value.split(',').map(x => x.trim()).filter(Boolean)).join(', ');
|
||||||
const fFecha = el('input', { type: 'date', value: t.fecha_limite || '' });
|
const fFecha = el('input', { type: 'date', value: t.fecha_limite || '' });
|
||||||
const fVer = el('select'); VERSIONES.forEach(v => fVer.append(el('option', { value: v, text: v === '—' ? '— (no aplica)' : v }))); fVer.value = t.version_tarjeta || '—';
|
const fVer = el('select'); VERSIONES.forEach(v => fVer.append(el('option', { value: v, text: v === '—' ? 'No aplica' : v }))); fVer.value = t.version_tarjeta || '—';
|
||||||
const fEtq = el('input', { type: 'text', value: (t.etiquetas || []).join(', '), placeholder: 'layout, pedido 570, bloqueante' });
|
const fEtq = el('input', { type: 'text', value: (t.etiquetas || []).join(', '), placeholder: 'layout, pedido 570, bloqueante' });
|
||||||
|
|
||||||
cuerpo.append(campo('Título', fTit), campo('Descripción', fDesc));
|
cuerpo.append(campo('Título', fTit), campo('Descripción', fDesc));
|
||||||
@ -252,19 +352,19 @@
|
|||||||
const f = el('div', { class: 'fila' });
|
const f = el('div', { class: 'fila' });
|
||||||
const a = el('input', { type: 'text', value: e.titulo || '', placeholder: 'Título' });
|
const a = el('input', { type: 'text', value: e.titulo || '', placeholder: 'Título' });
|
||||||
const b = el('input', { type: 'text', value: e.url || '', placeholder: '/hardware/… o https://…' });
|
const b = el('input', { type: 'text', value: e.url || '', placeholder: '/hardware/… o https://…' });
|
||||||
const q = el('button', { type: 'button', class: 'sec', text: '✕' }); q.addEventListener('click', () => f.remove());
|
const q = el('button', { type: 'button', class: 'sec', title: 'Quitar la liga', 'aria-label': 'Quitar la liga', html: icono('cerrar') }); q.addEventListener('click', () => f.remove());
|
||||||
f.append(a, b, q); enlBox.append(f);
|
f.append(a, b, q); enlBox.append(f);
|
||||||
}
|
}
|
||||||
(t.enlaces || []).forEach(filaEnlace);
|
(t.enlaces || []).forEach(filaEnlace);
|
||||||
const addEnl = el('button', { type: 'button', class: 'sec', text: '+ enlace', style: 'padding:6px 10px;font-size:13px' }); addEnl.addEventListener('click', () => filaEnlace({}));
|
const addEnl = el('button', { type: 'button', class: 'sec', html: icono('mas') + 'Liga', style: 'padding:6px 10px;font-size:13px' }); addEnl.addEventListener('click', () => filaEnlace({}));
|
||||||
cuerpo.append(campo('Enlaces (documentos, actas, expediente de unidad)', enlBox), addEnl);
|
cuerpo.append(campo('Documentos, actas o expediente de unidad', enlBox), addEnl);
|
||||||
|
|
||||||
// bloqueada por (otros pendientes abiertos)
|
// bloqueada por (otros pendientes abiertos)
|
||||||
const lst = el('div', { class: 'lista-chk' });
|
const lst = el('div', { class: 'lista-chk' });
|
||||||
const otros = tareas.filter(x => x.id !== t.id && (x.estado !== 'hecho' || (t.bloqueada_por || []).includes(x.id)));
|
const otros = tareas.filter(x => x.id !== t.id && (x.estado !== 'hecho' || (t.bloqueada_por || []).includes(x.id)));
|
||||||
if (!otros.length) lst.append(el('span', { class: 'vacio', text: 'No hay otros pendientes.' }));
|
if (!otros.length) lst.append(el('span', { class: 'vacio', text: 'No hay otros pendientes.' }));
|
||||||
otros.forEach(x => { const l = el('label'); const ck = el('input', { type: 'checkbox', value: x.id }); ck.checked = (t.bloqueada_por || []).includes(x.id); l.append(ck, document.createTextNode(x.titulo + ' (' + (PISTAS[x.pista] || x.pista) + ')')); lst.append(l); });
|
otros.forEach(x => { const l = el('label'); const ck = el('input', { type: 'checkbox', value: x.id }); ck.checked = (t.bloqueada_por || []).includes(x.id); l.append(ck, document.createTextNode(x.titulo + ' (' + (PISTAS[x.pista] || x.pista) + ')')); lst.append(l); });
|
||||||
cuerpo.append(campo('Bloqueado por (espera a que terminen)', lst));
|
cuerpo.append(campo('Espera a que terminen', lst));
|
||||||
|
|
||||||
if (!nuevo) {
|
if (!nuevo) {
|
||||||
const h = el('div', { class: 'hist' });
|
const h = el('div', { class: 'hist' });
|
||||||
@ -301,7 +401,7 @@
|
|||||||
const cambios = {}; Object.keys(d).forEach(k => { if (JSON.stringify(d[k]) !== JSON.stringify(t[k] == null ? (Array.isArray(d[k]) ? [] : (k === 'fecha_limite' ? null : '')) : t[k])) cambios[k] = d[k]; });
|
const cambios = {}; Object.keys(d).forEach(k => { if (JSON.stringify(d[k]) !== JSON.stringify(t[k] == null ? (Array.isArray(d[k]) ? [] : (k === 'fecha_limite' ? null : '')) : t[k])) cambios[k] = d[k]; });
|
||||||
if (Object.keys(cambios).length) r = await api('/api/tarea?id=' + encodeURIComponent(t.id), json('PATCH', cambios));
|
if (Object.keys(cambios).length) r = await api('/api/tarea?id=' + encodeURIComponent(t.id), json('PATCH', cambios));
|
||||||
}
|
}
|
||||||
dlg.close(); mensaje((nuevo ? 'Pendiente creado.' : 'Cambios guardados.') + (r && r.aviso ? ' ⚠ ' + r.aviso : ''), !(r && r.aviso)); recargar();
|
dlg.close(); mensaje((nuevo ? 'Pendiente creado.' : 'Cambios guardados.') + (r && r.aviso ? ' Aviso: ' + r.aviso : ''), !(r && r.aviso)); recargar();
|
||||||
} catch (e) { m.className = 'msg err'; m.textContent = e.message; }
|
} catch (e) { m.className = 'msg err'; m.textContent = e.message; }
|
||||||
guardar.disabled = false;
|
guardar.disabled = false;
|
||||||
});
|
});
|
||||||
@ -316,15 +416,19 @@
|
|||||||
selVer.addEventListener('change', () => { filtro.version = selVer.value; pintar(); });
|
selVer.addEventListener('change', () => { filtro.version = selVer.value; pintar(); });
|
||||||
selHechas.addEventListener('change', () => { filtro.hechas = selHechas.value; pintar(); });
|
selHechas.addEventListener('change', () => { filtro.hechas = selHechas.value; pintar(); });
|
||||||
buscar.addEventListener('input', () => { filtro.buscar = buscar.value; pintar(); });
|
buscar.addEventListener('input', () => { filtro.buscar = buscar.value; pintar(); });
|
||||||
|
btnMios.addEventListener('click', () => {
|
||||||
|
filtro.mios = !filtro.mios; pintar();
|
||||||
|
const u = new URL(location.href); if (filtro.mios) u.searchParams.set('mios', '1'); else u.searchParams.delete('mios');
|
||||||
|
history.replaceState(null, '', u.pathname + u.search + u.hash);
|
||||||
|
});
|
||||||
btnNuevo.addEventListener('click', () => abrir(null));
|
btnNuevo.addEventListener('click', () => abrir(null));
|
||||||
const q = new URLSearchParams(location.search);
|
if (q0.get('pista') && !opts.pista) { selPista.value = q0.get('pista'); filtro.pista = q0.get('pista'); }
|
||||||
if (q.get('pista') && !opts.pista) { selPista.value = q.get('pista'); filtro.pista = q.get('pista'); }
|
if (q0.get('responsable')) { filtro.responsable = q0.get('responsable').toLowerCase(); }
|
||||||
if (q.get('responsable')) { filtro.responsable = q.get('responsable').toLowerCase(); }
|
if (q0.get('id')) setTimeout(() => { const t = porId[q0.get('id')]; if (t) abrir(t); }, 600);
|
||||||
if (q.get('id')) setTimeout(() => { const t = porId[q.get('id')]; if (t) abrir(t); }, 600);
|
|
||||||
|
|
||||||
recargar();
|
recargar();
|
||||||
return { recargar, abrir, get tareas() { return tareas; }, get admin() { return admin; } };
|
return { recargar, abrir, get tareas() { return tareas; }, get admin() { return admin; }, get user() { return user; } };
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Kanban = { montar, PISTAS, ESTADOS, VERSIONES, RESPONSABLES, cargarEquipo, usarEquipo, nombresResp, idsResp, norm, get equipo() { return EQUIPO; } };
|
window.Kanban = { montar, PISTAS, ESTADOS, VERSIONES, RESPONSABLES, cargarEquipo, usarEquipo, nombresResp, idsResp, norm, avatar, avatares, icono, fechaDe, get equipo() { return EQUIPO; } };
|
||||||
})();
|
})();
|
||||||
|
|||||||
47
docs/tableros/pendientes.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
title: "Pendientes"
|
||||||
|
description: "El tablero de pendientes del equipo: por hacer, en curso, bloqueado y hecho."
|
||||||
|
template: tablero.html
|
||||||
|
hide:
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="tb tb-pendientes">
|
||||||
|
<header class="tb-cab">
|
||||||
|
<div class="tb-cab__t"><h1>Pendientes</h1><span class="meta" id="meta">Ingeniería, producción, mecánica, compras y campo</span></div>
|
||||||
|
<div class="tb-cab__acc"><span id="acciones-kb"></span></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<details class="info tb-como">
|
||||||
|
<summary>Cómo se usa</summary>
|
||||||
|
<ol>
|
||||||
|
<li>Cada tarjeta del tablero es un <b>pendiente</b> (no confundir con la tarjeta EinarBoard). Léelo antes de empezar el trabajo.</li>
|
||||||
|
<li>Arrastra el pendiente a la columna que corresponda; en el teléfono usa <b>Mover a…</b>. El tablero anota quién lo movió y cuándo.</li>
|
||||||
|
<li>Toca el título para ver el detalle: descripción, enlaces, de qué depende y su historial. Ahí mismo edita y guarda.</li>
|
||||||
|
<li><b>Nuevo pendiente</b>: título corto, pista, responsable y fecha límite. Si depende de otro, márcalo en «Bloqueado por».</li>
|
||||||
|
<li>Columna <b>Bloqueado</b>: solo si no puedes avanzar. Escribe en la descripción qué falta y quién lo destraba.</li>
|
||||||
|
<li>Al terminar, muévelo a <b>Hecho</b>. No lo borres: solo el administrador borra, y el historial sirve de bitácora.</li>
|
||||||
|
<li>Rojo = vencido. Filtra por pista, responsable o versión de tarjeta; los filtros no cambian nada en el servidor.</li>
|
||||||
|
</ol>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div id="kb"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
Kanban.montar(document.getElementById('kb'), {
|
||||||
|
filtros: true,
|
||||||
|
botonNuevoEn: document.getElementById('acciones-kb'),
|
||||||
|
onCargado: (tareas, meta) => {
|
||||||
|
const abiertas = tareas.filter(t => t.estado !== 'hecho').length;
|
||||||
|
const venc = tareas.filter(t => t.vencida).length;
|
||||||
|
const bloq = tareas.filter(t => t.estado === 'bloqueado').length;
|
||||||
|
const n = (k, uno, varios) => k + ' ' + (k === 1 ? uno : varios);
|
||||||
|
document.getElementById('meta').textContent = [n(abiertas, 'abierto', 'abiertos'), n(bloq, 'bloqueado', 'bloqueados'), n(venc, 'vencido', 'vencidos')].join(' · ')
|
||||||
|
+ ' · al día a las ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||||
|
+ (meta.user ? ' · ' + meta.user + (meta.admin ? ' (admin)' : '') : '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@ -1,236 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
<!-- Desde el 20-oct-2026 este tablero vive dentro del sitio: docs/tableros/produccion.md → /tableros/produccion/
|
||||||
|
Este archivo solo redirige (conserva ?serie=, ?id=, ?pista=… y #…) para que las ligas viejas
|
||||||
|
guardadas en pendientes y actas sigan sirviendo. No se edita: los cambios van en tableros/produccion.md. -->
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Tablero de producción — pedido 570</title>
|
<meta name="robots" content="noindex">
|
||||||
<link rel="stylesheet" href="tablero.css">
|
<title>Producción - Drogheda</title>
|
||||||
|
<script>location.replace('/tableros/produccion/' + location.search + location.hash);</script>
|
||||||
|
<meta http-equiv="refresh" content="0; url=/tableros/produccion/">
|
||||||
|
<style>body{margin:0;padding:24px;background:#0E1524;color:#E7ECF5;font:16px/1.5 system-ui,sans-serif}a{color:#7FE9D8}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<p>Este tablero ahora está dentro del sitio: <a href="/tableros/produccion/">Producción</a>.</p>
|
||||||
<header class="top">
|
|
||||||
<div><h1>Tablero de producción — pedido de 570 HBOX</h1><div class="meta" id="meta">Cargando…</div></div>
|
|
||||||
<nav>
|
|
||||||
<a href="../">← Documentación</a>
|
|
||||||
<a href="kanban.html">Pendientes</a>
|
|
||||||
<a href="ingenieria.html">Ingeniería</a>
|
|
||||||
<a class="activo" href="produccion.html">Producción</a>
|
|
||||||
<a href="../tablero.html">Detalle por tarjeta</a>
|
|
||||||
<a href="../inventario/inventario.html">Inventario</a>
|
|
||||||
<a href="equipo.html">👥 Equipo</a>
|
|
||||||
<button id="refrescar" style="padding:6px 12px">Actualizar</button>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<details class="como">
|
|
||||||
<summary>Cómo se usa</summary>
|
|
||||||
<ol>
|
|
||||||
<li>Arriba: el <b>plan</b> (cuántas unidades de cada versión y para cuándo) contra lo <b>real</b>, que sale solo de las actas enviadas. Si una unidad no tiene acta, aquí no existe.</li>
|
|
||||||
<li>El <b>embudo</b> cuenta series distintas por etapa: tarjeta probada → módulos → integración → ensamble → prueba final → configuración → envío → instalación. Toca una etapa para ir a su acta.</li>
|
|
||||||
<li><b>Bloqueos</b>: pendientes en Bloqueado de las pistas Producción, Mecánica y Compras. Resuélvelos en el <a href="kanban.html">tablero de pendientes</a>.</li>
|
|
||||||
<li><b>Alertas de inventario</b>: ítems por debajo del mínimo. Registra las entradas al llegar el material.</li>
|
|
||||||
<li>Solo el administrador edita el plan (botón «Editar plan»). Las fechas son objetivo, no promesa.</li>
|
|
||||||
</ol>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div id="cont"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dialog id="dlgPlan"></dialog>
|
|
||||||
<script src="kanban.js"></script>
|
|
||||||
<script>
|
|
||||||
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 esPrueba = s => String(s || '').toUpperCase().includes('PRUEBA');
|
|
||||||
const esUnidad = s => /^HBOX/i.test(String(s || ''));
|
|
||||||
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
|
|
||||||
const FORM = { 'prueba-v5': '../formularios/v5.html', 'prueba-v4': '../formularios/v4.html', 'modulo-ard': '../formularios/ardusimple.html', 'modulo-4g': '../formularios/modem4g.html',
|
|
||||||
'integracion': '../formularios/integracion.html', 'ensamble': '../formularios/ensamble.html', 'reflasheo': '../formularios/reflasheo.html',
|
|
||||||
'prueba-ethernet-serie': '../formularios/ethernet-serie.html', 'envio': '../formularios/envio.html', 'instalacion-campo': '../formularios/instalacion.html' };
|
|
||||||
const PISTAS = { produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', ingenieria: 'Ingeniería', campo: 'Campo' };
|
|
||||||
const cont = document.getElementById('cont'), meta = document.getElementById('meta');
|
|
||||||
let PLAN = null, ADMIN = false, EQUIPO = {};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function seriesEtapa(et, actas) {
|
|
||||||
const s = new Set();
|
|
||||||
actas.forEach(a => {
|
|
||||||
if (!et.tipos.includes(a.tipo) || esPrueba(a.serie)) return;
|
|
||||||
if (et.ok && a.resultado === 'FALLA') return;
|
|
||||||
if (et.solo_unidades && !esUnidad(a.serie)) return;
|
|
||||||
s.add(a.serie);
|
|
||||||
});
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(plan, actas, series, danos, bloqueos, inv) {
|
|
||||||
cont.innerHTML = '';
|
|
||||||
const reales = actas.filter(a => !esPrueba(a.serie));
|
|
||||||
const ens = reales.filter(a => a.tipo === 'ensamble' && a.resultado !== 'FALLA');
|
|
||||||
const unidades = new Set(ens.map(a => a.serie));
|
|
||||||
const enviadas = new Set(reales.filter(a => a.tipo === 'envio' && esUnidad(a.serie)).map(a => a.serie));
|
|
||||||
const instaladas = new Set(reales.filter(a => a.tipo === 'instalacion-campo' && a.resultado !== 'FALLA').map(a => a.serie));
|
|
||||||
const total = plan.versiones.reduce((n, v) => n + (v.objetivo || 0), 0);
|
|
||||||
|
|
||||||
// ---- KPIs
|
|
||||||
const kp = el('div', { class: 'kpis' });
|
|
||||||
const kpi = (n, l, cls) => kp.append(el('div', { class: 'kpi ' + (cls || '') }, el('div', { class: 'n', text: String(n) }), el('div', { class: 'l', text: l })));
|
|
||||||
kpi(plan.pedido_total || total, 'Pedido total');
|
|
||||||
kpi(unidades.size, 'Unidades HBOX ensambladas (acta OK)', 'bien');
|
|
||||||
kpi(enviadas.size, 'Unidades enviadas');
|
|
||||||
kpi(instaladas.size, 'Instaladas en sitio');
|
|
||||||
kpi(danos.length, 'Series dañadas o de baja', danos.length ? 'mal' : '');
|
|
||||||
kpi(bloqueos.length, 'Bloqueos abiertos', bloqueos.length ? 'mal' : '');
|
|
||||||
kpi(inv.length, 'Alertas de inventario', inv.length ? 'mal' : '');
|
|
||||||
cont.append(kp);
|
|
||||||
|
|
||||||
// ---- plan vs real por versión
|
|
||||||
const b1 = el('section', { class: 'bloque' });
|
|
||||||
const cab = el('div', { style: 'display:flex;justify-content:space-between;gap:10px;align-items:baseline;flex-wrap:wrap' });
|
|
||||||
cab.append(el('h2', { text: 'Plan por versión vs. real' }));
|
|
||||||
if (ADMIN) { const b = el('button', { class: 'sec', text: 'Editar plan', style: 'padding:6px 12px' }); b.addEventListener('click', editarPlan); cab.append(b); }
|
|
||||||
b1.append(cab);
|
|
||||||
b1.append(el('p', { class: 'sub', html: esc(plan.nota || '') + (plan.actualizado_en ? ' · plan actualizado por <b>' + esc(plan.actualizado_por) + '</b> el ' + esc(fmt(plan.actualizado_en)) : (plan._default ? ' · <b>plan por omisión</b>: aún no se ha guardado (siembra o «Editar plan»)' : '')) }));
|
|
||||||
const grid = el('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px' });
|
|
||||||
plan.versiones.forEach(v => {
|
|
||||||
const pref = (v.serie_prefijo || '').toUpperCase();
|
|
||||||
const hechas = pref ? ens.filter(a => String((a.componentes || {}).einar || '').toUpperCase().startsWith(pref + '-')).length : 0;
|
|
||||||
const pct = v.objetivo ? Math.min(100, Math.round(100 * hechas / v.objetivo)) : 0;
|
|
||||||
const c = el('div', { class: 'etapa', style: 'text-align:left' });
|
|
||||||
c.append(el('div', { style: 'display:flex;justify-content:space-between;align-items:center' }, el('b', { text: 'EinarBoard ' + v.version }), el('span', { class: BADGE[v.version] || 'pill pend', text: v.version })));
|
|
||||||
c.append(el('div', { class: 'n', style: 'font-size:22px;margin-top:4px', html: hechas + ' <span style="font-size:13px;color:var(--tinta2)">/ ' + v.objetivo + ' unidades</span>' }));
|
|
||||||
const pr = el('div', { class: 'prog' }); pr.append(el('span', { style: 'width:' + pct + '%' })); c.append(pr);
|
|
||||||
c.append(el('div', { class: 'obj', html: '<span>' + pct + ' %</span><span>' + (v.fecha ? 'objetivo ' + esc(v.fecha) : 'sin fecha objetivo') + '</span>' }));
|
|
||||||
if (v.nota) c.append(el('div', { class: 'l', style: 'text-align:left;margin-top:5px', text: v.nota }));
|
|
||||||
if (pref) c.append(el('div', { class: 'f', style: 'text-align:left', text: 'cuenta ensambles OK cuya tarjeta sea ' + pref + '-nn' }));
|
|
||||||
grid.append(c);
|
|
||||||
});
|
|
||||||
// lote V5.5 actual (fuera del pedido)
|
|
||||||
const v55 = ens.filter(a => /^V5-\d/i.test(String((a.componentes || {}).einar || ''))).length;
|
|
||||||
const c55 = el('div', { class: 'etapa', style: 'text-align:left;opacity:.9' });
|
|
||||||
c55.append(el('div', { style: 'display:flex;justify-content:space-between;align-items:center' }, el('b', { text: 'Lote actual V5.5' }), el('span', { class: 'badge-v55', text: 'V5.5' })));
|
|
||||||
c55.append(el('div', { class: 'n', style: 'font-size:22px;margin-top:4px', html: v55 + ' <span style="font-size:13px;color:var(--tinta2)">unidades ensambladas</span>' }));
|
|
||||||
c55.append(el('div', { class: 'l', style: 'text-align:left;margin-top:5px', text: 'Tarjetas V5-nn (15 fabricadas el 22-ago). No forman parte del pedido de 570; se cuentan aparte.' }));
|
|
||||||
grid.append(c55);
|
|
||||||
b1.append(grid);
|
|
||||||
if (total !== (plan.pedido_total || total)) b1.append(el('p', { class: 'sub', style: 'margin-top:8px;color:var(--falla)', text: '⚠ La suma de objetivos (' + total + ') no coincide con el pedido total (' + plan.pedido_total + ').' }));
|
|
||||||
cont.append(b1);
|
|
||||||
|
|
||||||
// ---- embudo
|
|
||||||
const b2 = el('section', { class: 'bloque' });
|
|
||||||
b2.append(el('h2', { text: 'Embudo de producción (series distintas con acta)' }), el('p', { class: 'sub', text: 'Cada etapa cuenta las series que ya tienen acta de ese tipo. Las tres primeras son por pieza (tarjeta, módulo, pareja); de ensamble en adelante, por unidad HBOX. Toca una etapa para abrir su acta.' }));
|
|
||||||
const emb = el('div', { class: 'embudo' });
|
|
||||||
(plan.etapas || []).forEach(et => {
|
|
||||||
const s = seriesEtapa(et, reales);
|
|
||||||
const a = el('a', { class: 'etapa' + (s.size ? '' : ' sin'), href: FORM[et.tipos[0]] || '../actas/', style: 'text-decoration:none;color:inherit' });
|
|
||||||
a.append(el('div', { class: 'n', text: String(s.size) }), el('div', { class: 'l', text: et.nombre }));
|
|
||||||
a.append(el('div', { class: 'f', text: et.nota || ('acta: ' + et.tipos.join(', ')) }));
|
|
||||||
emb.append(a);
|
|
||||||
});
|
|
||||||
b2.append(emb);
|
|
||||||
cont.append(b2);
|
|
||||||
|
|
||||||
// ---- bloqueos + inventario
|
|
||||||
const dos = el('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px' });
|
|
||||||
const b3 = el('section', { class: 'bloque bloqueos', style: 'margin:0' });
|
|
||||||
b3.append(el('h2', { text: 'Bloqueos (Producción · Mecánica · Compras)' }), el('p', { class: 'sub', text: 'Pendientes en la columna Bloqueado. Quien lo destrabe, que lo mueva.' }));
|
|
||||||
if (!bloqueos.length) b3.append(el('div', { class: 'vacio', text: 'Sin bloqueos registrados.' }));
|
|
||||||
bloqueos.forEach(t => {
|
|
||||||
const c = el('div', { class: 'card p-' + t.pista });
|
|
||||||
c.append(el('div', { class: 'tit', html: '<a href="kanban.html?id=' + encodeURIComponent(t.id) + '" style="color:inherit;text-decoration:none">' + esc(t.titulo) + '</a>' }));
|
|
||||||
c.append(el('div', { class: 'meta', html: '<span class="chip p-' + t.pista + '">' + esc(PISTAS[t.pista] || t.pista) + '</span>' + (t.responsable ? '<span title="' + esc(t.responsable) + '">👤 ' + esc(Kanban.nombresResp(t, EQUIPO).join(', ')) + '</span>' : '<span>sin responsable</span>') + (t.fecha_limite ? '<span class="fecha' + (t.vencida ? ' mal' : '') + '">📅 ' + esc(t.fecha_limite) + '</span>' : '') }));
|
|
||||||
if (t.descripcion) c.append(el('div', { style: 'font-size:13px;color:var(--tinta2);margin-top:4px', text: t.descripcion.length > 160 ? t.descripcion.slice(0, 159) + '…' : t.descripcion }));
|
|
||||||
b3.append(c);
|
|
||||||
});
|
|
||||||
dos.append(b3);
|
|
||||||
const b4 = el('section', { class: 'bloque', style: 'margin:0' });
|
|
||||||
b4.append(el('h2', { text: 'Alertas de inventario' }), el('p', { class: 'sub', html: 'Ítems con stock por debajo del mínimo. <a href="../inventario/inventario.html">Abrir inventario →</a>' }));
|
|
||||||
if (!inv.length) b4.append(el('div', { class: 'vacio', text: 'Sin alertas: todo por encima del mínimo (o sin mínimo definido).' }));
|
|
||||||
else {
|
|
||||||
const t = el('table', { class: 't' }); t.innerHTML = '<tr><th>Código</th><th>Descripción</th><th>Stock</th><th>Mínimo</th></tr>';
|
|
||||||
inv.forEach(i => { const tr = el('tr'); tr.innerHTML = '<td><b>' + esc(i.codigo) + '</b></td><td>' + esc(i.descripcion) + '</td><td style="color:var(--falla);font-weight:700">' + esc(i.stock) + ' ' + esc(i.unidad) + '</td><td>' + esc(i.minimo) + '</td>'; t.append(tr); });
|
|
||||||
b4.append(el('div', { class: 'tblwrap' }, t));
|
|
||||||
}
|
|
||||||
dos.append(b4);
|
|
||||||
cont.append(dos);
|
|
||||||
|
|
||||||
// ---- daños
|
|
||||||
if (danos.length) {
|
|
||||||
const b5 = el('section', { class: 'bloque' });
|
|
||||||
b5.append(el('h2', { text: 'Series con marca de daño o baja' }), el('p', { class: 'sub', text: 'Lo último registrado en un acta de daño manda. «Reparada» levanta la marca.' }));
|
|
||||||
const t = el('table', { class: 't' }); t.innerHTML = '<tr><th>Serie</th><th>Marca</th><th>Qué pasó</th><th>Disposición</th><th>Fecha</th></tr>';
|
|
||||||
danos.forEach(d => { const a = d.acta || {}; const tr = el('tr'); tr.innerHTML = '<td><a href="../unidad.html?serie=' + encodeURIComponent(d.serie) + '"><b>' + esc(d.serie) + '</b></a></td><td><span class="pill ' + (d.marca === 'BAJA' ? 'falla' : 'aviso') + '">' + esc(d.marca === 'BAJA' ? 'BAJA' : 'DAÑADA') + '</span></td><td>' + esc(a.clasificacion || '') + (a.resumen ? ' — ' + esc(a.resumen) : '') + '</td><td>' + esc(a.accion || '') + '</td><td>' + esc(a.fecha || fmt(a.recibido)) + '</td>'; t.append(tr); });
|
|
||||||
b5.append(el('div', { class: 'tblwrap' }, t));
|
|
||||||
cont.append(b5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- edición del plan (solo admin)
|
|
||||||
function editarPlan() {
|
|
||||||
const dlg = document.getElementById('dlgPlan');
|
|
||||||
dlg.innerHTML = '';
|
|
||||||
const cab = el('div', { class: 'dcab' }); cab.append(el('h2', { text: 'Editar plan de producción' }));
|
|
||||||
const x = el('button', { type: 'button', text: '✕' }); x.addEventListener('click', () => dlg.close()); cab.append(x); dlg.append(cab);
|
|
||||||
const cuerpo = el('div', { class: 'dcuerpo' }); dlg.append(cuerpo);
|
|
||||||
const fTot = el('input', { type: 'number', min: '0', value: PLAN.pedido_total || 0 });
|
|
||||||
const fNota = el('input', { type: 'text', value: PLAN.nota || '', maxlength: '500' });
|
|
||||||
const d1 = el('div', { class: 'dos-col' }); d1.append(campo('Pedido total (unidades)', fTot), campo('Nota', fNota)); cuerpo.append(d1);
|
|
||||||
cuerpo.append(el('label', { text: 'Versiones: objetivo, nota, prefijo de serie de la tarjeta (para contar ensambles) y fecha objetivo' }));
|
|
||||||
const filas = el('div');
|
|
||||||
function fila(v) {
|
|
||||||
const f = el('div', { style: 'display:grid;grid-template-columns:70px 80px 1fr 70px 140px auto;gap:6px;margin-bottom:6px;align-items:center' });
|
|
||||||
f.append(el('input', { type: 'text', value: v.version || '', placeholder: 'V5.6', maxlength: '10' }),
|
|
||||||
el('input', { type: 'number', min: '0', value: v.objetivo || 0 }),
|
|
||||||
el('input', { type: 'text', value: v.nota || '', placeholder: 'nota' }),
|
|
||||||
el('input', { type: 'text', value: v.serie_prefijo || '', placeholder: 'V56', maxlength: '10' }),
|
|
||||||
el('input', { type: 'date', value: v.fecha || '' }));
|
|
||||||
const q = el('button', { type: 'button', class: 'sec', text: '✕', style: 'padding:6px 9px' }); q.addEventListener('click', () => f.remove()); f.append(q);
|
|
||||||
filas.append(f);
|
|
||||||
}
|
|
||||||
(PLAN.versiones || []).forEach(fila); cuerpo.append(filas);
|
|
||||||
const add = el('button', { type: 'button', class: 'sec', text: '+ versión', style: 'padding:6px 10px;font-size:13px' }); add.addEventListener('click', () => fila({})); cuerpo.append(add);
|
|
||||||
cuerpo.append(el('p', { class: 'sub', style: 'margin-top:10px', text: 'El embudo (etapas) se conserva como está; se edita por API si hace falta.' }));
|
|
||||||
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: 'Guardar plan' });
|
|
||||||
guardar.addEventListener('click', async () => {
|
|
||||||
const versiones = [...filas.children].map(f => ({ version: f.children[0].value.trim(), objetivo: +f.children[1].value || 0, nota: f.children[2].value.trim(), serie_prefijo: f.children[3].value.trim(), fecha: f.children[4].value || null })).filter(v => v.version);
|
|
||||||
if (!versiones.length) { m.className = 'msg err'; m.textContent = 'Deja al menos una versión.'; return; }
|
|
||||||
guardar.disabled = true;
|
|
||||||
try {
|
|
||||||
await api('/api/plan', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pedido_total: +fTot.value || 0, nota: fNota.value.trim(), versiones }) });
|
|
||||||
dlg.close(); cargar();
|
|
||||||
} 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', '');
|
|
||||||
}
|
|
||||||
function campo(l, i) { const d = el('div'); d.append(el('label', { text: l }), i); return d; }
|
|
||||||
|
|
||||||
async function cargar() {
|
|
||||||
meta.textContent = 'Actualizando…';
|
|
||||||
try {
|
|
||||||
const [p, a, s, d, t, i] = await Promise.all([
|
|
||||||
api('/api/plan'), api('/api/actas'), api('/api/series').catch(() => ({ series: [] })), api('/api/danos').catch(() => ({ series: [] })),
|
|
||||||
api('/api/tareas?estado=bloqueado').catch(() => ({ tareas: [] })), api('/api/inventario?alertas=1').catch(() => ({ items: [] }))]);
|
|
||||||
PLAN = p.plan; ADMIN = !!p.admin; EQUIPO = Kanban.usarEquipo(t.equipo);
|
|
||||||
const bloqueos = (t.tareas || []).filter(x => ['produccion', 'mecanica', 'compras'].includes(x.pista));
|
|
||||||
render(PLAN, a.actas || [], s.series || [], d.series || [], bloqueos, i.items || []);
|
|
||||||
meta.textContent = 'Actualizado ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }) + ' · ' + (a.total || 0) + ' actas · ' + (s.series || []).length + ' series con registro';
|
|
||||||
} catch (e) { cont.innerHTML = ''; cont.append(el('div', { class: 'err', text: 'No se pudo cargar el tablero: ' + e.message })); meta.textContent = 'Error'; }
|
|
||||||
}
|
|
||||||
document.getElementById('refrescar').addEventListener('click', cargar);
|
|
||||||
cargar();
|
|
||||||
setInterval(cargar, 60000);
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
232
docs/tableros/produccion.md
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
---
|
||||||
|
title: "Producción"
|
||||||
|
description: "El pedido de 570 HBOX: plan contra lo real, embudo por etapa, bloqueos e inventario."
|
||||||
|
template: tablero.html
|
||||||
|
hide:
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="tb tb-produccion">
|
||||||
|
<header class="tb-cab">
|
||||||
|
<div class="tb-cab__t"><h1>Producción</h1><span class="meta" id="meta">Cargando…</span></div>
|
||||||
|
<div class="tb-cab__acc"><button type="button" class="sec" id="refrescar"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M19.5 12a7.5 7.5 0 1 1-2.2-5.3M19.5 4.5v4h-4"/></svg>Actualizar</button></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<details class="info tb-como">
|
||||||
|
<summary>Cómo se usa</summary>
|
||||||
|
<ol>
|
||||||
|
<li>Arriba: el <b>plan</b> (cuántas unidades de cada versión y para cuándo) contra lo <b>real</b>, que sale solo de las actas enviadas. Si una unidad no tiene acta, aquí no existe.</li>
|
||||||
|
<li>El <b>embudo</b> cuenta series distintas por etapa: tarjeta probada → módulos → integración → ensamble → prueba final → configuración → envío → instalación. Toca una etapa para ir a su acta.</li>
|
||||||
|
<li><b>Bloqueos</b>: pendientes en Bloqueado de las pistas Producción, Mecánica y Compras. Resuélvelos en el <a href="/tableros/pendientes/">tablero de pendientes</a>.</li>
|
||||||
|
<li><b>Alertas de inventario</b>: ítems por debajo del mínimo. Registra las entradas al llegar el material.</li>
|
||||||
|
<li>Solo el administrador edita el plan (botón «Editar plan»). Las fechas son objetivo, no promesa.</li>
|
||||||
|
</ol>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div id="cont"></div>
|
||||||
|
|
||||||
|
<dialog id="dlgPlan"></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 esPrueba = s => String(s || '').toUpperCase().includes('PRUEBA');
|
||||||
|
const esUnidad = s => /^HBOX/i.test(String(s || ''));
|
||||||
|
const BADGE = { 'V5.5': 'badge-v55', 'V5.6': 'badge-v56', 'V5.7': 'badge-v57', 'v6': 'badge-v6' };
|
||||||
|
const FORM = { 'prueba-v5': '/formularios/v5.html', 'prueba-v4': '/formularios/v4.html', 'modulo-ard': '/formularios/ardusimple.html', 'modulo-4g': '/formularios/modem4g.html',
|
||||||
|
'integracion': '/formularios/integracion.html', 'ensamble': '/formularios/ensamble.html', 'reflasheo': '/formularios/reflasheo.html',
|
||||||
|
'prueba-ethernet-serie': '/formularios/ethernet-serie.html', 'envio': '/formularios/envio.html', 'instalacion-campo': '/formularios/instalacion.html' };
|
||||||
|
const PISTAS = { produccion: 'Producción', mecanica: 'Mecánica', compras: 'Compras', ingenieria: 'Ingeniería', campo: 'Campo' };
|
||||||
|
const cont = document.getElementById('cont'), meta = document.getElementById('meta');
|
||||||
|
let PLAN = null, ADMIN = false, EQUIPO = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function seriesEtapa(et, actas) {
|
||||||
|
const s = new Set();
|
||||||
|
actas.forEach(a => {
|
||||||
|
if (!et.tipos.includes(a.tipo) || esPrueba(a.serie)) return;
|
||||||
|
if (et.ok && a.resultado === 'FALLA') return;
|
||||||
|
if (et.solo_unidades && !esUnidad(a.serie)) return;
|
||||||
|
s.add(a.serie);
|
||||||
|
});
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(plan, actas, series, danos, bloqueos, inv) {
|
||||||
|
cont.innerHTML = '';
|
||||||
|
const reales = actas.filter(a => !esPrueba(a.serie));
|
||||||
|
const ens = reales.filter(a => a.tipo === 'ensamble' && a.resultado !== 'FALLA');
|
||||||
|
const unidades = new Set(ens.map(a => a.serie));
|
||||||
|
const enviadas = new Set(reales.filter(a => a.tipo === 'envio' && esUnidad(a.serie)).map(a => a.serie));
|
||||||
|
const instaladas = new Set(reales.filter(a => a.tipo === 'instalacion-campo' && a.resultado !== 'FALLA').map(a => a.serie));
|
||||||
|
const total = plan.versiones.reduce((n, v) => n + (v.objetivo || 0), 0);
|
||||||
|
|
||||||
|
// ---- KPIs
|
||||||
|
const kp = el('div', { class: 'kpis' });
|
||||||
|
const kpi = (n, l, cls) => kp.append(el('div', { class: 'kpi ' + (cls || '') }, el('div', { class: 'n', text: String(n) }), el('div', { class: 'l', text: l })));
|
||||||
|
kpi(plan.pedido_total || total, 'Pedido total');
|
||||||
|
kpi(unidades.size, 'Unidades HBOX ensambladas (acta OK)', 'bien');
|
||||||
|
kpi(enviadas.size, 'Unidades enviadas');
|
||||||
|
kpi(instaladas.size, 'Instaladas en sitio');
|
||||||
|
kpi(danos.length, 'Series dañadas o de baja', danos.length ? 'mal' : '');
|
||||||
|
kpi(bloqueos.length, 'Bloqueos abiertos', bloqueos.length ? 'mal' : '');
|
||||||
|
kpi(inv.length, 'Alertas de inventario', inv.length ? 'mal' : '');
|
||||||
|
cont.append(kp);
|
||||||
|
|
||||||
|
// ---- plan vs real por versión
|
||||||
|
const b1 = el('section', { class: 'bloque' });
|
||||||
|
const cab = el('div', { style: 'display:flex;justify-content:space-between;gap:10px;align-items:baseline;flex-wrap:wrap' });
|
||||||
|
cab.append(el('h2', { text: 'Plan por versión vs. real' }));
|
||||||
|
if (ADMIN) { const b = el('button', { class: 'sec', text: 'Editar plan', style: 'padding:6px 12px' }); b.addEventListener('click', editarPlan); cab.append(b); }
|
||||||
|
b1.append(cab);
|
||||||
|
b1.append(el('p', { class: 'sub', html: esc(plan.nota || '') + (plan.actualizado_en ? ' · plan actualizado por <b>' + esc(plan.actualizado_por) + '</b> el ' + esc(fmt(plan.actualizado_en)) : (plan._default ? ' · <b>plan por omisión</b>: aún no se ha guardado (siembra o «Editar plan»)' : '')) }));
|
||||||
|
const grid = el('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px' });
|
||||||
|
plan.versiones.forEach(v => {
|
||||||
|
const pref = (v.serie_prefijo || '').toUpperCase();
|
||||||
|
const hechas = pref ? ens.filter(a => String((a.componentes || {}).einar || '').toUpperCase().startsWith(pref + '-')).length : 0;
|
||||||
|
const pct = v.objetivo ? Math.min(100, Math.round(100 * hechas / v.objetivo)) : 0;
|
||||||
|
const c = el('div', { class: 'etapa', style: 'text-align:left' });
|
||||||
|
c.append(el('div', { style: 'display:flex;justify-content:space-between;align-items:center' }, el('b', { text: 'EinarBoard ' + v.version }), el('span', { class: BADGE[v.version] || 'pill pend', text: v.version })));
|
||||||
|
c.append(el('div', { class: 'n', style: 'font-size:22px;margin-top:4px', html: hechas + ' <span style="font-size:13px;color:var(--tinta2)">/ ' + v.objetivo + ' unidades</span>' }));
|
||||||
|
const pr = el('div', { class: 'prog' }); pr.append(el('span', { style: 'width:' + pct + '%' })); c.append(pr);
|
||||||
|
c.append(el('div', { class: 'obj', html: '<span>' + pct + ' %</span><span>' + (v.fecha ? 'objetivo ' + esc(v.fecha) : 'sin fecha objetivo') + '</span>' }));
|
||||||
|
if (v.nota) c.append(el('div', { class: 'l', style: 'text-align:left;margin-top:5px', text: v.nota }));
|
||||||
|
if (pref) c.append(el('div', { class: 'f', style: 'text-align:left', text: 'cuenta ensambles OK cuya tarjeta sea ' + pref + '-nn' }));
|
||||||
|
grid.append(c);
|
||||||
|
});
|
||||||
|
// lote V5.5 actual (fuera del pedido)
|
||||||
|
const v55 = ens.filter(a => /^V5-\d/i.test(String((a.componentes || {}).einar || ''))).length;
|
||||||
|
const c55 = el('div', { class: 'etapa', style: 'text-align:left;opacity:.9' });
|
||||||
|
c55.append(el('div', { style: 'display:flex;justify-content:space-between;align-items:center' }, el('b', { text: 'Lote actual V5.5' }), el('span', { class: 'badge-v55', text: 'V5.5' })));
|
||||||
|
c55.append(el('div', { class: 'n', style: 'font-size:22px;margin-top:4px', html: v55 + ' <span style="font-size:13px;color:var(--tinta2)">unidades ensambladas</span>' }));
|
||||||
|
c55.append(el('div', { class: 'l', style: 'text-align:left;margin-top:5px', text: 'Tarjetas V5-nn (15 fabricadas el 22-ago). No forman parte del pedido de 570; se cuentan aparte.' }));
|
||||||
|
grid.append(c55);
|
||||||
|
b1.append(grid);
|
||||||
|
if (total !== (plan.pedido_total || total)) b1.append(el('p', { class: 'sub', style: 'margin-top:8px;color:var(--falla)', text: 'Aviso: la suma de objetivos (' + total + ') no coincide con el pedido total (' + plan.pedido_total + ').' }));
|
||||||
|
cont.append(b1);
|
||||||
|
|
||||||
|
// ---- embudo
|
||||||
|
const b2 = el('section', { class: 'bloque' });
|
||||||
|
b2.append(el('h2', { text: 'Embudo de producción (series distintas con acta)' }), el('p', { class: 'sub', text: 'Cada etapa cuenta las series que ya tienen acta de ese tipo. Las tres primeras son por pieza (tarjeta, módulo, pareja); de ensamble en adelante, por unidad HBOX. Toca una etapa para abrir su acta.' }));
|
||||||
|
const emb = el('div', { class: 'embudo' });
|
||||||
|
(plan.etapas || []).forEach(et => {
|
||||||
|
const s = seriesEtapa(et, reales);
|
||||||
|
const a = el('a', { class: 'etapa' + (s.size ? '' : ' sin'), href: FORM[et.tipos[0]] || '/actas/', style: 'text-decoration:none;color:inherit' });
|
||||||
|
a.append(el('div', { class: 'n', text: String(s.size) }), el('div', { class: 'l', text: et.nombre }));
|
||||||
|
a.append(el('div', { class: 'f', text: et.nota || ('acta: ' + et.tipos.join(', ')) }));
|
||||||
|
emb.append(a);
|
||||||
|
});
|
||||||
|
b2.append(emb);
|
||||||
|
cont.append(b2);
|
||||||
|
|
||||||
|
// ---- bloqueos + inventario
|
||||||
|
const dos = el('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px' });
|
||||||
|
const b3 = el('section', { class: 'bloque bloqueos', style: 'margin:0' });
|
||||||
|
b3.append(el('h2', { text: 'Bloqueos (Producción · Mecánica · Compras)' }), el('p', { class: 'sub', text: 'Pendientes en la columna Bloqueado. Quien lo destrabe, que lo mueva.' }));
|
||||||
|
if (!bloqueos.length) b3.append(el('div', { class: 'vacio', text: 'Sin bloqueos registrados.' }));
|
||||||
|
bloqueos.forEach(t => {
|
||||||
|
const c = el('div', { class: 'card p-' + t.pista + (t.vencida ? ' vencida' : '') });
|
||||||
|
const sup = el('div', { class: 'sup' }); sup.append(el('span', { class: 'pista-p' }), el('span', { text: PISTAS[t.pista] || t.pista }));
|
||||||
|
if (BADGE[t.version_tarjeta]) sup.append(el('span', { class: BADGE[t.version_tarjeta] + ' ver-t', text: t.version_tarjeta }));
|
||||||
|
c.append(sup);
|
||||||
|
c.append(el('div', { class: 'tit', html: '<a href="/tableros/pendientes/?id=' + encodeURIComponent(t.id) + '">' + esc(t.titulo) + '</a>' }));
|
||||||
|
if (t.descripcion) c.append(el('div', { style: 'font-size:13px;color:var(--tinta2)', text: t.descripcion.length > 160 ? t.descripcion.slice(0, 159) + '…' : t.descripcion }));
|
||||||
|
const pie = el('div', { class: 'pie' });
|
||||||
|
pie.append(t.responsable ? Kanban.avatares(t) : el('span', { class: 'vacio', text: 'Sin responsable' }));
|
||||||
|
const fd = Kanban.fechaDe(t); if (fd) pie.append(el('span', { class: 'fecha ' + fd.cls, text: fd.texto }));
|
||||||
|
c.append(pie);
|
||||||
|
b3.append(c);
|
||||||
|
});
|
||||||
|
dos.append(b3);
|
||||||
|
const b4 = el('section', { class: 'bloque', style: 'margin:0' });
|
||||||
|
b4.append(el('h2', { text: 'Alertas de inventario' }), el('p', { class: 'sub', html: 'Ítems con stock por debajo del mínimo. <a href="/tableros/inventario/">Abrir inventario →</a>' }));
|
||||||
|
if (!inv.length) b4.append(el('div', { class: 'vacio', text: 'Sin alertas: todo por encima del mínimo (o sin mínimo definido).' }));
|
||||||
|
else {
|
||||||
|
const t = el('table', { class: 't' }); t.innerHTML = '<tr><th>Código</th><th>Descripción</th><th>Stock</th><th>Mínimo</th></tr>';
|
||||||
|
inv.forEach(i => { const tr = el('tr'); tr.innerHTML = '<td><b>' + esc(i.codigo) + '</b></td><td>' + esc(i.descripcion) + '</td><td style="color:var(--falla);font-weight:700">' + esc(i.stock) + ' ' + esc(i.unidad) + '</td><td>' + esc(i.minimo) + '</td>'; t.append(tr); });
|
||||||
|
b4.append(el('div', { class: 'tblwrap' }, t));
|
||||||
|
}
|
||||||
|
dos.append(b4);
|
||||||
|
cont.append(dos);
|
||||||
|
|
||||||
|
// ---- daños
|
||||||
|
if (danos.length) {
|
||||||
|
const b5 = el('section', { class: 'bloque' });
|
||||||
|
b5.append(el('h2', { text: 'Series con marca de daño o baja' }), el('p', { class: 'sub', text: 'Lo último registrado en un acta de daño manda. «Reparada» levanta la marca.' }));
|
||||||
|
const t = el('table', { class: 't' }); t.innerHTML = '<tr><th>Serie</th><th>Marca</th><th>Qué pasó</th><th>Disposición</th><th>Fecha</th></tr>';
|
||||||
|
danos.forEach(d => { const a = d.acta || {}; const tr = el('tr'); tr.innerHTML = '<td><a href="/tableros/unidad/?serie=' + encodeURIComponent(d.serie) + '"><b>' + esc(d.serie) + '</b></a></td><td><span class="pill ' + (d.marca === 'BAJA' ? 'falla' : 'aviso') + '">' + esc(d.marca === 'BAJA' ? 'BAJA' : 'DAÑADA') + '</span></td><td>' + esc(a.clasificacion || '') + (a.resumen ? ' — ' + esc(a.resumen) : '') + '</td><td>' + esc(a.accion || '') + '</td><td>' + esc(a.fecha || fmt(a.recibido)) + '</td>'; t.append(tr); });
|
||||||
|
b5.append(el('div', { class: 'tblwrap' }, t));
|
||||||
|
cont.append(b5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- edición del plan (solo admin)
|
||||||
|
function editarPlan() {
|
||||||
|
const dlg = document.getElementById('dlgPlan');
|
||||||
|
dlg.innerHTML = '';
|
||||||
|
const cab = el('div', { class: 'dcab' }); cab.append(el('h2', { text: 'Editar plan de producción' }));
|
||||||
|
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 fTot = el('input', { type: 'number', min: '0', value: PLAN.pedido_total || 0 });
|
||||||
|
const fNota = el('input', { type: 'text', value: PLAN.nota || '', maxlength: '500' });
|
||||||
|
const d1 = el('div', { class: 'dos-col' }); d1.append(campo('Pedido total (unidades)', fTot), campo('Nota', fNota)); cuerpo.append(d1);
|
||||||
|
cuerpo.append(el('label', { text: 'Versiones: objetivo, nota, prefijo de serie de la tarjeta (para contar ensambles) y fecha objetivo' }));
|
||||||
|
const filas = el('div');
|
||||||
|
function fila(v) {
|
||||||
|
const f = el('div', { style: 'display:grid;grid-template-columns:70px 80px 1fr 70px 140px auto;gap:6px;margin-bottom:6px;align-items:center' });
|
||||||
|
f.append(el('input', { type: 'text', value: v.version || '', placeholder: 'V5.6', maxlength: '10' }),
|
||||||
|
el('input', { type: 'number', min: '0', value: v.objetivo || 0 }),
|
||||||
|
el('input', { type: 'text', value: v.nota || '', placeholder: 'nota' }),
|
||||||
|
el('input', { type: 'text', value: v.serie_prefijo || '', placeholder: 'V56', maxlength: '10' }),
|
||||||
|
el('input', { type: 'date', value: v.fecha || '' }));
|
||||||
|
const q = el('button', { type: 'button', class: 'sec', 'aria-label': 'Quitar la versión', html: Kanban.icono('cerrar'), style: 'padding:6px 9px' }); q.addEventListener('click', () => f.remove()); f.append(q);
|
||||||
|
filas.append(f);
|
||||||
|
}
|
||||||
|
(PLAN.versiones || []).forEach(fila); cuerpo.append(filas);
|
||||||
|
const add = el('button', { type: 'button', class: 'sec', html: Kanban.icono('mas') + 'Versión', style: 'padding:6px 10px;font-size:13px' }); add.addEventListener('click', () => fila({})); cuerpo.append(add);
|
||||||
|
cuerpo.append(el('p', { class: 'sub', style: 'margin-top:10px', text: 'El embudo (etapas) se conserva como está; se edita por API si hace falta.' }));
|
||||||
|
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: 'Guardar plan' });
|
||||||
|
guardar.addEventListener('click', async () => {
|
||||||
|
const versiones = [...filas.children].map(f => ({ version: f.children[0].value.trim(), objetivo: +f.children[1].value || 0, nota: f.children[2].value.trim(), serie_prefijo: f.children[3].value.trim(), fecha: f.children[4].value || null })).filter(v => v.version);
|
||||||
|
if (!versiones.length) { m.className = 'msg err'; m.textContent = 'Deja al menos una versión.'; return; }
|
||||||
|
guardar.disabled = true;
|
||||||
|
try {
|
||||||
|
await api('/api/plan', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pedido_total: +fTot.value || 0, nota: fNota.value.trim(), versiones }) });
|
||||||
|
dlg.close(); cargar();
|
||||||
|
} 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', '');
|
||||||
|
}
|
||||||
|
function campo(l, i) { const d = el('div'); d.append(el('label', { text: l }), i); return d; }
|
||||||
|
|
||||||
|
async function cargar() {
|
||||||
|
meta.textContent = 'Actualizando…';
|
||||||
|
try {
|
||||||
|
const [p, a, s, d, t, i] = await Promise.all([
|
||||||
|
api('/api/plan'), api('/api/actas'), api('/api/series').catch(() => ({ series: [] })), api('/api/danos').catch(() => ({ series: [] })),
|
||||||
|
api('/api/tareas?estado=bloqueado').catch(() => ({ tareas: [] })), api('/api/inventario?alertas=1').catch(() => ({ items: [] }))]);
|
||||||
|
PLAN = p.plan; ADMIN = !!p.admin; EQUIPO = Kanban.usarEquipo(t.equipo);
|
||||||
|
const bloqueos = (t.tareas || []).filter(x => ['produccion', 'mecanica', 'compras'].includes(x.pista));
|
||||||
|
render(PLAN, a.actas || [], s.series || [], d.series || [], bloqueos, i.items || []);
|
||||||
|
meta.textContent = 'Actualizado ' + new Date().toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' }) + ' · ' + (a.total || 0) + ' actas · ' + (s.series || []).length + ' series con registro';
|
||||||
|
} catch (e) { cont.innerHTML = ''; cont.append(el('div', { class: 'err', text: 'No se pudo cargar el tablero: ' + e.message })); meta.textContent = 'Error'; }
|
||||||
|
}
|
||||||
|
document.getElementById('refrescar').addEventListener('click', cargar);
|
||||||
|
cargar();
|
||||||
|
setInterval(cargar, 60000);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
178
docs/tableros/sitio.md
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
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', '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);
|
||||||
|
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>
|
||||||
@ -1,148 +1,530 @@
|
|||||||
/* Tableros HBOX (kanban, ingeniería, producción) — misma paleta que tablero.html y acta.css */
|
/* ==========================================================================
|
||||||
:root{
|
Tableros dentro del sitio (plantilla overrides/tablero.html)
|
||||||
--naranja:#e8590c; --naranja-suave:#fff1e8; --tinta:#1d1c19; --tinta2:#5a564d; --linea:#e6e1d6;
|
Pendientes · Ingeniería · Producción · Actas · Inventario · Sitios · Equipo ·
|
||||||
--papel:#faf8f4; --tarjeta:#fff; --superf:#fcfcfb;
|
Expediente de unidad · Visitas de campo.
|
||||||
--ok:#2f9e44; --ok-bg:#ebfbee; --falla:#e03131; --falla-bg:#fff5f5;
|
Todo vive bajo .tb (el contenedor de cada página) para no chocar con los
|
||||||
--pend:#adb5bd; --pend-bg:#f4f2ee; --info:#1971c2; --info-bg:#e7f1fb;
|
estilos de Material. Colores: tokens --hg-* de stylesheets/helgen.css.
|
||||||
--aviso:#f08c00; --aviso-bg:#fff9db; --morado:#7048e8; --morado-bg:#f3f0ff;
|
========================================================================== */
|
||||||
--cabecera:#2C3C5B; --cabecera-texto:#fff; --cabecera-meta:#c9d0e0; /* índigo Helgen, igual que la barra del sitio; fijo en claro y oscuro */
|
|
||||||
|
.tb {
|
||||||
|
/* nombres viejos que todavía usan los scripts de las páginas en estilos en línea */
|
||||||
|
--tinta: var(--hg-texto);
|
||||||
|
--tinta2: var(--hg-texto-2);
|
||||||
|
--linea: var(--hg-linea);
|
||||||
|
--tarjeta: var(--hg-plano);
|
||||||
|
--superf: var(--hg-plano-2);
|
||||||
|
--papel: var(--hg-fondo);
|
||||||
|
--naranja: var(--hg-liga);
|
||||||
|
--ok: var(--hg-hecho); --ok-bg: var(--hg-hecho-bg);
|
||||||
|
--apro: var(--hg-hecho); --apro-bg: var(--hg-hecho-bg);
|
||||||
|
--falla: var(--hg-bloqueado); --falla-bg: var(--hg-bloqueado-bg);
|
||||||
|
--info: var(--hg-info); --info-bg: var(--hg-info-bg);
|
||||||
|
--ens: var(--hg-info); --ens-bg: var(--hg-info-bg);
|
||||||
|
--pend: var(--hg-pend); --pend-bg: var(--hg-pend-bg);
|
||||||
|
--aviso: var(--hg-aviso); --aviso-bg: var(--hg-aviso-bg);
|
||||||
|
|
||||||
|
font-family: var(--hg-fuente);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--hg-texto);
|
||||||
}
|
}
|
||||||
@media(prefers-color-scheme:dark){:root:not([data-theme=light]){
|
.md-typeset .tb { font-size: 15px; line-height: 1.45; }
|
||||||
--papel:#191b1e;--tinta:#e6e1d6;--tinta2:#a09a8d;--linea:#33363b;--tarjeta:#1f2226;--superf:#1b1d20;
|
.tb *, .tb *::before, .tb *::after { box-sizing: border-box; }
|
||||||
--naranja-suave:#2e2218;--ok:#69db7c;--ok-bg:#1b2a1f;--falla:#ff8787;--falla-bg:#2c1d1d;--pend:#5c636a;--pend-bg:#26282c;
|
|
||||||
--info:#74c0fc;--info-bg:#182734;--aviso:#ffc078;--aviso-bg:#2e2712;--morado:#b197fc;--morado-bg:#241d33;
|
|
||||||
--cabecera:#1F2A44;--cabecera-texto:#fff;--cabecera-meta:#b8c0d4;}}
|
|
||||||
*{box-sizing:border-box}
|
|
||||||
body{margin:0;background:var(--papel);color:var(--tinta);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-text-size-adjust:100%}
|
|
||||||
.wrap{max-width:1240px;margin:0 auto;padding:16px 16px 70px}
|
|
||||||
header.top{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;
|
|
||||||
background:var(--cabecera);color:var(--cabecera-texto);padding:14px 16px;border-radius:12px;margin-bottom:14px}
|
|
||||||
header.top h1{margin:0;font-size:20px}
|
|
||||||
header.top .meta{color:var(--cabecera-meta);font-size:13px}
|
|
||||||
header.top nav{display:flex;flex-wrap:wrap;gap:4px 14px;align-items:center}
|
|
||||||
header.top nav a{color:var(--cabecera-texto);font-size:13px;text-decoration:none;opacity:.85}
|
|
||||||
header.top nav a.activo{opacity:1;font-weight:700;border-bottom:2px solid var(--naranja)}
|
|
||||||
button{background:var(--naranja);color:#fff;border:0;border-radius:9px;padding:9px 16px;font-size:14px;font-weight:600;cursor:pointer}
|
|
||||||
button:disabled{opacity:.5;cursor:not-allowed}
|
|
||||||
button.sec{background:var(--tarjeta);color:var(--tinta2);border:1px solid var(--linea)}
|
|
||||||
button.sec:hover{border-color:var(--naranja);color:var(--naranja)}
|
|
||||||
button.peligro{background:var(--tarjeta);color:var(--falla);border:1px solid var(--falla)}
|
|
||||||
select,input,textarea{padding:9px 11px;border:1px solid var(--linea);border-radius:9px;font-size:15px;background:var(--tarjeta);color:var(--tinta);max-width:100%}
|
|
||||||
input:focus,select:focus,textarea:focus{outline:2px solid var(--naranja);border-color:var(--naranja)}
|
|
||||||
label{display:block;font-weight:600;font-size:13px;margin:10px 0 4px;color:var(--tinta2)}
|
|
||||||
section.bloque{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:16px;margin:14px 0}
|
|
||||||
section.bloque h2{margin:0 0 4px;font-size:17px}
|
|
||||||
section.bloque .sub{color:var(--tinta2);font-size:13.5px;margin:0 0 10px}
|
|
||||||
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:14px}
|
|
||||||
.kpi{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:12px 14px}
|
|
||||||
.kpi .n{font-size:28px;font-weight:700;line-height:1}
|
|
||||||
.kpi .l{color:var(--tinta2);font-size:12.5px;margin-top:4px}
|
|
||||||
.kpi.mal .n{color:var(--falla)} .kpi.bien .n{color:var(--ok)}
|
|
||||||
.err{background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla);padding:10px 14px;border-radius:10px;margin:10px 0}
|
|
||||||
.vacio{color:var(--tinta2);font-style:italic;font-size:14px}
|
|
||||||
.pill{display:inline-block;font-size:11.5px;font-weight:700;padding:2px 9px;border-radius:99px;white-space:nowrap;vertical-align:middle}
|
|
||||||
.pill.ok{background:var(--ok-bg);color:var(--ok)} .pill.falla{background:var(--falla-bg);color:var(--falla)}
|
|
||||||
.pill.info{background:var(--info-bg);color:var(--info)} .pill.pend{background:var(--pend-bg);color:var(--tinta2)}
|
|
||||||
.pill.aviso{background:var(--aviso-bg);color:#7a4a00}
|
|
||||||
table.t{border-collapse:collapse;width:100%;font-size:13.5px}
|
|
||||||
table.t th{text-align:left;color:var(--tinta2);font-size:11.5px;text-transform:uppercase;letter-spacing:.05em;padding:6px 8px 6px 0;border-bottom:1px solid var(--tinta2)}
|
|
||||||
table.t td{padding:7px 8px 7px 0;border-bottom:1px solid var(--linea);vertical-align:top}
|
|
||||||
.tblwrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
||||||
a{color:var(--naranja)}
|
|
||||||
|
|
||||||
/* ---- "Cómo se usa": bloque plegable al inicio de cada página ---- */
|
/* ---- Encabezado de la página -------------------------------------------- */
|
||||||
details.como{background:var(--info-bg);border:1px solid #c9dcec;border-radius:10px;padding:8px 14px;margin:0 0 14px;font-size:14px}
|
.tb-cab {
|
||||||
details.como summary{cursor:pointer;font-weight:700;color:var(--info)}
|
display: flex;
|
||||||
details.como ol{margin:8px 0 4px;padding-left:20px} details.como li{margin:3px 0}
|
flex-wrap: wrap;
|
||||||
@media(prefers-color-scheme:dark){:root:not([data-theme=light]) details.como{border-color:#2d4760}}
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px 16px;
|
||||||
|
margin: 2px 0 16px;
|
||||||
|
}
|
||||||
|
.tb-cab__t { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px; min-width: 0; }
|
||||||
|
.md-typeset .tb .tb-cab h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--hg-fuente-titulo);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 34px;
|
||||||
|
line-height: 1.05;
|
||||||
|
letter-spacing: -0.012em;
|
||||||
|
color: var(--hg-texto-fuerte);
|
||||||
|
}
|
||||||
|
.tb .tb-cab .meta, .tb-cab__meta { font-size: 14px; color: var(--hg-texto-2); }
|
||||||
|
.tb-cab__acc { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||||
|
|
||||||
/* ---- Kanban ---- */
|
/* «Cómo se usa»: el bloque plegable de Material, más compacto */
|
||||||
.kb-barra{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 12px}
|
.md-typeset .tb details.tb-como { margin: 0 0 16px; font-size: 14px; }
|
||||||
.kb-barra select,.kb-barra input{padding:8px 10px;font-size:14px}
|
.md-typeset .tb details.tb-como ol { margin: 6px 0 10px 1.2em; }
|
||||||
.kb-barra .cnt{color:var(--tinta2);font-size:13px;margin-left:auto}
|
.md-typeset .tb details.tb-como li { margin: 4px 0; }
|
||||||
.kb{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;align-items:start}
|
|
||||||
@media(max-width:980px){.kb{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
||||||
@media(max-width:560px){.kb{grid-template-columns:1fr}}
|
|
||||||
.col{background:var(--pend-bg);border:1px solid var(--linea);border-radius:12px;padding:8px;min-height:120px}
|
|
||||||
.col.sobre{outline:2px dashed var(--naranja);outline-offset:-4px;background:var(--naranja-suave)}
|
|
||||||
.col h3{margin:2px 4px 8px;font-size:13px;text-transform:uppercase;letter-spacing:.05em;color:var(--tinta2);display:flex;justify-content:space-between}
|
|
||||||
.col h3 .n{background:var(--tarjeta);border:1px solid var(--linea);border-radius:99px;padding:0 8px;font-size:12px}
|
|
||||||
.col.e-bloqueado h3{color:var(--falla)} .col.e-hecho h3{color:var(--ok)} .col.e-en-curso h3{color:var(--info)}
|
|
||||||
.card{background:var(--tarjeta);border:1px solid var(--linea);border-left:4px solid var(--pend);border-radius:10px;padding:9px 10px;margin:0 0 8px;cursor:grab;font-size:14px;position:relative}
|
|
||||||
.card:active{cursor:grabbing}
|
|
||||||
.card.arrastrando{opacity:.45}
|
|
||||||
.card.p-ingenieria{border-left-color:var(--morado)} .card.p-produccion{border-left-color:var(--ok)}
|
|
||||||
.card.p-mecanica{border-left-color:var(--aviso)} .card.p-compras{border-left-color:var(--info)} .card.p-campo{border-left-color:var(--naranja)}
|
|
||||||
.card.vencida{box-shadow:inset 0 0 0 1px var(--falla)}
|
|
||||||
.card .tit{font-weight:700;line-height:1.3;margin-bottom:5px;cursor:pointer}
|
|
||||||
.card .tit:hover{color:var(--naranja)}
|
|
||||||
.card .meta{display:flex;flex-wrap:wrap;gap:4px 8px;font-size:12px;color:var(--tinta2);align-items:center}
|
|
||||||
.card .chip{display:inline-block;font-size:11px;font-weight:700;padding:1px 7px;border-radius:99px;background:var(--pend-bg);color:var(--tinta2)}
|
|
||||||
.card .chip.p-ingenieria{background:var(--morado-bg);color:var(--morado)} .card .chip.p-produccion{background:var(--ok-bg);color:var(--ok)}
|
|
||||||
.card .chip.p-mecanica{background:var(--aviso-bg);color:#7a4a00} .card .chip.p-compras{background:var(--info-bg);color:var(--info)}
|
|
||||||
.card .chip.p-campo{background:var(--naranja-suave);color:var(--naranja)}
|
|
||||||
.card .fecha.mal{color:var(--falla);font-weight:700}
|
|
||||||
.card .bloq{margin-top:5px;font-size:12px;color:var(--falla)}
|
|
||||||
.card .enl{margin-top:5px;display:flex;flex-wrap:wrap;gap:4px 10px;font-size:12px}
|
|
||||||
.card .mover{margin-top:7px;display:flex;gap:6px;align-items:center}
|
|
||||||
.card .mover select{padding:5px 7px;font-size:12.5px;flex:1}
|
|
||||||
@media(min-width:981px){.card .mover{display:none} .kb.movil .card .mover{display:flex}}
|
|
||||||
.badge-v3,.badge-v4,.badge-v55,.badge-v56,.badge-v57,.badge-v6{display:inline-block;padding:0 7px;border-radius:999px;font-size:11px;font-weight:700;line-height:1.7;white-space:nowrap}
|
|
||||||
.badge-v56{background:#2C3C5B;color:#fff} .badge-v55{background:#17A08C;color:#fff}
|
|
||||||
.badge-v57{background:#e3f5e8;color:#1c5c2e;border:1px solid #8fd1a3} .badge-v6{background:#fff3cd;color:#7a5200;border:1px solid #f0c36d}
|
|
||||||
.badge-v4{background:#EDEFF4;color:#2C3C5B;border:1px solid #c9d0e0} .badge-v3{background:#e9e9e9;color:#555;border:1px solid #ccc}
|
|
||||||
|
|
||||||
/* ---- diálogo de detalle / alta ---- */
|
/* los <details> que arman los scripts (detalle de un acta) van sin caja */
|
||||||
dialog{border:1px solid var(--linea);border-radius:14px;padding:0;max-width:720px;width:calc(100vw - 24px);background:var(--tarjeta);color:var(--tinta)}
|
.md-typeset .tb details:not(.tb-como) {
|
||||||
dialog::backdrop{background:rgba(0,0,0,.45)}
|
margin: 8px 0 0;
|
||||||
dialog .dcab{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:14px 18px;border-bottom:1px solid var(--linea);background:var(--cabecera);color:var(--cabecera-texto);border-radius:14px 14px 0 0}
|
padding: 0;
|
||||||
dialog .dcab h2{margin:0;font-size:17px}
|
border: 0;
|
||||||
dialog .dcab button{background:transparent;color:#fff;font-size:20px;padding:2px 8px}
|
background: none;
|
||||||
dialog .dcuerpo{padding:6px 18px 14px;max-height:calc(100vh - 160px);overflow:auto}
|
box-shadow: none;
|
||||||
dialog .dpie{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end;padding:12px 18px;border-top:1px solid var(--linea)}
|
font-size: inherit;
|
||||||
dialog .dpie .izq{margin-right:auto}
|
overflow: visible;
|
||||||
.dos-col{display:grid;grid-template-columns:1fr 1fr;gap:0 14px}
|
}
|
||||||
@media(max-width:560px){.dos-col{grid-template-columns:1fr}}
|
.md-typeset .tb details:not(.tb-como) > summary {
|
||||||
dialog input,dialog select,dialog textarea{width:100%}
|
padding: 0;
|
||||||
.enlaces-ed .fila{display:grid;grid-template-columns:1fr 1.4fr auto;gap:6px;margin-bottom:6px}
|
margin: 0;
|
||||||
.enlaces-ed .fila button{padding:6px 9px;font-size:13px}
|
min-height: 0;
|
||||||
.lista-chk{max-height:140px;overflow:auto;border:1px solid var(--linea);border-radius:9px;padding:6px 10px;font-size:13.5px}
|
background: none;
|
||||||
.lista-chk label{display:flex;gap:8px;align-items:flex-start;font-weight:500;margin:4px 0;color:var(--tinta)}
|
border: 0;
|
||||||
.lista-chk input{width:auto;margin-top:3px}
|
font-size: 13px;
|
||||||
/* responsables: chips seleccionables (miembros del equipo) */
|
font-weight: 600;
|
||||||
.chips{display:flex;flex-wrap:wrap;gap:6px}
|
color: var(--hg-liga);
|
||||||
.chip-sel{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--linea);border-radius:99px;padding:5px 11px;font-size:13.5px;font-weight:500;cursor:pointer;background:var(--superf);margin:0;color:var(--tinta)}
|
cursor: pointer;
|
||||||
.chip-sel input{width:auto;margin:0;accent-color:var(--naranja)}
|
}
|
||||||
.chip-sel.on{border-color:var(--naranja);background:var(--naranja-suave);color:var(--naranja);font-weight:700}
|
.md-typeset .tb details:not(.tb-como) > summary::before,
|
||||||
.hist{font-size:13px;color:var(--tinta2);max-height:180px;overflow:auto;border-top:1px solid var(--linea);margin-top:12px;padding-top:8px}
|
.md-typeset .tb details:not(.tb-como) > summary::after { display: none; }
|
||||||
.hist div{padding:3px 0;border-bottom:1px dashed var(--linea)}
|
.md-typeset .tb details:not(.tb-como) > summary:hover { color: var(--hg-liga-hover); }
|
||||||
.hist b{color:var(--tinta)}
|
|
||||||
.msg{display:none;padding:10px 12px;border-radius:9px;margin-top:10px;font-weight:600;font-size:14px}
|
|
||||||
.msg.ok{display:block;background:var(--ok-bg);color:var(--ok);border:1px solid var(--ok)}
|
|
||||||
.msg.err{display:block;background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla)}
|
|
||||||
|
|
||||||
/* ---- tablero de ingeniería: tira de versiones ---- */
|
/* ---- Controles ----------------------------------------------------------- */
|
||||||
.versiones{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px;margin-bottom:14px}
|
.tb button,
|
||||||
.ver{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:14px 16px;border-top:4px solid var(--pend)}
|
.tb .boton {
|
||||||
.ver.v55{border-top-color:#17A08C} .ver.v56{border-top-color:#2C3C5B} .ver.v57{border-top-color:#1c5c2e} .ver.v6{border-top-color:#c99a1a}
|
display: inline-flex;
|
||||||
.ver h3{margin:0 0 4px;font-size:17px;display:flex;justify-content:space-between;align-items:center;gap:8px}
|
align-items: center;
|
||||||
.ver .est{font-weight:700;font-size:13.5px}
|
gap: 7px;
|
||||||
.ver .sig{font-size:13.5px;margin:6px 0} .ver .sig b{color:var(--naranja)}
|
padding: 8px 14px;
|
||||||
.ver .quien{font-size:12.5px;color:var(--tinta2)}
|
border-radius: 8px;
|
||||||
.ver .docs{margin-top:8px;display:flex;flex-wrap:wrap;gap:4px 10px;font-size:12.5px}
|
border: 1px solid var(--hg-acento);
|
||||||
.ver .cnt{font-size:12.5px;color:var(--tinta2);margin-top:6px}
|
background: var(--hg-acento);
|
||||||
.decisiones li{margin:5px 0;font-size:14px} .decisiones b{color:var(--tinta)}
|
color: var(--hg-sobre-acento);
|
||||||
.filosofia li{margin:6px 0;font-size:14px}
|
font: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tb button:hover:not(:disabled) { filter: brightness(1.07); }
|
||||||
|
.tb button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.tb button.sec,
|
||||||
|
.tb .boton.sec {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--hg-linea-2);
|
||||||
|
color: var(--hg-texto);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.tb button.sec:hover:not(:disabled),
|
||||||
|
.tb .boton.sec:hover { background: var(--hg-elevado); filter: none; color: var(--hg-texto-fuerte); text-decoration: none; }
|
||||||
|
.tb button.peligro {
|
||||||
|
background: transparent;
|
||||||
|
border-color: color-mix(in srgb, var(--hg-bloqueado) 55%, transparent);
|
||||||
|
color: var(--hg-bloqueado);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.tb button.peligro:hover:not(:disabled) { background: var(--hg-bloqueado-bg); filter: none; }
|
||||||
|
.tb button.fantasma { background: transparent; border-color: transparent; color: var(--hg-texto-2); font-weight: 500; }
|
||||||
|
.tb button.fantasma:hover:not(:disabled) { color: var(--hg-texto-fuerte); background: var(--hg-elevado); filter: none; }
|
||||||
|
.tb button.icono { padding: 6px; width: 32px; height: 32px; justify-content: center; }
|
||||||
|
.tb button svg, .tb .boton svg { width: 15px; height: 15px; flex: none; }
|
||||||
|
|
||||||
/* ---- tablero de producción ---- */
|
.tb select,
|
||||||
.prog{height:14px;border-radius:7px;background:var(--pend-bg);overflow:hidden;margin:6px 0 4px;display:flex}
|
.tb input:not([type="checkbox"]):not([type="radio"]),
|
||||||
.prog span{display:block;height:100%;background:var(--ok)}
|
.tb textarea {
|
||||||
.prog span.pl{background:var(--info)}
|
padding: 8px 11px;
|
||||||
.obj{display:flex;justify-content:space-between;font-size:13px;color:var(--tinta2)}
|
border: 1px solid var(--hg-linea-2);
|
||||||
.embudo{display:grid;grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:8px}
|
border-radius: var(--hg-radio-campo);
|
||||||
.etapa{background:var(--superf);border:1px solid var(--linea);border-radius:10px;padding:10px 10px;text-align:center}
|
background: var(--hg-plano);
|
||||||
.etapa .n{font-size:26px;font-weight:700;line-height:1}
|
color: var(--hg-texto);
|
||||||
.etapa .l{font-size:12px;color:var(--tinta2);margin-top:3px;line-height:1.25}
|
font: inherit;
|
||||||
.etapa .f{font-size:11px;color:var(--tinta2);margin-top:3px;opacity:.8}
|
font-size: 14px;
|
||||||
.etapa.sin .n{color:var(--pend)}
|
max-width: 100%;
|
||||||
.bloqueos .card{cursor:default}
|
}
|
||||||
|
.tb select { padding-right: 28px; }
|
||||||
|
.tb input::placeholder, .tb textarea::placeholder { color: var(--hg-texto-3); }
|
||||||
|
.tb input:focus, .tb select:focus, .tb textarea:focus { outline: 2px solid var(--hg-foco); outline-offset: 1px; border-color: transparent; }
|
||||||
|
.tb input[type="checkbox"], .tb input[type="radio"] { accent-color: var(--hg-teal); }
|
||||||
|
.tb label { display: block; font-weight: 600; font-size: 13px; margin: 12px 0 5px; color: var(--hg-texto-2); }
|
||||||
|
|
||||||
|
/* ---- Bloques, cifras, avisos -------------------------------------------- */
|
||||||
|
.tb section.bloque {
|
||||||
|
background: var(--hg-plano-2);
|
||||||
|
border: 1px solid var(--hg-linea);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
margin: 14px 0;
|
||||||
|
}
|
||||||
|
.md-typeset .tb section.bloque h2,
|
||||||
|
.md-typeset .tb h2 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-family: var(--hg-fuente-titulo);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 21px;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: var(--hg-texto-fuerte);
|
||||||
|
}
|
||||||
|
.md-typeset .tb h3 { margin: 0 0 6px; font-size: 16px; line-height: 1.3; color: var(--hg-texto-fuerte); }
|
||||||
|
.md-typeset .tb p { margin: 0 0 10px; }
|
||||||
|
.tb .sub, .tb .resumen { color: var(--hg-texto-2); font-size: 14px; margin: 0 0 12px; }
|
||||||
|
.md-typeset .tb ul, .md-typeset .tb ol { margin: 6px 0 0 1.25em; }
|
||||||
|
.md-typeset .tb li { margin: 0 0 6px; }
|
||||||
|
|
||||||
|
.tb .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 16px; }
|
||||||
|
.tb .kpi { background: var(--hg-plano); border: 1px solid var(--hg-linea); border-radius: 12px; padding: 13px 15px; }
|
||||||
|
.tb .kpi .n { font-family: var(--hg-fuente-titulo); font-size: 30px; font-weight: 600; line-height: 1; color: var(--hg-texto-fuerte); font-variant-numeric: tabular-nums; }
|
||||||
|
.tb .kpi .l { color: var(--hg-texto-2); font-size: 13px; margin-top: 6px; line-height: 1.3; }
|
||||||
|
.tb .kpi.mal .n { color: var(--hg-bloqueado); }
|
||||||
|
.tb .kpi.bien .n { color: var(--hg-hecho); }
|
||||||
|
|
||||||
|
.tb .err { background: var(--hg-bloqueado-bg); color: var(--hg-bloqueado); border: 1px solid color-mix(in srgb, var(--hg-bloqueado) 45%, transparent); padding: 10px 14px; border-radius: 10px; margin: 10px 0; }
|
||||||
|
.tb .vacio { color: var(--hg-texto-3); font-size: 14px; }
|
||||||
|
.tb .msg { display: none; padding: 10px 12px; border-radius: 9px; margin: 10px 0; font-weight: 600; font-size: 14px; }
|
||||||
|
.tb .msg.ok { display: block; background: var(--hg-hecho-bg); color: var(--hg-hecho); border: 1px solid color-mix(in srgb, var(--hg-hecho) 40%, transparent); }
|
||||||
|
.tb .msg.err { display: block; background: var(--hg-bloqueado-bg); color: var(--hg-bloqueado); border: 1px solid color-mix(in srgb, var(--hg-bloqueado) 45%, transparent); }
|
||||||
|
|
||||||
|
.tb .pill { display: inline-block; font-size: 12px; font-weight: 600; padding: 2px 9px; border-radius: 6px; white-space: nowrap; vertical-align: middle; background: var(--hg-pend-bg); color: var(--hg-texto-2); }
|
||||||
|
.tb .pill.ok, .tb .pill.apro { background: var(--hg-hecho-bg); color: var(--hg-hecho); }
|
||||||
|
.tb .pill.falla { background: var(--hg-bloqueado-bg); color: var(--hg-bloqueado); }
|
||||||
|
.tb .pill.info { background: var(--hg-info-bg); color: var(--hg-info); }
|
||||||
|
.tb .pill.aviso { background: var(--hg-aviso-bg); color: var(--hg-aviso); }
|
||||||
|
.tb .pill.pend { background: var(--hg-pend-bg); color: var(--hg-texto-2); }
|
||||||
|
|
||||||
|
/* tablas */
|
||||||
|
.tb .tblwrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||||
|
.md-typeset .tb table.t,
|
||||||
|
.md-typeset .tb table:not([class]) {
|
||||||
|
display: table;
|
||||||
|
border: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-radius: 0;
|
||||||
|
background: none;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.md-typeset .tb table.t th,
|
||||||
|
.md-typeset .tb table:not([class]) th {
|
||||||
|
text-align: left;
|
||||||
|
color: var(--hg-texto-3);
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: 0;
|
||||||
|
padding: 6px 10px 7px 0;
|
||||||
|
min-width: 0;
|
||||||
|
background: none;
|
||||||
|
border-bottom: 1px solid var(--hg-linea-2);
|
||||||
|
}
|
||||||
|
.md-typeset .tb table.t td,
|
||||||
|
.md-typeset .tb table:not([class]) td { padding: 8px 10px 8px 0; border-top: 0; border-bottom: 1px solid var(--hg-linea); vertical-align: top; }
|
||||||
|
.md-typeset .tb table.t tr:hover,
|
||||||
|
.md-typeset .tb table:not([class]) tbody tr:hover { background: none; box-shadow: none; }
|
||||||
|
|
||||||
|
/* versión de tarjeta: etiqueta de serigrafía (helgen.css) a tamaño de tablero */
|
||||||
|
.tb .badge-v3, .tb .badge-v4, .tb .badge-v55, .tb .badge-v56, .tb .badge-v57, .tb .badge-v6 { font-size: 11.5px; padding: 3px 6px; vertical-align: middle; }
|
||||||
|
|
||||||
|
/* avatares del equipo */
|
||||||
|
.tb .avs { display: inline-flex; align-items: center; }
|
||||||
|
.tb .av {
|
||||||
|
width: 22px; height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
flex: none;
|
||||||
|
font-size: 9.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background: #4A5468;
|
||||||
|
box-shadow: 0 0 0 2px var(--hg-plano);
|
||||||
|
}
|
||||||
|
.tb .avs .av + .av { margin-left: -6px; }
|
||||||
|
.tb .av.libre { background: transparent; color: var(--hg-texto-2); box-shadow: inset 0 0 0 1px var(--hg-linea-2); }
|
||||||
|
.tb .persona { display: inline-flex; align-items: center; gap: 7px; }
|
||||||
|
|
||||||
|
/* íconos de línea en el tablero */
|
||||||
|
.tb .i { width: 14px; height: 14px; flex: none; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||||
|
|
||||||
|
/* ---- Tablero de pendientes ---------------------------------------------- */
|
||||||
|
.tb .kb-barra { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 14px; }
|
||||||
|
.tb .kb-barra select, .tb .kb-barra input { padding: 7px 10px; font-size: 13.5px; }
|
||||||
|
.tb .kb-barra input[type="search"] { flex: 0 1 210px; min-width: 140px; }
|
||||||
|
.tb .kb-barra .cnt { color: var(--hg-texto-3); font-size: 13px; }
|
||||||
|
.tb .kb-barra .kb-nuevo { margin-left: auto; }
|
||||||
|
.tb .kb-solo {
|
||||||
|
display: inline-flex; align-items: center; gap: 7px;
|
||||||
|
padding: 7px 12px; border-radius: 99px;
|
||||||
|
border: 1px solid var(--hg-linea-2); background: transparent; color: var(--hg-texto-suave);
|
||||||
|
font-size: 13.5px; font-weight: 500;
|
||||||
|
}
|
||||||
|
.tb .kb-solo[aria-pressed="true"] { background: var(--hg-curso-bg); border-color: color-mix(in srgb, var(--hg-curso) 45%, transparent); color: var(--hg-curso); }
|
||||||
|
.tb .kb-solo:hover:not(:disabled) { filter: none; color: var(--hg-texto-fuerte); }
|
||||||
|
|
||||||
|
.tb .kb { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; align-items: start; }
|
||||||
|
@media (max-width: 1100px) { .tb .kb { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||||
|
@media (max-width: 600px) { .tb .kb { grid-template-columns: 1fr; } }
|
||||||
|
.tb .kb.compacto { gap: 12px; }
|
||||||
|
|
||||||
|
.tb .col { display: flex; flex-direction: column; gap: 8px; min-width: 0; min-height: 90px; border-radius: 10px; }
|
||||||
|
.tb .col.sobre { outline: 2px dashed color-mix(in srgb, var(--hg-acento) 60%, transparent); outline-offset: 4px; }
|
||||||
|
.md-typeset .tb .col h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 2px 8px;
|
||||||
|
border-bottom: 1px solid var(--hg-linea);
|
||||||
|
font-family: var(--hg-fuente);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-transform: none;
|
||||||
|
color: var(--hg-texto);
|
||||||
|
}
|
||||||
|
.tb .col h3 .n { font-weight: 400; font-size: 13px; color: var(--hg-texto-3); font-variant-numeric: tabular-nums; }
|
||||||
|
.tb .col h3 svg { width: 16px; height: 16px; flex: none; }
|
||||||
|
.tb .col .kb-mas {
|
||||||
|
align-self: flex-start;
|
||||||
|
background: transparent; border: 0; color: var(--hg-texto-3);
|
||||||
|
font-size: 13.5px; font-weight: 500; padding: 6px 4px;
|
||||||
|
}
|
||||||
|
.tb .col .kb-mas:hover:not(:disabled) { color: var(--hg-texto-fuerte); filter: none; }
|
||||||
|
|
||||||
|
.tb .card {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 12px 11px;
|
||||||
|
background: var(--hg-plano);
|
||||||
|
border: 1px solid var(--hg-linea);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.tb .card:hover { border-color: var(--hg-linea-2); }
|
||||||
|
.tb .card:active { cursor: grabbing; }
|
||||||
|
.tb .card.arrastrando { opacity: 0.45; }
|
||||||
|
.tb .card.vencida { border-color: color-mix(in srgb, var(--hg-bloqueado) 45%, var(--hg-linea)); }
|
||||||
|
.tb .card.e-hecho .tit { color: var(--hg-texto-2); }
|
||||||
|
.tb .card .sup { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--hg-texto-2); min-width: 0; }
|
||||||
|
.tb .card .sup .ver-t { margin-left: auto; }
|
||||||
|
.tb .pista-p { width: 8px; height: 8px; border-radius: 2px; flex: none; background: var(--hg-texto-3); display: inline-block; }
|
||||||
|
.tb .p-ingenieria .pista-p, .tb .pista-p.p-ingenieria { background: var(--hg-p-ingenieria); }
|
||||||
|
.tb .p-produccion .pista-p, .tb .pista-p.p-produccion { background: var(--hg-p-produccion); }
|
||||||
|
.tb .p-mecanica .pista-p, .tb .pista-p.p-mecanica { background: var(--hg-p-mecanica); }
|
||||||
|
.tb .p-compras .pista-p, .tb .pista-p.p-compras { background: var(--hg-p-compras); }
|
||||||
|
.tb .p-campo .pista-p, .tb .pista-p.p-campo { background: var(--hg-p-campo); }
|
||||||
|
.tb .card .tit { font-weight: 600; font-size: 14.5px; line-height: 1.36; color: var(--hg-texto); cursor: pointer; overflow-wrap: anywhere; }
|
||||||
|
.tb .card .tit:hover { color: var(--hg-liga-hover); }
|
||||||
|
.tb .card .tit a { color: inherit; text-decoration: none; }
|
||||||
|
.tb .card .nota { display: flex; gap: 6px; align-items: flex-start; font-size: 12.5px; line-height: 1.35; color: var(--hg-aviso); }
|
||||||
|
.tb .card .nota.mal { color: var(--hg-bloqueado); }
|
||||||
|
.tb .card .nota .i { margin-top: 1px; }
|
||||||
|
.tb .card .enl { display: flex; flex-direction: column; gap: 3px; font-size: 12.5px; }
|
||||||
|
.tb .card .enl a { display: inline-flex; align-items: center; gap: 6px; color: var(--hg-liga); min-width: 0; }
|
||||||
|
.tb .card .enl a span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.tb .card .etq { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||||
|
.tb .chip { display: inline-block; font-size: 11.5px; font-weight: 500; padding: 1px 7px; border-radius: 5px; background: var(--hg-pend-bg); color: var(--hg-texto-2); white-space: nowrap; }
|
||||||
|
.tb .chip.p-ingenieria, .tb .chip.p-produccion, .tb .chip.p-mecanica, .tb .chip.p-compras, .tb .chip.p-campo { background: none; padding: 0; color: var(--hg-texto-2); display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; }
|
||||||
|
.tb .chip.p-ingenieria::before, .tb .chip.p-produccion::before, .tb .chip.p-mecanica::before, .tb .chip.p-compras::before, .tb .chip.p-campo::before { content: ""; width: 8px; height: 8px; border-radius: 2px; background: var(--hg-texto-3); }
|
||||||
|
.tb .chip.p-ingenieria::before { background: var(--hg-p-ingenieria); }
|
||||||
|
.tb .chip.p-produccion::before { background: var(--hg-p-produccion); }
|
||||||
|
.tb .chip.p-mecanica::before { background: var(--hg-p-mecanica); }
|
||||||
|
.tb .chip.p-compras::before { background: var(--hg-p-compras); }
|
||||||
|
.tb .chip.p-campo::before { background: var(--hg-p-campo); }
|
||||||
|
.tb .card .pie { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||||
|
.tb .card .fecha { font-size: 12.5px; color: var(--hg-texto-2); white-space: nowrap; }
|
||||||
|
.tb .card .fecha.hoy { color: var(--hg-acento); font-weight: 600; }
|
||||||
|
.tb .card .fecha.mal { color: var(--hg-bloqueado); font-weight: 600; }
|
||||||
|
.tb .card .meta { display: flex; flex-wrap: wrap; gap: 4px 10px; font-size: 12.5px; color: var(--hg-texto-2); align-items: center; }
|
||||||
|
.tb .card .mover { display: flex; gap: 6px; align-items: center; }
|
||||||
|
.tb .card .mover select { padding: 5px 8px; font-size: 13px; flex: 1; }
|
||||||
|
@media (min-width: 981px) { .tb .card .mover { display: none; } .tb .kb.movil .card .mover { display: flex; } }
|
||||||
|
|
||||||
|
/* ---- Diálogo (detalle y alta) -------------------------------------------- */
|
||||||
|
.tb dialog {
|
||||||
|
border: 1px solid var(--hg-linea-2);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 760px;
|
||||||
|
width: calc(100vw - 24px);
|
||||||
|
background: var(--hg-plano-2);
|
||||||
|
color: var(--hg-texto);
|
||||||
|
box-shadow: var(--hg-sombra);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.tb dialog::backdrop { background: rgba(5, 9, 18, 0.62); backdrop-filter: blur(1.5px); }
|
||||||
|
.tb dialog .dcab { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 16px 20px 10px; }
|
||||||
|
.md-typeset .tb dialog .dcab h2 { margin: 0; font-family: var(--hg-fuente-titulo); font-size: 22px; font-weight: 600; line-height: 1.15; }
|
||||||
|
.tb dialog .dcab .dcab-id { font-family: var(--hg-fuente-mono); font-size: 12.5px; color: var(--hg-texto-3); margin-right: auto; padding-left: 4px; }
|
||||||
|
.tb dialog .dcab button { background: transparent; border-color: transparent; color: var(--hg-texto-2); padding: 6px; width: 32px; height: 32px; justify-content: center; }
|
||||||
|
.tb dialog .dcab button:hover { background: var(--hg-elevado); color: var(--hg-texto-fuerte); filter: none; }
|
||||||
|
.tb dialog .dcuerpo { padding: 2px 20px 16px; max-height: calc(100vh - 180px); overflow: auto; }
|
||||||
|
.tb dialog .dpie { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; align-items: center; padding: 12px 20px; border-top: 1px solid var(--hg-linea); }
|
||||||
|
.tb dialog .dpie .izq { margin-right: auto; }
|
||||||
|
.tb .dos-col { display: grid; grid-template-columns: 1fr 1fr; gap: 0 14px; }
|
||||||
|
@media (max-width: 560px) { .tb .dos-col { grid-template-columns: 1fr; } }
|
||||||
|
.tb dialog input:not([type="checkbox"]):not([type="radio"]), .tb dialog select, .tb dialog textarea { width: 100%; }
|
||||||
|
.tb .enlaces-ed .fila { display: grid; grid-template-columns: 1fr 1.4fr auto; gap: 6px; margin-bottom: 6px; }
|
||||||
|
.tb .enlaces-ed .fila button { padding: 6px 9px; }
|
||||||
|
.tb .lista-chk { max-height: 150px; overflow: auto; border: 1px solid var(--hg-linea); border-radius: 9px; padding: 6px 10px; font-size: 13.5px; background: var(--hg-plano); }
|
||||||
|
.tb .lista-chk label { display: flex; gap: 8px; align-items: flex-start; font-weight: 500; margin: 4px 0; color: var(--hg-texto); font-size: 13.5px; }
|
||||||
|
.tb .lista-chk input { width: auto; margin-top: 3px; }
|
||||||
|
.tb .chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.tb .chip-sel {
|
||||||
|
display: inline-flex; align-items: center; gap: 7px;
|
||||||
|
border: 1px solid var(--hg-linea-2); border-radius: 99px;
|
||||||
|
padding: 4px 11px 4px 5px; margin: 0;
|
||||||
|
font-size: 13.5px; font-weight: 500; cursor: pointer;
|
||||||
|
background: var(--hg-plano); color: var(--hg-texto);
|
||||||
|
}
|
||||||
|
.tb .chip-sel input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
||||||
|
.tb .chip-sel.on { border-color: color-mix(in srgb, var(--hg-curso) 50%, transparent); background: var(--hg-curso-bg); color: var(--hg-texto-fuerte); font-weight: 600; }
|
||||||
|
.tb .chip-sel:focus-within { outline: 2px solid var(--hg-foco); outline-offset: 1px; }
|
||||||
|
.tb .hist { font-size: 13px; color: var(--hg-texto-2); max-height: 200px; overflow: auto; border-top: 1px solid var(--hg-linea); margin-top: 14px; padding-top: 8px; }
|
||||||
|
.tb .hist div { padding: 4px 0; border-bottom: 1px dashed var(--hg-linea); }
|
||||||
|
.tb .hist b { color: var(--hg-texto); }
|
||||||
|
|
||||||
|
/* ---- Ingeniería: una tarjeta por versión --------------------------------- */
|
||||||
|
.tb .versiones { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px; margin-bottom: 14px; }
|
||||||
|
.tb .versiones .ver { background: var(--hg-plano); border: 1px solid var(--hg-linea); border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.md-typeset .tb .versiones .ver h3 { margin: 0; font-family: var(--hg-fuente-titulo); font-size: 19px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
|
||||||
|
.tb .versiones .ver .est { font-weight: 600; font-size: 13.5px; color: var(--hg-texto); }
|
||||||
|
.tb .versiones .ver .sig { font-size: 13.5px; color: var(--hg-texto-suave); }
|
||||||
|
.tb .versiones .ver .sig b { color: var(--hg-texto-fuerte); }
|
||||||
|
.tb .versiones .ver .quien { font-size: 12.5px; color: var(--hg-texto-2); display: flex; gap: 6px; align-items: flex-start; }
|
||||||
|
.tb .versiones .ver .docs { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 13px; }
|
||||||
|
.tb .versiones .ver .cnt { font-size: 13px; color: var(--hg-texto-3); margin-top: auto; padding-top: 6px; border-top: 1px solid var(--hg-linea); }
|
||||||
|
.md-typeset .tb .decisiones li { margin: 6px 0; font-size: 14px; color: var(--hg-texto-suave); }
|
||||||
|
.md-typeset .tb .decisiones b { color: var(--hg-texto-fuerte); }
|
||||||
|
.md-typeset .tb .filosofia li { margin: 7px 0; font-size: 14px; color: var(--hg-texto-suave); }
|
||||||
|
|
||||||
|
/* ---- Producción ---------------------------------------------------------- */
|
||||||
|
.tb .prog { height: 10px; border-radius: 5px; background: var(--hg-pend-bg); overflow: hidden; margin: 8px 0 4px; display: flex; }
|
||||||
|
.tb .prog span { display: block; height: 100%; background: var(--hg-hecho); }
|
||||||
|
.tb .prog span.pl { background: var(--hg-info); }
|
||||||
|
.tb .obj { display: flex; justify-content: space-between; font-size: 13px; color: var(--hg-texto-2); }
|
||||||
|
.tb .embudo { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: 8px; }
|
||||||
|
.tb .etapa { background: var(--hg-plano); border: 1px solid var(--hg-linea); border-radius: 10px; padding: 11px 11px; text-align: center; color: inherit; text-decoration: none; }
|
||||||
|
.tb a.etapa:hover { border-color: var(--hg-linea-2); text-decoration: none; }
|
||||||
|
.tb .etapa .n { font-family: var(--hg-fuente-titulo); font-size: 26px; font-weight: 600; line-height: 1; color: var(--hg-texto-fuerte); }
|
||||||
|
.tb .etapa .l { font-size: 12.5px; color: var(--hg-texto-2); margin-top: 4px; line-height: 1.25; }
|
||||||
|
.tb .etapa .f { font-size: 11.5px; color: var(--hg-texto-3); margin-top: 3px; }
|
||||||
|
.tb .etapa.sin .n { color: var(--hg-texto-3); }
|
||||||
|
.tb .bloqueos .card { cursor: default; }
|
||||||
|
|
||||||
|
/* ---- Equipo ---------------------------------------------------------------- */
|
||||||
|
.tb table.t td.num, .tb table.t th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
.tb table.t tr.inactivo td { opacity: 0.55; }
|
||||||
|
.tb table.t td .nom { font-weight: 700; cursor: pointer; color: var(--hg-liga); display: inline-flex; align-items: center; gap: 8px; }
|
||||||
|
.tb table.t td .nom:hover { color: var(--hg-liga-hover); text-decoration: underline; }
|
||||||
|
.tb table.t td .id { font-size: 12px; color: var(--hg-texto-3); display: block; margin-top: 2px; }
|
||||||
|
.tb table.t td button { padding: 5px 9px; font-size: 12.5px; margin: 1px 2px 1px 0; }
|
||||||
|
.tb .area { display: inline-block; font-size: 12px; font-weight: 500; padding: 1px 8px; border-radius: 5px; background: var(--hg-pend-bg); color: var(--hg-texto-2); }
|
||||||
|
.tb .pass-una-vez { background: var(--hg-aviso-bg); border: 1px solid color-mix(in srgb, var(--hg-aviso) 45%, transparent); border-radius: 10px; padding: 12px 14px; margin-top: 10px; font-size: 14.5px; color: var(--hg-texto); }
|
||||||
|
.tb .pass-una-vez code { font-family: var(--hg-fuente-mono); font-size: 17px; font-weight: 600; letter-spacing: 0.04em; user-select: all; }
|
||||||
|
.tb .mis-pend .card { cursor: default; }
|
||||||
|
|
||||||
|
/* ---- Inventario ------------------------------------------------------------ */
|
||||||
|
.tb table.t tr.alerta td.num.stock { color: var(--hg-bloqueado); font-weight: 700; }
|
||||||
|
.tb table.t td .cod { font-family: var(--hg-fuente-mono); font-weight: 600; cursor: pointer; color: var(--hg-liga); }
|
||||||
|
.tb table.t td .cod:hover { text-decoration: underline; }
|
||||||
|
.tb table.t td .cat { font-size: 12px; color: var(--hg-texto-3); display: block; }
|
||||||
|
.tb .acciones { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 12px; }
|
||||||
|
.tb .acciones .cnt { color: var(--hg-texto-3); font-size: 13px; margin-left: auto; }
|
||||||
|
.tb .acciones label { margin: 0; display: inline-flex; gap: 6px; align-items: center; font-weight: 500; color: var(--hg-texto-2); }
|
||||||
|
.tb .mov-fila { display: grid; grid-template-columns: 120px 80px 90px 1fr; gap: 6px 10px; padding: 7px 0; border-bottom: 1px dashed var(--hg-linea); font-size: 13px; align-items: baseline; }
|
||||||
|
.tb .mov-fila .d { font-variant-numeric: tabular-nums; }
|
||||||
|
.tb .mov-fila .pos { color: var(--hg-hecho); font-weight: 700; }
|
||||||
|
.tb .mov-fila .neg { color: var(--hg-bloqueado); font-weight: 700; }
|
||||||
|
@media (max-width: 560px) { .tb .mov-fila { grid-template-columns: 1fr 1fr; } }
|
||||||
|
.tb .ayuda { font-size: 12.5px; color: var(--hg-texto-3); margin-top: 4px; }
|
||||||
|
.tb .mini { font-size: 12.5px; color: var(--hg-texto-3); }
|
||||||
|
@media (max-width: 760px) { .tb #cont table.t { min-width: 780px; } }
|
||||||
|
|
||||||
|
/* ---- Sitios ------------------------------------------------------------------ */
|
||||||
|
.tb .ficha { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; margin-bottom: 14px; }
|
||||||
|
.tb .ficha .c { background: var(--hg-plano); border: 1px solid var(--hg-linea); border-radius: 10px; padding: 10px 13px; }
|
||||||
|
.tb .ficha .l { font-size: 12.5px; color: var(--hg-texto-3); }
|
||||||
|
.tb .ficha .v { font-weight: 600; font-size: 15px; }
|
||||||
|
.tb .contacto { padding: 7px 0; border-top: 1px dashed var(--hg-linea); font-size: 14px; }
|
||||||
|
.tb .contacto:first-of-type { border-top: 0; }
|
||||||
|
.tb .contacto .r { color: var(--hg-texto-2); font-size: 12.5px; }
|
||||||
|
.tb .sitio-sel { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
|
||||||
|
.tb .sitio-sel label { margin: 0; }
|
||||||
|
.tb .contactos-ed .fila { display: grid; grid-template-columns: 1.2fr 1fr 1fr 1.2fr auto; gap: 6px; margin-bottom: 6px; }
|
||||||
|
@media (max-width: 700px) { .tb .contactos-ed .fila { grid-template-columns: 1fr 1fr; } }
|
||||||
|
.tb .contactos-ed .fila button { padding: 6px 9px; }
|
||||||
|
.tb .por-texto { font-size: 11.5px; color: var(--hg-texto-3); border: 1px dashed var(--hg-linea-2); border-radius: 5px; padding: 0 6px; }
|
||||||
|
|
||||||
|
/* línea de tiempo (sitio y expediente) */
|
||||||
|
.tb .linea { position: relative; padding-left: 20px; border-left: 2px solid var(--hg-linea); margin-left: 6px; }
|
||||||
|
.tb .ev { position: relative; padding: 11px 0 11px 4px; border-bottom: 1px solid var(--hg-linea); font-size: 14px; }
|
||||||
|
.tb .ev:last-child { border-bottom: 0; }
|
||||||
|
.tb .linea .ev::before { content: ""; position: absolute; left: -27px; top: 17px; width: 10px; height: 10px; border-radius: 50%; background: var(--hg-texto-3); box-shadow: 0 0 0 3px var(--hg-plano-2); }
|
||||||
|
.tb .ev.instalacion-campo::before, .tb .ev.prod::before { background: var(--hg-hecho-icono); }
|
||||||
|
.tb .ev.diagnostico-campo::before, .tb .ev.campo::before { background: var(--hg-p-campo); }
|
||||||
|
.tb .ev.dano::before, .tb .ev.falla::before { background: var(--hg-bloqueado); }
|
||||||
|
.tb .ev.envio::before, .tb .ev.recepcion::before, .tb .ev.banco::before { background: var(--hg-info); }
|
||||||
|
.tb .ev.flash::before { background: var(--hg-p-compras); }
|
||||||
|
.tb .ev .f { color: var(--hg-texto-3); font-size: 12.5px; }
|
||||||
|
.tb .ev .cab { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; justify-content: space-between; }
|
||||||
|
.tb .ev .t { font-weight: 700; font-size: 15px; }
|
||||||
|
.tb .ev .cl { margin-top: 3px; }
|
||||||
|
|
||||||
|
/* ---- Estado de actas ---------------------------------------------------- */
|
||||||
|
.tb .barra { display: flex; height: 10px; border-radius: 5px; overflow: hidden; background: var(--hg-pend-bg); margin: 6px 0 14px; gap: 2px; }
|
||||||
|
.tb .barra span { display: block; height: 100%; }
|
||||||
|
.tb .barra .a { background: var(--hg-hecho-icono); }
|
||||||
|
.tb .barra .f { background: var(--hg-bloqueado); }
|
||||||
|
.tb .barra .e { background: var(--hg-info); }
|
||||||
|
.tb .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; }
|
||||||
|
.tb .cel { display: flex; flex-direction: column; gap: 2px; text-decoration: none; color: inherit; border: 1px solid var(--hg-linea); border-radius: 9px; padding: 8px 10px; background: var(--hg-plano); }
|
||||||
|
.tb a.cel:hover { border-color: var(--hg-linea-2); text-decoration: none; }
|
||||||
|
.tb .cel .s { font-family: var(--hg-fuente-mono); font-weight: 600; font-size: 13.5px; color: var(--hg-texto-fuerte); }
|
||||||
|
.tb .cel .st { font-size: 12px; font-weight: 600; display: flex; align-items: center; gap: 5px; }
|
||||||
|
.tb .cel .who { font-size: 11.5px; color: var(--hg-texto-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.tb .cel.apro { border-color: color-mix(in srgb, var(--hg-hecho) 40%, var(--hg-linea)); } .tb .cel.apro .st { color: var(--hg-hecho); }
|
||||||
|
.tb .cel.falla { border-color: color-mix(in srgb, var(--hg-bloqueado) 45%, var(--hg-linea)); background: var(--hg-bloqueado-bg); } .tb .cel.falla .st { color: var(--hg-bloqueado); }
|
||||||
|
.tb .cel.ens { border-color: color-mix(in srgb, var(--hg-info) 40%, var(--hg-linea)); } .tb .cel.ens .st { color: var(--hg-info); }
|
||||||
|
.tb .cel.pend .st { color: var(--hg-texto-3); }
|
||||||
|
.tb .leyenda { display: flex; flex-wrap: wrap; gap: 14px; font-size: 13px; color: var(--hg-texto-2); margin-top: 10px; }
|
||||||
|
.tb .leyenda b { font-weight: 600; }
|
||||||
|
.tb .dos { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
||||||
|
@media (max-width: 680px) { .tb .dos { grid-template-columns: 1fr; } }
|
||||||
|
.tb .st .i, .tb .leyenda .i { width: 13px; height: 13px; }
|
||||||
|
|
||||||
|
/* ---- Expediente de unidad y visitas de campo ----------------------------- */
|
||||||
|
.tb .buscar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 16px; }
|
||||||
|
.tb .buscar input { flex: 1 1 260px; }
|
||||||
|
.tb .fotos { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 9px; }
|
||||||
|
.tb .fotos a { display: block; width: 78px; height: 78px; border-radius: 8px; overflow: hidden; border: 1px solid var(--hg-linea); }
|
||||||
|
.tb .fotos img { width: 100%; height: 100%; object-fit: cover; display: block; background: var(--hg-pend-bg); }
|
||||||
|
.tb .logs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 9px; }
|
||||||
|
.tb .logs a { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 600; color: var(--hg-liga); text-decoration: none; border: 1px solid var(--hg-linea); border-radius: 7px; padding: 5px 10px; background: var(--hg-plano); }
|
||||||
|
.tb .logs a:hover { border-color: var(--hg-linea-2); }
|
||||||
|
.md-typeset .tb table.det { display: table; border: 0; border-collapse: collapse; width: 100%; font-size: 13px; margin-top: 8px; background: none; }
|
||||||
|
.md-typeset .tb table.det td { padding: 5px 8px 5px 0; border: 0; border-bottom: 1px solid var(--hg-linea); vertical-align: top; }
|
||||||
|
.md-typeset .tb table.det td.k { color: var(--hg-texto-2); width: 46%; }
|
||||||
|
.tb .cadena { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 4px; }
|
||||||
|
.tb .cadena .paso { flex: 1 1 118px; min-width: 118px; border: 1px solid var(--hg-linea); border-radius: 9px; padding: 7px 9px; background: var(--hg-plano); font-size: 12.5px; position: relative; }
|
||||||
|
.tb .cadena .paso .n { font-weight: 700; font-size: 13px; }
|
||||||
|
.tb .cadena .paso .e { margin-top: 2px; font-weight: 600; }
|
||||||
|
.tb .cadena .paso .d { color: var(--hg-texto-3); font-size: 11.5px; margin-top: 2px; line-height: 1.3; }
|
||||||
|
.tb .cadena .paso.ok { border-color: color-mix(in srgb, var(--hg-hecho) 40%, var(--hg-linea)); } .tb .cadena .paso.ok .e { color: var(--hg-hecho); }
|
||||||
|
.tb .cadena .paso.falla { border-color: color-mix(in srgb, var(--hg-bloqueado) 45%, var(--hg-linea)); background: var(--hg-bloqueado-bg); } .tb .cadena .paso.falla .e { color: var(--hg-bloqueado); }
|
||||||
|
.tb .cadena .paso.info { border-color: color-mix(in srgb, var(--hg-info) 40%, var(--hg-linea)); } .tb .cadena .paso.info .e { color: var(--hg-info); }
|
||||||
|
.tb .cadena .paso.pend .e { color: var(--hg-texto-3); }
|
||||||
|
.tb .cadena .paso a { color: inherit; }
|
||||||
|
.tb .cad-t { font-size: 13px; font-weight: 600; color: var(--hg-texto-3); margin: 10px 0 0; }
|
||||||
|
.tb .unidad { border: 1px solid var(--hg-linea); border-radius: 11px; padding: 12px 14px; margin: 10px 0; background: var(--hg-plano); }
|
||||||
|
.tb .unidad .cab { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; justify-content: space-between; }
|
||||||
|
.tb .unidad .s { font-family: var(--hg-fuente-mono); font-weight: 600; font-size: 15px; }
|
||||||
|
.tb .unidad .m { color: var(--hg-texto-2); font-size: 13px; }
|
||||||
|
.tb .visita { border-top: 1px solid var(--hg-linea); margin-top: 10px; padding-top: 10px; font-size: 14px; }
|
||||||
|
.tb .visita .f { color: var(--hg-texto-3); font-size: 12.5px; }
|
||||||
|
.tb .visita .cl { font-weight: 700; }
|
||||||
|
.tb .marca-unidad { border-radius: 10px; padding: 10px 14px; margin: 0 0 14px; font-size: 14.5px; }
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.tb .tb-cab__acc, .tb .buscar, .tb button, .tb .kb-barra, .tb details.tb-como { display: none !important; }
|
||||||
|
.tb section.bloque, .tb .kpi, .tb .card, .tb .cel, .tb .cadena .paso { background: #fff !important; border-color: #999 !important; color: #000 !important; }
|
||||||
|
}
|
||||||
|
|||||||
268
docs/tableros/unidad.md
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
---
|
||||||
|
title: "Expediente de unidad"
|
||||||
|
description: "Todo lo registrado para una serie: actas, fotos, logs y trazabilidad de la tarjeta al puerto."
|
||||||
|
template: tablero.html
|
||||||
|
hide:
|
||||||
|
- toc
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="tb tb-unidad">
|
||||||
|
<header class="tb-cab">
|
||||||
|
<div class="tb-cab__t"><h1 id="titulo">Expediente de unidad</h1><span class="meta" id="meta">Elige una unidad para ver todo su historial.</span></div>
|
||||||
|
<div class="tb-cab__acc"><a class="boton sec" href="/tableros/campo/"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM12 7v5l3.2 2"/></svg>Visitas de campo</a><button type="button" class="sec" onclick="window.print()"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 8.5V4h10v4.5M7 17H5a1.5 1.5 0 0 1-1.5-1.5v-5A1.5 1.5 0 0 1 5 9h14a1.5 1.5 0 0 1 1.5 1.5v5A1.5 1.5 0 0 1 19 17h-2M7 14h10v6H7z"/></svg>Imprimir</button></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="buscar">
|
||||||
|
<select id="sel"><option value="">— elegir unidad —</option></select>
|
||||||
|
<input id="libre" placeholder="…o escribe una serie: RTK-014, ARD-007, V5-03">
|
||||||
|
<button id="ir">Ver expediente</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="cont"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/formularios/flota.js"></script>
|
||||||
|
<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=>e.append(c)); return e;};
|
||||||
|
const $=i=>document.getElementById(i);
|
||||||
|
const cont=$('cont');
|
||||||
|
const fecha=s=>(s||'').replace('T',' ').slice(0,16);
|
||||||
|
const kb=n=>n>1048576?(n/1048576).toFixed(1)+' MB':Math.max(1,Math.round(n/1024))+' kB';
|
||||||
|
|
||||||
|
const NOMBRE={'diagnostico-campo':['Diagnóstico en campo','campo'],'diagnostico-banco':['Diagnóstico en banco','banco'],
|
||||||
|
'reflasheo':['Reflasheo','flash'],'instalacion-campo':['Instalación en sitio','campo'],
|
||||||
|
'dano':['Daño o incidente','falla'],'envio':['Envío','prod'],'recepcion':['Recepción','prod'],
|
||||||
|
'prueba-v5':['Prueba de tarjeta V5','prod'],'prueba-v4':['Prueba de tarjeta V4','prod'],
|
||||||
|
'modulo-ard':['Módulo ArduSimple','prod'],'modulo-4g':['Módem 4G','prod'],
|
||||||
|
'integracion':['Integración RTK + 4G','prod'],'ensamble':['Ensamble de unidad','prod']};
|
||||||
|
|
||||||
|
// lista de unidades: la flota + lo que el servidor ya tenga guardado
|
||||||
|
(async () => {
|
||||||
|
const vistas = new Set();
|
||||||
|
(window.FLOTA||[]).forEach(u => {
|
||||||
|
vistas.add(u.serie);
|
||||||
|
$('sel').append(el('option',{value:u.serie,text:u.serie+' — '+(u.maquina||'')}));
|
||||||
|
});
|
||||||
|
try{
|
||||||
|
const j = await (await fetch('/api/series')).json();
|
||||||
|
(j.series||[]).forEach(s => { if(!vistas.has(s.serie))
|
||||||
|
$('sel').append(el('option',{value:s.serie,text:s.serie+' ('+s.tipos.join(', ')+')'})); });
|
||||||
|
}catch(e){}
|
||||||
|
const q = new URLSearchParams(location.search).get('serie');
|
||||||
|
if(q){ $('sel').value=q; $('libre').value=q; cargar(q); }
|
||||||
|
})();
|
||||||
|
|
||||||
|
$('ir').addEventListener('click',()=>{ const s=($('libre').value||$('sel').value||'').trim().toUpperCase(); if(s) irA(s); });
|
||||||
|
$('sel').addEventListener('change',()=>{ if($('sel').value){ $('libre').value=''; irA($('sel').value); } });
|
||||||
|
$('libre').addEventListener('keydown',e=>{ if(e.key==='Enter') $('ir').click(); });
|
||||||
|
function irA(s){ history.replaceState(null,'','?serie='+encodeURIComponent(s)); cargar(s); }
|
||||||
|
|
||||||
|
async function detalle(tipo, serie, archivo, caja){
|
||||||
|
caja.textContent='Cargando…';
|
||||||
|
try{
|
||||||
|
const j = await (await fetch(`/api/acta?tipo=${encodeURIComponent(tipo)}&serie=${encodeURIComponent(serie)}&archivo=${encodeURIComponent(archivo)}`)).json();
|
||||||
|
if(!j.exists) throw new Error('no encontrada');
|
||||||
|
const t=el('table',{class:'det'}); const res=j.acta.respuestas||{};
|
||||||
|
Object.keys(res).forEach(k=>{
|
||||||
|
const v=res[k]; if(v.val===''||v.val===false||v.val==null) return;
|
||||||
|
t.append(el('tr',null, el('td',{class:'k',text:(v.lbl||k).replace(/<[^>]+>/g,'')}),
|
||||||
|
el('td',{text: v.val===true?'sí':String(v.val)})));
|
||||||
|
});
|
||||||
|
caja.innerHTML=''; caja.append(t.rows.length?t:el('div',{class:'vacio',text:'El acta no tiene respuestas guardadas.'}));
|
||||||
|
}catch(e){ caja.textContent='No se pudo cargar el detalle.'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function galeria(rutas){
|
||||||
|
const g=el('div',{class:'fotos'});
|
||||||
|
rutas.forEach(r=>{
|
||||||
|
const a=el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(r),target:'_blank'});
|
||||||
|
a.append(el('img',{src:'/api/adjunto?ruta='+encodeURIComponent(r),loading:'lazy'}));
|
||||||
|
g.append(a);
|
||||||
|
});
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(j){
|
||||||
|
cont.innerHTML='';
|
||||||
|
const u=(window.FLOTA||[]).find(x=>x.serie===j.serie);
|
||||||
|
$('titulo').textContent='Expediente · '+j.serie;
|
||||||
|
$('meta').innerHTML = u
|
||||||
|
? '<b>'+(u.maquina||'')+'</b> · '+(u.terminal||'')+(u.nota?'<br>'+u.nota:'')
|
||||||
|
: 'Esta serie no está en el registro de flota (flota.js).';
|
||||||
|
|
||||||
|
if(j.marca){
|
||||||
|
const m=j.marca_acta||{};
|
||||||
|
cont.append(el('div',{class:'err',style:j.marca==='BAJA'?'':'background:var(--aviso-bg);color:var(--aviso);border-color:var(--aviso)',
|
||||||
|
html:(j.marca==='BAJA'?'<b>Serie dada de BAJA</b>':'<b>Serie marcada como DAÑADA</b>')
|
||||||
|
+(m.fecha?' desde el '+m.fecha:'')+(m.clasificacion?' — '+m.clasificacion:'')+(m.resumen?'<br>'+m.resumen:'')
|
||||||
|
+(m.accion?'<br>Disposición: '+m.accion:'')}));
|
||||||
|
}
|
||||||
|
const kp=el('div',{class:'kpis'});
|
||||||
|
[[j.total,'Actas registradas'],[j.total_fotos,'Fotos'],[j.total_logs,'Archivos de log'],
|
||||||
|
[j.relacionadas.length,'Actas que la mencionan']]
|
||||||
|
.forEach(([n,l])=>kp.append(el('div',{class:'kpi'},el('div',{class:'n',text:String(n)}),el('div',{class:'l',text:l}))));
|
||||||
|
cont.append(kp);
|
||||||
|
|
||||||
|
// ---- trazabilidad: cadena de producción y cadena de campo ----
|
||||||
|
cont.append(trazabilidad(j));
|
||||||
|
|
||||||
|
// ---- línea de tiempo ----
|
||||||
|
const b=el('section',{class:'bloque'});
|
||||||
|
b.append(el('h2',{text:'Historial'}), el('p',{class:'sub',text:'Todo lo registrado para esta serie, de lo más reciente a lo más viejo.'}));
|
||||||
|
if(!j.actas.length) b.append(el('div',{class:'vacio',text:'Todavía no hay ninguna acta para esta unidad.'}));
|
||||||
|
else{
|
||||||
|
const lt=el('div',{class:'linea'});
|
||||||
|
j.actas.forEach(a=>{
|
||||||
|
const [nom,cls]=NOMBRE[a.tipo]||[a.tipo,'pend'];
|
||||||
|
const ev=el('div',{class:'ev '+cls});
|
||||||
|
const cab=el('div',{class:'cab'});
|
||||||
|
cab.append(el('div',null, el('div',{class:'t',text:nom}),
|
||||||
|
el('div',{class:'f',text:(a.fecha?a.fecha+' · ':'')+fecha(a.recibido)+' · '+(a.tecnico||'—')+(a.maquina?' · '+a.maquina:'')})));
|
||||||
|
const r=String(a.resultado||'');
|
||||||
|
cab.append(el('span',{class:'pill '+(r==='FALLA'?'falla':r?'ok':'pend'),text:r||'—'}));
|
||||||
|
ev.append(cab);
|
||||||
|
if(a.motivo) ev.append(el('div',{class:'cl',text:'Motivo: '+a.motivo}));
|
||||||
|
if(a.clasificacion) ev.append(el('div',{class:'cl',html:'<b>'+a.clasificacion+'</b>'}));
|
||||||
|
if(a.accion) ev.append(el('div',{class:'cl',text:'→ '+a.accion}));
|
||||||
|
if(a.fotos.length) ev.append(galeria(a.fotos));
|
||||||
|
if(a.archivos.length){
|
||||||
|
const g=el('div',{class:'logs'});
|
||||||
|
a.archivos.forEach(x=>g.append(el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(x.ruta),
|
||||||
|
target:'_blank',text:(x.nombre||'captura')})));
|
||||||
|
ev.append(g);
|
||||||
|
}
|
||||||
|
const det=el('details'); det.append(el('summary',{text:'Ver todas las respuestas'}));
|
||||||
|
const caja=el('div'); det.append(caja);
|
||||||
|
det.addEventListener('toggle',()=>{ if(det.open&&!caja.dataset.l){ caja.dataset.l='1'; detalle(a.tipo,j.serie,a.archivo,caja);} });
|
||||||
|
ev.append(det);
|
||||||
|
lt.append(ev);
|
||||||
|
});
|
||||||
|
b.append(lt);
|
||||||
|
}
|
||||||
|
cont.append(b);
|
||||||
|
|
||||||
|
// ---- todos los archivos ----
|
||||||
|
const f=el('section',{class:'bloque'});
|
||||||
|
f.append(el('h2',{text:'Archivos de log'}),
|
||||||
|
el('p',{class:'sub',text:'Todas las capturas subidas para esta serie, vengan del acta que vengan.'}));
|
||||||
|
if(!j.logs.length) f.append(el('div',{class:'vacio',text:'Sin archivos de log.'}));
|
||||||
|
else{
|
||||||
|
const g=el('div',{class:'logs'});
|
||||||
|
j.logs.forEach(l=>g.append(el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(l.ruta),target:'_blank',
|
||||||
|
text:l.nombre+' ('+kb(l.bytes)+')'})));
|
||||||
|
f.append(g);
|
||||||
|
}
|
||||||
|
cont.append(f);
|
||||||
|
|
||||||
|
const ph=el('section',{class:'bloque'});
|
||||||
|
ph.append(el('h2',{text:'Todas las fotos'}), el('p',{class:'sub',text:j.total_fotos+' foto(s) acumuladas.'}));
|
||||||
|
if(!j.fotos.length) ph.append(el('div',{class:'vacio',text:'Sin fotos.'}));
|
||||||
|
else ph.append(galeria(j.fotos.map(x=>x.ruta)));
|
||||||
|
cont.append(ph);
|
||||||
|
|
||||||
|
if(j.relacionadas.length){
|
||||||
|
const r=el('section',{class:'bloque'});
|
||||||
|
r.append(el('h2',{text:'Aparece como componente en'}),
|
||||||
|
el('p',{class:'sub',text:'Actas de otras unidades que incluyen esta serie entre sus módulos.'}));
|
||||||
|
j.relacionadas.forEach(a=>{
|
||||||
|
const [nom]=NOMBRE[a.tipo]||[a.tipo];
|
||||||
|
r.append(el('div',{class:'ev'},
|
||||||
|
el('div',{class:'t',html:nom+' — <a href="?serie='+encodeURIComponent(a.serie)+'">'+a.serie+'</a>'}),
|
||||||
|
el('div',{class:'f',text:fecha(a.recibido)+' · '+(a.tecnico||'—')+' · '+(a.resultado||'')})));
|
||||||
|
});
|
||||||
|
cont.append(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- trazabilidad ----------
|
||||||
|
// Cadena de producción (tarjeta → módulos → integración → ensamble → configuración) y cadena de
|
||||||
|
// campo (envío → recepción → instalación → diagnósticos → daños), con fecha y sitio. Para una unidad
|
||||||
|
// HBOX, las actas de sus módulos viven en OTRAS series (componentes del ensamble): se consultan aparte.
|
||||||
|
let SITIOS = null;
|
||||||
|
async function nombreSitio(id){
|
||||||
|
if(!id) return '';
|
||||||
|
if(!SITIOS){ try{ SITIOS = ((await (await fetch('/api/sitios?todos=1')).json()).sitios)||[]; }catch(e){ SITIOS=[]; } }
|
||||||
|
const s = SITIOS.find(x=>x.id===id); return s ? s.nombre : id;
|
||||||
|
}
|
||||||
|
function paso(nombre, cls, estado, detalle){
|
||||||
|
const p=el('div',{class:'paso '+cls}); p.append(el('div',{class:'n',text:nombre}), el('div',{class:'e',text:estado}));
|
||||||
|
if(detalle) p.append(el('div',{class:'d',html:detalle}));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
function ultima(actas, tipos){ return actas.filter(a=>tipos.includes(a.tipo))[0]||null; } // ya vienen de reciente a vieja
|
||||||
|
function pasoActa(nombre, a, textoOK, textoPend){
|
||||||
|
if(!a) return paso(nombre,'pend',textoPend||'sin acta');
|
||||||
|
const falla = a.resultado==='FALLA';
|
||||||
|
return paso(nombre, falla?'falla':'ok', falla?'Falla':(textoOK||(a.resultado||'OK')), (a.fecha||fecha(a.recibido).slice(0,10))+(a.tecnico?' · '+a.tecnico:''));
|
||||||
|
}
|
||||||
|
function trazabilidad(j){
|
||||||
|
const sec=el('section',{class:'bloque'});
|
||||||
|
sec.append(el('h2',{text:'Trazabilidad'}), el('p',{class:'sub',text:'De la tarjeta al puerto: cada paso con su acta, fecha y sitio. Gris = todavía no hay acta.'}));
|
||||||
|
const A=j.actas, esHbox=/^HBOX/i.test(j.serie);
|
||||||
|
// --- producción
|
||||||
|
sec.append(el('div',{class:'cad-t',text:'Producción'}));
|
||||||
|
const prod=el('div',{class:'cadena'}); sec.append(prod);
|
||||||
|
const ens=ultima(A,['ensamble']);
|
||||||
|
let comp=(ens&&ens.componentes)||{};
|
||||||
|
// sin acta de ensamble, los módulos se deducen del número de unidad: HBOX5-E02-A002-R0002 → V5-02 · ARD-002 · RTK-0002
|
||||||
|
const mSer=!comp.einar && j.serie.match(/^HBOX5-E(\d+)-A(\d+)-R(\d+)$/i);
|
||||||
|
if(mSer) comp={einar:'V5-'+mSer[1].padStart(2,'0'), ard:'ARD-'+mSer[2].padStart(3,'0'), modem:'RTK-'+mSer[3].padStart(4,'0')};
|
||||||
|
if(esHbox){
|
||||||
|
// módulos: se consultan por su serie
|
||||||
|
const m=[['Tarjeta EinarBoard','prueba-v5',comp.einar],['ArduSimple','modulo-ard',comp.ard],['Módem 4G','modulo-4g',comp.modem]];
|
||||||
|
const ph={}; m.forEach(([n,,s])=>{ const p=paso(n,'pend',s?'consultando…':'sin ensamble', s?'<a href="?serie='+encodeURIComponent(s)+'">'+s+'</a>':''); ph[n]=p; prod.append(p); });
|
||||||
|
const integ=paso('Integración','pend',comp.ard&&comp.modem?'consultando…':'—'); prod.append(integ);
|
||||||
|
(async()=>{
|
||||||
|
for(const [n,tipo,s] of m){
|
||||||
|
if(!s) continue;
|
||||||
|
let txt='sin acta', cls='pend';
|
||||||
|
try{ const r=await (await fetch(`/api/acta?tipo=${tipo}&serie=${encodeURIComponent(s)}`)).json();
|
||||||
|
if(r.exists){ cls=r.acta.resultado==='FALLA'?'falla':'ok'; txt=cls==='ok'?'Aprobada':'Falla'; ph[n].querySelector('.d').innerHTML+=' · '+(r.acta.fecha||fecha(r.acta._recibido).slice(0,10)); } }catch(e){}
|
||||||
|
ph[n].className='paso '+cls; ph[n].querySelector('.e').textContent=txt;
|
||||||
|
}
|
||||||
|
if(comp.ard&&comp.modem){
|
||||||
|
const si='INT-A'+(String(comp.ard).match(/(\d+)\s*$/)||[,''])[1]+'-R'+(String(comp.modem).match(/(\d+)\s*$/)||[,''])[1];
|
||||||
|
let txt='sin acta', cls='pend';
|
||||||
|
try{ const r=await (await fetch(`/api/acta?tipo=integracion&serie=${encodeURIComponent(si)}`)).json();
|
||||||
|
if(r.exists){ cls=r.acta.resultado==='FALLA'?'falla':'ok'; txt=cls==='ok'?'Aprobada':'Falla'; integ.append(el('div',{class:'d',html:'<a href="?serie='+encodeURIComponent(si)+'">'+si+'</a> · '+(r.acta.fecha||fecha(r.acta._recibido).slice(0,10))})); } }catch(e){}
|
||||||
|
integ.className='paso '+cls; integ.querySelector('.e').textContent=txt;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
prod.append(pasoActa('Ensamble',ens,'Unidad lista'));
|
||||||
|
prod.append(pasoActa('Configuración / pruebas',ultima(A,['reflasheo','prueba-ethernet-serie','prueba-bateria-modem']),null,'sin acta'));
|
||||||
|
} else {
|
||||||
|
// pieza suelta: su propia acta de prueba, y en qué unidad quedó
|
||||||
|
prod.append(pasoActa('Acta de la pieza',ultima(A,['prueba-v5','prueba-v4','modulo-ard','modulo-4g'])));
|
||||||
|
const integ=j.relacionadas.find(r=>r.tipo==='integracion'), ensR=j.relacionadas.find(r=>r.tipo==='ensamble');
|
||||||
|
prod.append(integ?paso('Integración',integ.resultado==='FALLA'?'falla':'ok',integ.resultado==='FALLA'?'Falla':'En pareja','<a href="?serie='+encodeURIComponent(integ.serie)+'">'+integ.serie+'</a>'):paso('Integración','pend','sin acta'));
|
||||||
|
prod.append(ensR?paso('Ensamble',ensR.resultado==='FALLA'?'falla':'info',ensR.resultado==='FALLA'?'Falla':'Montada en unidad','<a href="?serie='+encodeURIComponent(ensR.serie)+'">'+ensR.serie+'</a>'):paso('Ensamble','pend','libre (no está en una unidad)'));
|
||||||
|
prod.append(pasoActa('Banco / reflasheo',ultima(A,['diagnostico-banco','reflasheo']),null,'—'));
|
||||||
|
}
|
||||||
|
// --- campo
|
||||||
|
sec.append(el('div',{class:'cad-t',text:'Campo'}));
|
||||||
|
const campo=el('div',{class:'cadena'}); sec.append(campo);
|
||||||
|
const env=ultima(A,['envio']), rec=ultima(A,['recepcion']), ins=A.filter(a=>a.tipo==='instalacion-campo'&&a.resultado!=='FALLA')[0]||ultima(A,['instalacion-campo']);
|
||||||
|
const diags=A.filter(a=>a.tipo==='diagnostico-campo'), danos=A.filter(a=>a.tipo==='dano');
|
||||||
|
const conSitio=async(p,a)=>{ if(a&&a.sitio){ const n=await nombreSitio(a.sitio); let d=p.querySelector('.d'); if(!d){ d=el('div',{class:'d'}); p.append(d); } d.innerHTML+=' · <a href="/tableros/sitio/?id='+encodeURIComponent(a.sitio)+'">'+n+'</a>'; } };
|
||||||
|
const pEnv=env?paso('Envío',env.accion&&/detenida/i.test(env.accion)?'info':'ok',env.accion||'Enviada',(env.fecha||'')+(env.clasificacion?' · '+env.clasificacion:'')):paso('Envío','pend','sin acta'); campo.append(pEnv); conSitio(pEnv,env);
|
||||||
|
const pRec=rec?paso('Recepción',rec.resultado==='FALLA'?'falla':'ok',rec.accion||rec.resultado||'Recibida',(rec.fecha||'')+(rec.clasificacion?' · '+rec.clasificacion:'')):paso('Recepción','pend','sin acta'); campo.append(pRec); conSitio(pRec,rec);
|
||||||
|
const pIns=ins?paso('Instalación',ins.resultado==='FALLA'?'falla':'ok',ins.accion||(ins.resultado==='FALLA'?'Falla':'Instalada'),(ins.fecha||'')+(ins.maquina?' · '+ins.maquina:'')):paso('Instalación','pend','sin acta','<a href="/formularios/instalacion.html">llenar acta</a>'); campo.append(pIns); conSitio(pIns,ins);
|
||||||
|
const pDg=diags.length?paso('Diagnósticos',diags[0].resultado==='FALLA'?'falla':'info',diags.length+' visita(s)',(diags[0].fecha||'')+(diags[0].accion?' · '+diags[0].accion:'')):paso('Diagnósticos','pend','ninguno'); campo.append(pDg); conSitio(pDg,diags[0]);
|
||||||
|
const pDn=j.marca?paso('Daños',j.marca==='BAJA'?'falla':'falla',j.marca==='BAJA'?'Baja':'Dañada',(j.marca_acta&&j.marca_acta.fecha)||''):(danos.length?paso('Daños','ok','reparada / sin marca',danos.length+' acta(s)'):paso('Daños','ok','sin daños')); campo.append(pDn); conSitio(pDn,danos[0]);
|
||||||
|
return sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cargar(serie){
|
||||||
|
cont.innerHTML='<p class="vacio">Cargando…</p>';
|
||||||
|
try{
|
||||||
|
const j=await (await fetch('/api/unidad?serie='+encodeURIComponent(serie),{cache:'no-store'})).json();
|
||||||
|
if(!j.ok) throw new Error(j.error||'error');
|
||||||
|
render(j);
|
||||||
|
}catch(e){
|
||||||
|
cont.innerHTML='';
|
||||||
|
cont.append(el('div',{class:'err',text:'No se pudo leer el expediente: '+e.message}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@ -4,7 +4,7 @@ title: "Taller"
|
|||||||
|
|
||||||
# Taller
|
# Taller
|
||||||
|
|
||||||
Unidad o tarjeta **en la mesa**: llegó del campo, falló en producción o hay que validar un firmware. Antes de tocarla, abre su [expediente](../unidad.html) y lee las actas anteriores. Reglas de banco (límite de corriente, 5 V ≥ 5.2 V o paquete conectado, baterías hinchadas): [diagnóstico en banco](../procedimientos/diagnostico-banco.md#reglas-de-banco).
|
Unidad o tarjeta **en la mesa**: llegó del campo, falló en producción o hay que validar un firmware. Antes de tocarla, abre su [expediente](../tableros/unidad.md) y lee las actas anteriores. Reglas de banco (límite de corriente, 5 V ≥ 5.2 V o paquete conectado, baterías hinchadas): [diagnóstico en banco](../procedimientos/diagnostico-banco.md#reglas-de-banco).
|
||||||
|
|
||||||
## Qué trabajo, qué procedimiento, qué acta
|
## Qué trabajo, qué procedimiento, qué acta
|
||||||
|
|
||||||
@ -30,5 +30,5 @@ Por tarjeta, nunca por número de versión. **0.6.0** es el de campo para V4 y V
|
|||||||
## Referencias de la mesa
|
## Referencias de la mesa
|
||||||
|
|
||||||
- [Manual V5](../manuales/manual-tarjeta-V5.md) · [Manual V4](../manuales/manual-tarjeta-V4.md) · [Manual V3](../manuales/manual-tarjeta-V3.md) · [Pinout V5.6](../hardware/v56-pinout.md) · [Hoja de datos V5.6](../hardware/v56-datasheet.md)
|
- [Manual V5](../manuales/manual-tarjeta-V5.md) · [Manual V4](../manuales/manual-tarjeta-V4.md) · [Manual V3](../manuales/manual-tarjeta-V3.md) · [Pinout V5.6](../hardware/v56-pinout.md) · [Hoja de datos V5.6](../hardware/v56-datasheet.md)
|
||||||
- [Estado de actas](../tablero.html) · [Expediente de una unidad](../unidad.html) · [Kanban](../tableros/kanban.html) (pista Producción)
|
- [Estado de actas](../tableros/actas.md) · [Expediente de una unidad](../tableros/unidad.md) · [Pendientes](../tableros/pendientes.md) (pista Producción)
|
||||||
- Procedimientos anteriores (V3, protoboard, fan v3.0.3): [Referencia e histórico](../historia/index.md)
|
- Procedimientos anteriores (V3, protoboard, fan v3.0.3): [Referencia e histórico](../historia/index.md)
|
||||||
|
|||||||
341
docs/unidad.html
@ -1,341 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
<!-- Desde el 20-oct-2026 este tablero vive dentro del sitio: docs/tableros/unidad.md → /tableros/unidad/
|
||||||
|
Este archivo solo redirige (conserva ?serie=, ?id=, ?pista=… y #…) para que las ligas viejas
|
||||||
|
guardadas en pendientes y actas sigan sirviendo. No se edita: los cambios van en tableros/unidad.md. -->
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Expediente de la unidad — HBOX</title>
|
<meta name="robots" content="noindex">
|
||||||
<style>
|
<title>Expediente de unidad - Drogheda</title>
|
||||||
:root{
|
<script>location.replace('/tableros/unidad/' + location.search + location.hash);</script>
|
||||||
--naranja:#e8590c; --tinta:#1d1c19; --tinta2:#5a564d; --linea:#e6e1d6;
|
<meta http-equiv="refresh" content="0; url=/tableros/unidad/">
|
||||||
--papel:#faf8f4; --tarjeta:#fff; --superf:#fcfcfb;
|
<style>body{margin:0;padding:24px;background:#0E1524;color:#E7ECF5;font:16px/1.5 system-ui,sans-serif}a{color:#7FE9D8}</style>
|
||||||
--ok:#2f9e44; --ok-bg:#ebfbee; --falla:#e03131; --falla-bg:#fff5f5;
|
|
||||||
--pend:#adb5bd; --pend-bg:#f4f2ee; --info:#1971c2; --info-bg:#e7f1fb;
|
|
||||||
}
|
|
||||||
@media(prefers-color-scheme:dark){:root:not([data-theme=light]){
|
|
||||||
--papel:#191b1e;--tinta:#e6e1d6;--tinta2:#a09a8d;--linea:#33363b;--tarjeta:#1f2226;--superf:#1b1d20;
|
|
||||||
--ok:#69db7c;--ok-bg:#1b2a1f;--falla:#ff8787;--falla-bg:#2c1d1d;--pend:#5c636a;--pend-bg:#26282c;--info:#74c0fc;--info-bg:#182734;}}
|
|
||||||
*{box-sizing:border-box}
|
|
||||||
body{margin:0;background:var(--papel);color:var(--tinta);font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
||||||
.wrap{max-width:1020px;margin:0 auto;padding:18px 16px 70px}
|
|
||||||
header.top{background:#2C3C5B;color:#fff;padding:16px;border-radius:12px;margin-bottom:16px}
|
|
||||||
header.top h1{margin:0;font-size:21px}
|
|
||||||
header.top .meta{color:#d7d2c8;font-size:13.5px;margin-top:4px}
|
|
||||||
header.top a{color:#fff;font-size:13px;text-decoration:none;opacity:.85;margin-right:14px}
|
|
||||||
.buscar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:16px}
|
|
||||||
select,input{padding:10px 12px;border:1px solid var(--linea);border-radius:9px;font-size:15px;
|
|
||||||
background:var(--tarjeta);color:var(--tinta)}
|
|
||||||
button{background:var(--naranja);color:#fff;border:0;border-radius:9px;padding:10px 18px;font-size:14px;font-weight:600;cursor:pointer}
|
|
||||||
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin-bottom:18px}
|
|
||||||
.kpi{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:14px 16px}
|
|
||||||
.kpi .n{font-size:28px;font-weight:700;line-height:1}
|
|
||||||
.kpi .l{color:var(--tinta2);font-size:12.5px;margin-top:4px}
|
|
||||||
section.bloque{background:var(--tarjeta);border:1px solid var(--linea);border-radius:12px;padding:16px;margin:14px 0}
|
|
||||||
section.bloque h2{margin:0 0 4px;font-size:17px}
|
|
||||||
section.bloque .sub{color:var(--tinta2);font-size:13.5px;margin:0 0 12px}
|
|
||||||
.linea{position:relative;padding-left:20px;border-left:2px solid var(--linea);margin-left:6px}
|
|
||||||
.ev{position:relative;padding:12px 0 12px 4px;border-bottom:1px solid var(--linea)}
|
|
||||||
.ev:last-child{border-bottom:0}
|
|
||||||
.ev::before{content:"";position:absolute;left:-27px;top:18px;width:11px;height:11px;border-radius:50%;
|
|
||||||
background:var(--pend);border:2px solid var(--papel)}
|
|
||||||
.ev.campo::before{background:var(--naranja)} .ev.banco::before{background:var(--info)}
|
|
||||||
.ev.flash::before{background:#7048e8} .ev.prod::before{background:var(--ok)} .ev.falla::before{background:var(--falla)}
|
|
||||||
.ev .cab{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline;justify-content:space-between}
|
|
||||||
.ev .t{font-weight:700;font-size:15px}
|
|
||||||
.ev .f{color:var(--tinta2);font-size:12.5px}
|
|
||||||
.ev .cl{margin-top:3px}
|
|
||||||
.pill{font-size:11px;font-weight:700;padding:2px 9px;border-radius:99px;white-space:nowrap}
|
|
||||||
.pill.ok{background:var(--ok-bg);color:var(--ok)}
|
|
||||||
.pill.falla{background:var(--falla-bg);color:var(--falla)}
|
|
||||||
.pill.info{background:var(--info-bg);color:var(--info)}
|
|
||||||
.pill.pend{background:var(--pend-bg);color:var(--tinta2)}
|
|
||||||
.fotos{display:flex;flex-wrap:wrap;gap:8px;margin-top:9px}
|
|
||||||
.fotos a{display:block;width:78px;height:78px;border-radius:8px;overflow:hidden;border:1px solid var(--linea)}
|
|
||||||
.fotos img{width:100%;height:100%;object-fit:cover;display:block;background:var(--pend-bg)}
|
|
||||||
.logs{display:flex;flex-wrap:wrap;gap:8px;margin-top:9px}
|
|
||||||
.logs a{font-size:12.5px;font-weight:600;color:var(--info);text-decoration:none;
|
|
||||||
border:1px solid var(--linea);border-radius:7px;padding:5px 10px;background:var(--superf)}
|
|
||||||
details{margin-top:8px}
|
|
||||||
summary{cursor:pointer;color:var(--naranja);font-size:13px;font-weight:600}
|
|
||||||
table.det{border-collapse:collapse;width:100%;font-size:13px;margin-top:8px}
|
|
||||||
table.det td{padding:5px 8px 5px 0;border-bottom:1px solid var(--linea);vertical-align:top}
|
|
||||||
table.det td.k{color:var(--tinta2);width:46%}
|
|
||||||
.vacio{color:var(--tinta2);font-style:italic;font-size:14px}
|
|
||||||
.err{background:var(--falla-bg);color:var(--falla);border:1px solid var(--falla);padding:10px 14px;border-radius:10px}
|
|
||||||
/* cadenas de trazabilidad: producción → campo */
|
|
||||||
.cadena{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0 4px}
|
|
||||||
.cadena .paso{flex:1 1 118px;min-width:118px;border:1px solid var(--linea);border-radius:9px;padding:7px 9px;background:var(--superf);font-size:12.5px;position:relative}
|
|
||||||
.cadena .paso .n{font-weight:700;font-size:13px}
|
|
||||||
.cadena .paso .e{margin-top:2px}
|
|
||||||
.cadena .paso .d{color:var(--tinta2);font-size:11.5px;margin-top:2px;line-height:1.3}
|
|
||||||
.cadena .paso.ok{border-color:var(--ok);background:var(--ok-bg)} .cadena .paso.ok .e{color:var(--ok)}
|
|
||||||
.cadena .paso.falla{border-color:var(--falla);background:var(--falla-bg)} .cadena .paso.falla .e{color:var(--falla)}
|
|
||||||
.cadena .paso.info{border-color:var(--info);background:var(--info-bg)} .cadena .paso.info .e{color:var(--info)}
|
|
||||||
.cadena .paso.pend .e{color:var(--tinta2)}
|
|
||||||
.cadena .paso a{color:inherit}
|
|
||||||
.cad-t{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--tinta2);margin:8px 0 0}
|
|
||||||
@media print{header.top{background:#fff;color:#000} .buscar,button{display:none}}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<p>Este tablero ahora está dentro del sitio: <a href="/tableros/unidad/">Expediente de unidad</a>.</p>
|
||||||
<header class="top">
|
|
||||||
<div><a href="./">← Documentación</a><a href="diagnostico/tablero-campo.html">Tablero de campo</a>
|
|
||||||
<a href="#" onclick="window.print();return false">Imprimir</a></div>
|
|
||||||
<h1 id="titulo">Expediente de la unidad</h1>
|
|
||||||
<div class="meta" id="meta">Elige una unidad para ver todo su historial.</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="buscar">
|
|
||||||
<select id="sel"><option value="">— elegir unidad —</option></select>
|
|
||||||
<input id="libre" placeholder="…o escribe una serie: RTK-014, ARD-007, V5-03">
|
|
||||||
<button id="ir">Ver expediente</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="cont"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="formularios/flota.js"></script>
|
|
||||||
<script>
|
|
||||||
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=>e.append(c)); return e;};
|
|
||||||
const $=i=>document.getElementById(i);
|
|
||||||
const cont=$('cont');
|
|
||||||
const fecha=s=>(s||'').replace('T',' ').slice(0,16);
|
|
||||||
const kb=n=>n>1048576?(n/1048576).toFixed(1)+' MB':Math.max(1,Math.round(n/1024))+' kB';
|
|
||||||
|
|
||||||
const NOMBRE={'diagnostico-campo':['Diagnóstico en campo','campo'],'diagnostico-banco':['Diagnóstico en banco','banco'],
|
|
||||||
'reflasheo':['Reflasheo','flash'],'instalacion-campo':['Instalación en sitio','campo'],
|
|
||||||
'dano':['⚡ Daño o incidente','falla'],'envio':['📦 Envío','prod'],'recepcion':['📦 Recepción','prod'],
|
|
||||||
'prueba-v5':['Prueba de tarjeta V5','prod'],'prueba-v4':['Prueba de tarjeta V4','prod'],
|
|
||||||
'modulo-ard':['Módulo ArduSimple','prod'],'modulo-4g':['Módem 4G','prod'],
|
|
||||||
'integracion':['Integración RTK + 4G','prod'],'ensamble':['Ensamble de unidad','prod']};
|
|
||||||
|
|
||||||
// lista de unidades: la flota + lo que el servidor ya tenga guardado
|
|
||||||
(async () => {
|
|
||||||
const vistas = new Set();
|
|
||||||
(window.FLOTA||[]).forEach(u => {
|
|
||||||
vistas.add(u.serie);
|
|
||||||
$('sel').append(el('option',{value:u.serie,text:u.serie+' — '+(u.maquina||'')}));
|
|
||||||
});
|
|
||||||
try{
|
|
||||||
const j = await (await fetch('/api/series')).json();
|
|
||||||
(j.series||[]).forEach(s => { if(!vistas.has(s.serie))
|
|
||||||
$('sel').append(el('option',{value:s.serie,text:s.serie+' ('+s.tipos.join(', ')+')'})); });
|
|
||||||
}catch(e){}
|
|
||||||
const q = new URLSearchParams(location.search).get('serie');
|
|
||||||
if(q){ $('sel').value=q; $('libre').value=q; cargar(q); }
|
|
||||||
})();
|
|
||||||
|
|
||||||
$('ir').addEventListener('click',()=>{ const s=($('libre').value||$('sel').value||'').trim().toUpperCase(); if(s) irA(s); });
|
|
||||||
$('sel').addEventListener('change',()=>{ if($('sel').value){ $('libre').value=''; irA($('sel').value); } });
|
|
||||||
$('libre').addEventListener('keydown',e=>{ if(e.key==='Enter') $('ir').click(); });
|
|
||||||
function irA(s){ history.replaceState(null,'','?serie='+encodeURIComponent(s)); cargar(s); }
|
|
||||||
|
|
||||||
async function detalle(tipo, serie, archivo, caja){
|
|
||||||
caja.textContent='Cargando…';
|
|
||||||
try{
|
|
||||||
const j = await (await fetch(`/api/acta?tipo=${encodeURIComponent(tipo)}&serie=${encodeURIComponent(serie)}&archivo=${encodeURIComponent(archivo)}`)).json();
|
|
||||||
if(!j.exists) throw new Error('no encontrada');
|
|
||||||
const t=el('table',{class:'det'}); const res=j.acta.respuestas||{};
|
|
||||||
Object.keys(res).forEach(k=>{
|
|
||||||
const v=res[k]; if(v.val===''||v.val===false||v.val==null) return;
|
|
||||||
t.append(el('tr',null, el('td',{class:'k',text:(v.lbl||k).replace(/<[^>]+>/g,'')}),
|
|
||||||
el('td',{text: v.val===true?'✓':String(v.val)})));
|
|
||||||
});
|
|
||||||
caja.innerHTML=''; caja.append(t.rows.length?t:el('div',{class:'vacio',text:'El acta no tiene respuestas guardadas.'}));
|
|
||||||
}catch(e){ caja.textContent='No se pudo cargar el detalle.'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function galeria(rutas){
|
|
||||||
const g=el('div',{class:'fotos'});
|
|
||||||
rutas.forEach(r=>{
|
|
||||||
const a=el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(r),target:'_blank'});
|
|
||||||
a.append(el('img',{src:'/api/adjunto?ruta='+encodeURIComponent(r),loading:'lazy'}));
|
|
||||||
g.append(a);
|
|
||||||
});
|
|
||||||
return g;
|
|
||||||
}
|
|
||||||
|
|
||||||
function render(j){
|
|
||||||
cont.innerHTML='';
|
|
||||||
const u=(window.FLOTA||[]).find(x=>x.serie===j.serie);
|
|
||||||
$('titulo').textContent='Expediente · '+j.serie;
|
|
||||||
$('meta').innerHTML = u
|
|
||||||
? '<b>'+(u.maquina||'')+'</b> · '+(u.terminal||'')+(u.nota?'<br>'+u.nota:'')
|
|
||||||
: 'Esta serie no está en el registro de flota (flota.js).';
|
|
||||||
|
|
||||||
if(j.marca){
|
|
||||||
const m=j.marca_acta||{};
|
|
||||||
cont.append(el('div',{class:'err',style:j.marca==='BAJA'?'':'background:#fff9db;color:#7a4a00;border-color:#f08c00',
|
|
||||||
html:(j.marca==='BAJA'?'⛔ <b>Serie dada de BAJA</b>':'⚠ <b>Serie marcada como DAÑADA</b>')
|
|
||||||
+(m.fecha?' desde el '+m.fecha:'')+(m.clasificacion?' — '+m.clasificacion:'')+(m.resumen?'<br>'+m.resumen:'')
|
|
||||||
+(m.accion?'<br>Disposición: '+m.accion:'')}));
|
|
||||||
}
|
|
||||||
const kp=el('div',{class:'kpis'});
|
|
||||||
[[j.total,'Actas registradas'],[j.total_fotos,'Fotos'],[j.total_logs,'Archivos de log'],
|
|
||||||
[j.relacionadas.length,'Actas que la mencionan']]
|
|
||||||
.forEach(([n,l])=>kp.append(el('div',{class:'kpi'},el('div',{class:'n',text:String(n)}),el('div',{class:'l',text:l}))));
|
|
||||||
cont.append(kp);
|
|
||||||
|
|
||||||
// ---- trazabilidad: cadena de producción y cadena de campo ----
|
|
||||||
cont.append(trazabilidad(j));
|
|
||||||
|
|
||||||
// ---- línea de tiempo ----
|
|
||||||
const b=el('section',{class:'bloque'});
|
|
||||||
b.append(el('h2',{text:'Historial'}), el('p',{class:'sub',text:'Todo lo registrado para esta serie, de lo más reciente a lo más viejo.'}));
|
|
||||||
if(!j.actas.length) b.append(el('div',{class:'vacio',text:'Todavía no hay ninguna acta para esta unidad.'}));
|
|
||||||
else{
|
|
||||||
const lt=el('div',{class:'linea'});
|
|
||||||
j.actas.forEach(a=>{
|
|
||||||
const [nom,cls]=NOMBRE[a.tipo]||[a.tipo,'pend'];
|
|
||||||
const ev=el('div',{class:'ev '+cls});
|
|
||||||
const cab=el('div',{class:'cab'});
|
|
||||||
cab.append(el('div',null, el('div',{class:'t',text:nom}),
|
|
||||||
el('div',{class:'f',text:(a.fecha?a.fecha+' · ':'')+fecha(a.recibido)+' · '+(a.tecnico||'—')+(a.maquina?' · '+a.maquina:'')})));
|
|
||||||
const r=String(a.resultado||'');
|
|
||||||
cab.append(el('span',{class:'pill '+(r==='FALLA'?'falla':r?'ok':'pend'),text:r||'—'}));
|
|
||||||
ev.append(cab);
|
|
||||||
if(a.motivo) ev.append(el('div',{class:'cl',text:'Motivo: '+a.motivo}));
|
|
||||||
if(a.clasificacion) ev.append(el('div',{class:'cl',html:'<b>'+a.clasificacion+'</b>'}));
|
|
||||||
if(a.accion) ev.append(el('div',{class:'cl',text:'→ '+a.accion}));
|
|
||||||
if(a.fotos.length) ev.append(galeria(a.fotos));
|
|
||||||
if(a.archivos.length){
|
|
||||||
const g=el('div',{class:'logs'});
|
|
||||||
a.archivos.forEach(x=>g.append(el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(x.ruta),
|
|
||||||
target:'_blank',text:'📄 '+(x.nombre||'captura')})));
|
|
||||||
ev.append(g);
|
|
||||||
}
|
|
||||||
const det=el('details'); det.append(el('summary',{text:'Ver todas las respuestas'}));
|
|
||||||
const caja=el('div'); det.append(caja);
|
|
||||||
det.addEventListener('toggle',()=>{ if(det.open&&!caja.dataset.l){ caja.dataset.l='1'; detalle(a.tipo,j.serie,a.archivo,caja);} });
|
|
||||||
ev.append(det);
|
|
||||||
lt.append(ev);
|
|
||||||
});
|
|
||||||
b.append(lt);
|
|
||||||
}
|
|
||||||
cont.append(b);
|
|
||||||
|
|
||||||
// ---- todos los archivos ----
|
|
||||||
const f=el('section',{class:'bloque'});
|
|
||||||
f.append(el('h2',{text:'Archivos de log'}),
|
|
||||||
el('p',{class:'sub',text:'Todas las capturas subidas para esta serie, vengan del acta que vengan.'}));
|
|
||||||
if(!j.logs.length) f.append(el('div',{class:'vacio',text:'Sin archivos de log.'}));
|
|
||||||
else{
|
|
||||||
const g=el('div',{class:'logs'});
|
|
||||||
j.logs.forEach(l=>g.append(el('a',{href:'/api/adjunto?ruta='+encodeURIComponent(l.ruta),target:'_blank',
|
|
||||||
text:'📄 '+l.nombre+' ('+kb(l.bytes)+')'})));
|
|
||||||
f.append(g);
|
|
||||||
}
|
|
||||||
cont.append(f);
|
|
||||||
|
|
||||||
const ph=el('section',{class:'bloque'});
|
|
||||||
ph.append(el('h2',{text:'Todas las fotos'}), el('p',{class:'sub',text:j.total_fotos+' foto(s) acumuladas.'}));
|
|
||||||
if(!j.fotos.length) ph.append(el('div',{class:'vacio',text:'Sin fotos.'}));
|
|
||||||
else ph.append(galeria(j.fotos.map(x=>x.ruta)));
|
|
||||||
cont.append(ph);
|
|
||||||
|
|
||||||
if(j.relacionadas.length){
|
|
||||||
const r=el('section',{class:'bloque'});
|
|
||||||
r.append(el('h2',{text:'Aparece como componente en'}),
|
|
||||||
el('p',{class:'sub',text:'Actas de otras unidades que incluyen esta serie entre sus módulos.'}));
|
|
||||||
j.relacionadas.forEach(a=>{
|
|
||||||
const [nom]=NOMBRE[a.tipo]||[a.tipo];
|
|
||||||
r.append(el('div',{class:'ev'},
|
|
||||||
el('div',{class:'t',html:nom+' — <a href="?serie='+encodeURIComponent(a.serie)+'">'+a.serie+'</a>'}),
|
|
||||||
el('div',{class:'f',text:fecha(a.recibido)+' · '+(a.tecnico||'—')+' · '+(a.resultado||'')})));
|
|
||||||
});
|
|
||||||
cont.append(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- trazabilidad ----------
|
|
||||||
// Cadena de producción (tarjeta → módulos → integración → ensamble → configuración) y cadena de
|
|
||||||
// campo (envío → recepción → instalación → diagnósticos → daños), con fecha y sitio. Para una unidad
|
|
||||||
// HBOX, las actas de sus módulos viven en OTRAS series (componentes del ensamble): se consultan aparte.
|
|
||||||
let SITIOS = null;
|
|
||||||
async function nombreSitio(id){
|
|
||||||
if(!id) return '';
|
|
||||||
if(!SITIOS){ try{ SITIOS = ((await (await fetch('/api/sitios?todos=1')).json()).sitios)||[]; }catch(e){ SITIOS=[]; } }
|
|
||||||
const s = SITIOS.find(x=>x.id===id); return s ? s.nombre : id;
|
|
||||||
}
|
|
||||||
function paso(nombre, cls, estado, detalle){
|
|
||||||
const p=el('div',{class:'paso '+cls}); p.append(el('div',{class:'n',text:nombre}), el('div',{class:'e',text:estado}));
|
|
||||||
if(detalle) p.append(el('div',{class:'d',html:detalle}));
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
function ultima(actas, tipos){ return actas.filter(a=>tipos.includes(a.tipo))[0]||null; } // ya vienen de reciente a vieja
|
|
||||||
function pasoActa(nombre, a, textoOK, textoPend){
|
|
||||||
if(!a) return paso(nombre,'pend',textoPend||'sin acta');
|
|
||||||
const falla = a.resultado==='FALLA';
|
|
||||||
return paso(nombre, falla?'falla':'ok', falla?'✕ FALLA':(textoOK||'✓ '+(a.resultado||'OK')), (a.fecha||fecha(a.recibido).slice(0,10))+(a.tecnico?' · '+a.tecnico:''));
|
|
||||||
}
|
|
||||||
function trazabilidad(j){
|
|
||||||
const sec=el('section',{class:'bloque'});
|
|
||||||
sec.append(el('h2',{text:'Trazabilidad'}), el('p',{class:'sub',text:'De la tarjeta al puerto: cada paso con su acta, fecha y sitio. Gris = todavía no hay acta.'}));
|
|
||||||
const A=j.actas, esHbox=/^HBOX/i.test(j.serie);
|
|
||||||
// --- producción
|
|
||||||
sec.append(el('div',{class:'cad-t',text:'Producción'}));
|
|
||||||
const prod=el('div',{class:'cadena'}); sec.append(prod);
|
|
||||||
const ens=ultima(A,['ensamble']);
|
|
||||||
let comp=(ens&&ens.componentes)||{};
|
|
||||||
// sin acta de ensamble, los módulos se deducen del número de unidad: HBOX5-E02-A002-R0002 → V5-02 · ARD-002 · RTK-0002
|
|
||||||
const mSer=!comp.einar && j.serie.match(/^HBOX5-E(\d+)-A(\d+)-R(\d+)$/i);
|
|
||||||
if(mSer) comp={einar:'V5-'+mSer[1].padStart(2,'0'), ard:'ARD-'+mSer[2].padStart(3,'0'), modem:'RTK-'+mSer[3].padStart(4,'0')};
|
|
||||||
if(esHbox){
|
|
||||||
// módulos: se consultan por su serie
|
|
||||||
const m=[['Tarjeta EinarBoard','prueba-v5',comp.einar],['ArduSimple','modulo-ard',comp.ard],['Módem 4G','modulo-4g',comp.modem]];
|
|
||||||
const ph={}; m.forEach(([n,,s])=>{ const p=paso(n,'pend',s?'consultando…':'sin ensamble', s?'<a href="?serie='+encodeURIComponent(s)+'">'+s+'</a>':''); ph[n]=p; prod.append(p); });
|
|
||||||
const integ=paso('Integración','pend',comp.ard&&comp.modem?'consultando…':'—'); prod.append(integ);
|
|
||||||
(async()=>{
|
|
||||||
for(const [n,tipo,s] of m){
|
|
||||||
if(!s) continue;
|
|
||||||
let txt='sin acta', cls='pend';
|
|
||||||
try{ const r=await (await fetch(`/api/acta?tipo=${tipo}&serie=${encodeURIComponent(s)}`)).json();
|
|
||||||
if(r.exists){ cls=r.acta.resultado==='FALLA'?'falla':'ok'; txt=cls==='ok'?'✓ aprobada':'✕ FALLA'; ph[n].querySelector('.d').innerHTML+=' · '+(r.acta.fecha||fecha(r.acta._recibido).slice(0,10)); } }catch(e){}
|
|
||||||
ph[n].className='paso '+cls; ph[n].querySelector('.e').textContent=txt;
|
|
||||||
}
|
|
||||||
if(comp.ard&&comp.modem){
|
|
||||||
const si='INT-A'+(String(comp.ard).match(/(\d+)\s*$/)||[,''])[1]+'-R'+(String(comp.modem).match(/(\d+)\s*$/)||[,''])[1];
|
|
||||||
let txt='sin acta', cls='pend';
|
|
||||||
try{ const r=await (await fetch(`/api/acta?tipo=integracion&serie=${encodeURIComponent(si)}`)).json();
|
|
||||||
if(r.exists){ cls=r.acta.resultado==='FALLA'?'falla':'ok'; txt=cls==='ok'?'✓ aprobada':'✕ FALLA'; integ.append(el('div',{class:'d',html:'<a href="?serie='+encodeURIComponent(si)+'">'+si+'</a> · '+(r.acta.fecha||fecha(r.acta._recibido).slice(0,10))})); } }catch(e){}
|
|
||||||
integ.className='paso '+cls; integ.querySelector('.e').textContent=txt;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
prod.append(pasoActa('Ensamble',ens,'✓ unidad lista'));
|
|
||||||
prod.append(pasoActa('Configuración / pruebas',ultima(A,['reflasheo','prueba-ethernet-serie','prueba-bateria-modem']),null,'sin acta'));
|
|
||||||
} else {
|
|
||||||
// pieza suelta: su propia acta de prueba, y en qué unidad quedó
|
|
||||||
prod.append(pasoActa('Acta de la pieza',ultima(A,['prueba-v5','prueba-v4','modulo-ard','modulo-4g'])));
|
|
||||||
const integ=j.relacionadas.find(r=>r.tipo==='integracion'), ensR=j.relacionadas.find(r=>r.tipo==='ensamble');
|
|
||||||
prod.append(integ?paso('Integración',integ.resultado==='FALLA'?'falla':'ok',integ.resultado==='FALLA'?'✕ FALLA':'✓ en pareja','<a href="?serie='+encodeURIComponent(integ.serie)+'">'+integ.serie+'</a>'):paso('Integración','pend','sin acta'));
|
|
||||||
prod.append(ensR?paso('Ensamble',ensR.resultado==='FALLA'?'falla':'info',ensR.resultado==='FALLA'?'✕ FALLA':'▣ montada en unidad','<a href="?serie='+encodeURIComponent(ensR.serie)+'">'+ensR.serie+'</a>'):paso('Ensamble','pend','libre (no está en una unidad)'));
|
|
||||||
prod.append(pasoActa('Banco / reflasheo',ultima(A,['diagnostico-banco','reflasheo']),null,'—'));
|
|
||||||
}
|
|
||||||
// --- campo
|
|
||||||
sec.append(el('div',{class:'cad-t',text:'Campo'}));
|
|
||||||
const campo=el('div',{class:'cadena'}); sec.append(campo);
|
|
||||||
const env=ultima(A,['envio']), rec=ultima(A,['recepcion']), ins=A.filter(a=>a.tipo==='instalacion-campo'&&a.resultado!=='FALLA')[0]||ultima(A,['instalacion-campo']);
|
|
||||||
const diags=A.filter(a=>a.tipo==='diagnostico-campo'), danos=A.filter(a=>a.tipo==='dano');
|
|
||||||
const conSitio=async(p,a)=>{ if(a&&a.sitio){ const n=await nombreSitio(a.sitio); p.querySelector('.d').innerHTML+=' · <a href="sitios/sitio.html?id='+encodeURIComponent(a.sitio)+'">'+n+'</a>'; } };
|
|
||||||
const pEnv=env?paso('Envío',env.accion&&/detenida/i.test(env.accion)?'info':'ok',env.accion||'✓ enviada',(env.fecha||'')+(env.clasificacion?' · '+env.clasificacion:'')):paso('Envío','pend','sin acta'); campo.append(pEnv); conSitio(pEnv,env);
|
|
||||||
const pRec=rec?paso('Recepción',rec.resultado==='FALLA'?'falla':'ok',rec.accion||rec.resultado||'✓',(rec.fecha||'')+(rec.clasificacion?' · '+rec.clasificacion:'')):paso('Recepción','pend','sin acta'); campo.append(pRec); conSitio(pRec,rec);
|
|
||||||
const pIns=ins?paso('Instalación',ins.resultado==='FALLA'?'falla':'ok',ins.accion||(ins.resultado==='FALLA'?'✕ FALLA':'✓ instalada'),(ins.fecha||'')+(ins.maquina?' · '+ins.maquina:'')):paso('Instalación','pend','sin acta','<a href="formularios/instalacion.html">llenar acta</a>'); campo.append(pIns); conSitio(pIns,ins);
|
|
||||||
const pDg=diags.length?paso('Diagnósticos',diags[0].resultado==='FALLA'?'falla':'info',diags.length+' visita(s)',(diags[0].fecha||'')+(diags[0].accion?' · '+diags[0].accion:'')):paso('Diagnósticos','pend','ninguno'); campo.append(pDg); conSitio(pDg,diags[0]);
|
|
||||||
const pDn=j.marca?paso('Daños',j.marca==='BAJA'?'falla':'falla',j.marca==='BAJA'?'⛔ BAJA':'⚠ DAÑADA',(j.marca_acta&&j.marca_acta.fecha)||''):(danos.length?paso('Daños','ok','reparada / sin marca',danos.length+' acta(s)'):paso('Daños','ok','sin daños')); campo.append(pDn); conSitio(pDn,danos[0]);
|
|
||||||
return sec;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cargar(serie){
|
|
||||||
cont.innerHTML='<p class="vacio">Cargando…</p>';
|
|
||||||
try{
|
|
||||||
const j=await (await fetch('/api/unidad?serie='+encodeURIComponent(serie),{cache:'no-store'})).json();
|
|
||||||
if(!j.ok) throw new Error(j.error||'error');
|
|
||||||
render(j);
|
|
||||||
}catch(e){
|
|
||||||
cont.innerHTML='';
|
|
||||||
cont.append(el('div',{class:'err',text:'No se pudo leer el expediente: '+e.message}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
47
hooks/frontmatter.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
"""Repara el encabezado YAML (front matter) de las páginas que llegan con dos puntos sin comillas.
|
||||||
|
|
||||||
|
Ejemplo real (19-oct): `author: Helgen Technologies (auditoría: Claude; revisión: Leo)`.
|
||||||
|
YAML no acepta ese valor, MkDocs ignora todo el encabezado y lo pinta como texto al principio
|
||||||
|
de la página. Aquí, solo si el encabezado no se puede leer, se ponen entre comillas los valores
|
||||||
|
que tienen «: » y se vuelve a intentar. El archivo en disco no se toca (muchas de estas páginas
|
||||||
|
llegan por scripts/sync-docs.sh); lo correcto es arreglar el original en el repo Drogheda.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
log = logging.getLogger("mkdocs.hooks.frontmatter")
|
||||||
|
_FM = re.compile(r"\A(---[ \t]*\n)(.*?\n)(---[ \t]*\n)", re.S)
|
||||||
|
_LINEA = re.compile(r"^([A-Za-z0-9_\-]+):[ \t]+(.+?)[ \t]*$")
|
||||||
|
|
||||||
|
|
||||||
|
def _lee(texto):
|
||||||
|
try:
|
||||||
|
datos = yaml.safe_load(texto)
|
||||||
|
return isinstance(datos, dict)
|
||||||
|
except yaml.YAMLError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def on_page_read_source(page, config, **kwargs):
|
||||||
|
try:
|
||||||
|
with open(page.file.abs_src_path, encoding="utf-8-sig") as f:
|
||||||
|
fuente = f.read()
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
m = _FM.match(fuente)
|
||||||
|
if not m or _lee(m.group(2)):
|
||||||
|
return None
|
||||||
|
lineas = []
|
||||||
|
for linea in m.group(2).split("\n"):
|
||||||
|
r = _LINEA.match(linea)
|
||||||
|
if r and ": " in r.group(2) and r.group(2)[0] not in "\"'[{|>":
|
||||||
|
valor = r.group(2).replace("\\", "\\\\").replace('"', '\\"')
|
||||||
|
linea = f'{r.group(1)}: "{valor}"'
|
||||||
|
lineas.append(linea)
|
||||||
|
nuevo = "\n".join(lineas)
|
||||||
|
if not _lee(nuevo):
|
||||||
|
return None
|
||||||
|
log.info("Encabezado YAML reparado (dos puntos sin comillas): %s", page.file.src_uri)
|
||||||
|
return m.group(1) + nuevo + m.group(3) + fuente[m.end():]
|
||||||
38
hooks/iconos.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""Íconos de línea del menú lateral (overrides/.icons/helgen/*.svg).
|
||||||
|
|
||||||
|
Material pone el ícono que trae cada página en su front matter (`icon:`). Varias
|
||||||
|
páginas llegan por scripts/sync-docs.sh desde el repo Drogheda y se sobrescriben,
|
||||||
|
así que el ícono se asigna aquí, por ruta, sin tocar esas páginas.
|
||||||
|
Si una página ya trae `icon:` en su front matter, ese manda.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ICONOS = {
|
||||||
|
"index.md": "helgen/inicio",
|
||||||
|
"tableros/pendientes.md": "helgen/pendientes",
|
||||||
|
"bitacora.md": "helgen/bitacora",
|
||||||
|
# Tableros
|
||||||
|
"tableros/index.md": "helgen/pendientes",
|
||||||
|
"tableros/ingenieria.md": "helgen/ingenieria",
|
||||||
|
"tableros/produccion.md": "helgen/barras",
|
||||||
|
"tableros/actas.md": "helgen/actas",
|
||||||
|
"tableros/inventario.md": "helgen/inventario",
|
||||||
|
"tableros/sitio.md": "helgen/sitios",
|
||||||
|
"tableros/equipo.md": "helgen/equipo",
|
||||||
|
"tableros/unidad.md": "helgen/unidad",
|
||||||
|
"tableros/campo.md": "helgen/visitas",
|
||||||
|
# Documentos
|
||||||
|
"guia-ingeniero.md": "helgen/manuales",
|
||||||
|
"produccion/index.md": "helgen/fabrica",
|
||||||
|
"taller/index.md": "helgen/taller",
|
||||||
|
"campo/index.md": "helgen/campo",
|
||||||
|
"hardware/index.md": "helgen/tarjetas",
|
||||||
|
"historia/index.md": "helgen/historia",
|
||||||
|
"descargas/index.md": "helgen/descargas",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def on_page_markdown(markdown, page, config, files, **kwargs):
|
||||||
|
icono = ICONOS.get(page.file.src_uri)
|
||||||
|
if icono and not page.meta.get("icon"):
|
||||||
|
page.meta["icon"] = icono
|
||||||
|
return markdown
|
||||||
331
mkdocs.yml
@ -1,31 +1,37 @@
|
|||||||
site_name: HBOX — Documentación técnica (Helgen)
|
site_name: Drogheda
|
||||||
site_description: Procedimientos de producción, taller, campo y hardware del HBOX (EinarBoard) — Helgen Technologies
|
site_description: HBOX RTK (EinarBoard) — producción, taller, campo, tableros y hardware. Sitio interno de Helgen Technologies.
|
||||||
site_url: https://drogheda.helgen.online/
|
site_url: https://drogheda.helgen.online/
|
||||||
copyright: "© Helgen Technologies — uso interno"
|
copyright: "© Helgen Technologies — uso interno"
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
name: material
|
name: material
|
||||||
|
custom_dir: overrides # logo, plantilla de tableros e íconos de línea (overrides/.icons/helgen)
|
||||||
language: es
|
language: es
|
||||||
logo: img/helgen-logo-white.png
|
|
||||||
favicon: img/favicon.png
|
favicon: img/favicon.png
|
||||||
font: false
|
font: false # letras propias en docs/fonts (sitio utilizable sin Internet)
|
||||||
|
icon:
|
||||||
|
search: helgen/buscar
|
||||||
|
menu: helgen/menu
|
||||||
|
close: helgen/cerrar
|
||||||
|
previous: helgen/izquierda
|
||||||
|
next: helgen/derecha
|
||||||
|
top: helgen/arriba
|
||||||
|
# Oscuro por omisión: el primero de la lista manda. El botón del encabezado cambia a claro.
|
||||||
palette:
|
palette:
|
||||||
- scheme: default
|
|
||||||
primary: custom
|
|
||||||
accent: custom
|
|
||||||
toggle:
|
|
||||||
icon: material/weather-night
|
|
||||||
name: Modo oscuro
|
|
||||||
- scheme: slate
|
- scheme: slate
|
||||||
primary: custom
|
primary: custom
|
||||||
accent: custom
|
accent: custom
|
||||||
toggle:
|
toggle:
|
||||||
icon: material/weather-sunny
|
icon: helgen/sol
|
||||||
name: Modo claro
|
name: Cambiar a modo claro
|
||||||
|
- scheme: default
|
||||||
|
primary: custom
|
||||||
|
accent: custom
|
||||||
|
toggle:
|
||||||
|
icon: helgen/luna
|
||||||
|
name: Cambiar a modo oscuro
|
||||||
features:
|
features:
|
||||||
- navigation.tabs
|
- navigation.sections # barra lateral con secciones, sin pestañas arriba
|
||||||
- navigation.tabs.sticky
|
|
||||||
- navigation.sections
|
|
||||||
- navigation.indexes
|
- navigation.indexes
|
||||||
- navigation.top
|
- navigation.top
|
||||||
- navigation.footer
|
- navigation.footer
|
||||||
@ -35,9 +41,22 @@ theme:
|
|||||||
- content.code.copy
|
- content.code.copy
|
||||||
- toc.follow
|
- toc.follow
|
||||||
|
|
||||||
|
extra:
|
||||||
|
# Clave con que el navegador guarda las preferencias (modo claro/oscuro).
|
||||||
|
# Se cambió el 20-oct-2026 para que todos arranquen en oscuro; js/tema.js lee la misma clave.
|
||||||
|
scope: /drogheda/
|
||||||
|
|
||||||
extra_css:
|
extra_css:
|
||||||
|
- stylesheets/helgen.css
|
||||||
- stylesheets/extra.css
|
- stylesheets/extra.css
|
||||||
|
|
||||||
|
extra_javascript:
|
||||||
|
- js/sitio.js
|
||||||
|
|
||||||
|
hooks:
|
||||||
|
- hooks/iconos.py # íconos del menú lateral (también para páginas que llegan por sync-docs.sh)
|
||||||
|
- hooks/frontmatter.py # encabezados YAML con «: » sin comillas: se leen igual (no toca los archivos)
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- search:
|
- search:
|
||||||
lang: [es, en]
|
lang: [es, en]
|
||||||
@ -46,6 +65,7 @@ plugins:
|
|||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- admonition
|
- admonition
|
||||||
- tables
|
- tables
|
||||||
|
- def_list
|
||||||
- attr_list
|
- attr_list
|
||||||
- md_in_html
|
- md_in_html
|
||||||
- pymdownx.details
|
- pymdownx.details
|
||||||
@ -55,149 +75,148 @@ markdown_extensions:
|
|||||||
- toc:
|
- toc:
|
||||||
permalink: true
|
permalink: true
|
||||||
|
|
||||||
|
# Menú lateral. Reglas: sin emoji, títulos de 4 palabras o menos.
|
||||||
|
# Documento nuevo con fecha (auditoría, pasada, plan): agréguelo arriba en
|
||||||
|
# Tarjetas y hardware → Auditorías y pasadas («19 oct · Tema») y en bitacora.md.
|
||||||
|
# Cuando lo supere otro, muévalo a Historia → Tarjetas superadas.
|
||||||
nav:
|
nav:
|
||||||
- "🏠 Inicio":
|
- Inicio: index.md
|
||||||
- index.md
|
- Pendientes: tableros/pendientes.md
|
||||||
- "Guía del ingeniero": guia-ingeniero.md
|
- Bitácora: bitacora.md
|
||||||
- "⚙ Cuenta":
|
|
||||||
- "Mi cuenta (cambiar contraseña)": cuenta.html
|
|
||||||
- "Administración (Leo)": admin.html
|
|
||||||
- "Salir": salir.html
|
|
||||||
|
|
||||||
- "📋 Tableros":
|
- Tableros:
|
||||||
- tableros/index.md
|
- tableros/index.md
|
||||||
- "Kanban — tareas del equipo": tableros/kanban.html
|
- Ingeniería: tableros/ingenieria.md
|
||||||
- "Equipo — miembros, responsables y accesos": tableros/equipo.html
|
- Producción: tableros/produccion.md
|
||||||
- "Ingeniería — estado por versión de tarjeta": tableros/ingenieria.html
|
- Actas: tableros/actas.md
|
||||||
- "Producción — plan 570 vs. avance real": tableros/produccion.html
|
- Inventario: tableros/inventario.md
|
||||||
- "Estado de actas (tarjetas, módulos, unidades)": tablero.html
|
- Sitios: tableros/sitio.md
|
||||||
- "Tablero de campo (visitas de diagnóstico)": diagnostico/tablero-campo.html
|
- Equipo: tableros/equipo.md
|
||||||
- "Expediente de una unidad": unidad.html
|
- Expediente de unidad: tableros/unidad.md
|
||||||
|
- Visitas de campo: tableros/campo.md
|
||||||
|
|
||||||
- "🏭 Producción":
|
- Documentos:
|
||||||
- produccion/index.md
|
- Guía del ingeniero: guia-ingeniero.md
|
||||||
- "1 · Prueba y flasheo de tarjetas":
|
- Producción:
|
||||||
- procedimientos/v5-prueba-y-flasheo.md
|
- produccion/index.md
|
||||||
- "Acta de tarjeta V5": formularios/v5.html
|
- Prueba de tarjetas:
|
||||||
- "2 · Módulos RTK y 4G":
|
- Procedimiento: procedimientos/v5-prueba-y-flasheo.md
|
||||||
- procedimientos/v5-modulos-integracion.md
|
- Acta de tarjeta V5: formularios/v5.html
|
||||||
- "Acta ArduSimple": formularios/ardusimple.html
|
- Módulos RTK y 4G:
|
||||||
- "Acta módem 4G": formularios/modem4g.html
|
- Procedimiento: procedimientos/v5-modulos-integracion.md
|
||||||
- "Acta de integración": formularios/integracion.html
|
- Acta ArduSimple: formularios/ardusimple.html
|
||||||
- "3 · Ensamble de la unidad":
|
- Acta módem 4G: formularios/modem4g.html
|
||||||
- procedimientos/v5-ensamble.md
|
- Acta de integración: formularios/integracion.html
|
||||||
- "Acta de ensamble": formularios/ensamble.html
|
- Ensamble:
|
||||||
- "4 · Prueba final antes de embarcar": procedimientos/v5-prueba-final-unidad.md
|
- Procedimiento: procedimientos/v5-ensamble.md
|
||||||
- "5 · Configuración de la unidad": procedimientos/v5-configuracion-unidad.md
|
- Acta de ensamble: formularios/ensamble.html
|
||||||
- "📦 Inventario":
|
- Prueba final: procedimientos/v5-prueba-final-unidad.md
|
||||||
- inventario/index.md
|
- Configuración: procedimientos/v5-configuracion-unidad.md
|
||||||
- "Inventario — existencias y movimientos": inventario/inventario.html
|
- Política de inventario: inventario/index.md
|
||||||
|
- Taller:
|
||||||
|
- taller/index.md
|
||||||
|
- Diagnóstico en banco:
|
||||||
|
- Procedimiento: procedimientos/diagnostico-banco.md
|
||||||
|
- Acta de banco: formularios/banco.html
|
||||||
|
- Reflasheo:
|
||||||
|
- Procedimiento: procedimientos/reflasheo-unidades.md
|
||||||
|
- Acta de reflasheo: formularios/reflasheo.html
|
||||||
|
- Módem en batería:
|
||||||
|
- Procedimiento: procedimientos/prueba-bateria-modem.md
|
||||||
|
- Acta de la prueba: formularios/prueba-bateria.html
|
||||||
|
- Ethernet y serie:
|
||||||
|
- Procedimiento: procedimientos/v5-prueba-ethernet-serie.md
|
||||||
|
- Acta de la prueba: formularios/ethernet-serie.html
|
||||||
|
- V4 con 0.6.0:
|
||||||
|
- Procedimiento: procedimientos/v4-prueba-fw-0.5.6.md
|
||||||
|
- Acta de prueba V4: formularios/v4.html
|
||||||
|
- V4 sin NMEA: procedimientos/v4-prueba-riel-sw5v-f9p.md
|
||||||
|
- Campo:
|
||||||
|
- campo/index.md
|
||||||
|
- Diagnóstico en campo:
|
||||||
|
- Procedimiento: diagnostico/procedimiento-diagnostico-campo.md
|
||||||
|
- Acta de diagnóstico: formularios/diagnostico.html
|
||||||
|
- Instalación:
|
||||||
|
- Checklist para imprimir: procedimientos/v5-checklist-puesta-en-marcha.md
|
||||||
|
- Acta de instalación: formularios/instalacion.html
|
||||||
|
- Envío y recepción:
|
||||||
|
- Ciclo de vida: procedimientos/ciclo-de-vida.md
|
||||||
|
- Acta de envío: formularios/envio.html
|
||||||
|
- Acta de recepción: formularios/recepcion.html
|
||||||
|
- Acta de daño: formularios/dano.html
|
||||||
|
- Sitios y puertos: sitios/index.md
|
||||||
|
- Cómo funcionan las actas: actas/index.md
|
||||||
|
- Tarjetas y hardware:
|
||||||
|
- hardware/index.md
|
||||||
|
- Manuales por tarjeta: manuales/index.md
|
||||||
|
- Auditorías y pasadas:
|
||||||
|
- 19 oct · Críticos V5.6: hardware/v56-clasificacion-19oct.md
|
||||||
|
- 19 oct · Re-auditoría: hardware/re-auditoria-19oct.md
|
||||||
|
- 19 oct · Pasada 4: hardware/v56-v57-layout-pasada-4.md
|
||||||
|
- 16 oct · Plan v6: hardware/v6-plan-fines-de-semana.md
|
||||||
|
- 16 oct · Auditoría v6: hardware/v6-auditoria-produccion-16oct.md
|
||||||
|
- 16 oct · Auditoría de producción: hardware/auditoria-produccion-16oct.md
|
||||||
|
- V5.5 y V5.6:
|
||||||
|
- Manual V5: manuales/manual-tarjeta-V5.md
|
||||||
|
- Hoja de datos V5.6: hardware/v56-datasheet.md
|
||||||
|
- Pinout V5.6: hardware/v56-pinout.md
|
||||||
|
- ECO V5.6: hardware/v56-eco.md
|
||||||
|
- Cambios al esquemático: hardware/v56-cambios-esquematico.md
|
||||||
|
- Reglas de layout: hardware/v56-layout-instrucciones.md
|
||||||
|
- V5.7:
|
||||||
|
- ECO V5.7: hardware/v57-eco-fuente.md
|
||||||
|
- v6:
|
||||||
|
- Manual v6: manuales/manual-tarjeta-v6.md
|
||||||
|
- Plan de corrección: hardware/v6-plan-correccion.md
|
||||||
|
- Esquemático final: hardware/v6-cambios-resumen.md
|
||||||
|
- Mapa de pines: hardware/v6-pinmap.md
|
||||||
|
- Plan de cobre: hardware/EinarBoard-v6_Plan-de-Cobre_ES.html
|
||||||
|
- Plan de cobre (inglés): hardware/EinarBoard-v6_Copper-Plan_EN.html
|
||||||
|
- Auditoría del 28 sep: hardware/v6-auditoria-completa.md
|
||||||
|
- Auditoría 28 sep (inglés): hardware/v6-full-audit.md
|
||||||
|
- Pasada 1 (28 sep): hardware/v6-cambios-esquematico.md
|
||||||
|
- Pasada 2 (inglés): hardware/v6-puertos-sin-nueva-version.md
|
||||||
|
- Pasada 3 (inglés): hardware/v6-revision-final.md
|
||||||
|
- Próximos pasos (superado): hardware/v6-proximos-pasos.md
|
||||||
|
- V4 y V3:
|
||||||
|
- Manual V4: manuales/manual-tarjeta-V4.md
|
||||||
|
- Manual V3: manuales/manual-tarjeta-V3.md
|
||||||
|
- Pinout V3: montaje/pinout-arneses.md
|
||||||
|
- Manuales y descargas:
|
||||||
|
- Manual del cliente: manuales/manual-cliente.md
|
||||||
|
- Manual de instalación (PDF): manuales/Drogheda-Manual-Instalacion-Operacion_v2.0.pdf
|
||||||
|
- Ficha técnica (PDF): manuales/Drogheda-HBOX-Datasheet_v2.0.pdf
|
||||||
|
- Descargas:
|
||||||
|
- descargas/index.md
|
||||||
|
- Notas de versión: descargas/notas-de-version.md
|
||||||
|
- README del firmware: descargas/hbox-v5-bringup-README.md
|
||||||
|
- Git y KiCad:
|
||||||
|
- Guía Git: git/guia-git-es.md
|
||||||
|
- Guía Git (inglés): git/git-guide-en.md
|
||||||
|
- Historia:
|
||||||
|
- historia/index.md
|
||||||
|
- Procedimientos superados:
|
||||||
|
- Diagnóstico remoto v1: diagnostico/protocolo-diagnostico-remoto.md
|
||||||
|
- EinarBoard 1.0: procedimientos/einarboard-1.0-bringup.md
|
||||||
|
- Ventilador FW 3.0.3: procedimientos/fan-policy-bench-test.md
|
||||||
|
- Protoboard v3: procedimientos/historico-proto-bench-v3.md
|
||||||
|
- Compras protoboard v3: procedimientos/historico-protoboard.md
|
||||||
|
- Producción V3:
|
||||||
|
- BOM V3: montaje/bom-split.md
|
||||||
|
- Ensamble y QC V3: montaje/guia-ensamble-qc.md
|
||||||
|
- Gabinete 0007-1000: montaje/assembly-guide.md
|
||||||
|
- Tarjetas superadas:
|
||||||
|
- 16 oct · Pasada 3: hardware/v56-v57-layout-pasada-3.md
|
||||||
|
- 14 oct · Ruteo V5.7: hardware/v57-colocacion-ruteo.md
|
||||||
|
- 13 oct · Pasada 2: hardware/v56-layout-pasada-2.md
|
||||||
|
- 13 oct · Auditoría de layout: hardware/v56-auditoria-layout-13oct.md
|
||||||
|
- 13 oct · Guía fuente V5.7: hardware/v57-layout-guia.md
|
||||||
|
- 12 oct · Pasada 1: hardware/v56-layout-pasada-1.md
|
||||||
|
- 28 sep · Lecciones V5.5: hardware/v6-revision-lecciones-v55.md
|
||||||
|
- 25 sep · Pre-fabricación v6: hardware/v6-auditoria-prefab.md
|
||||||
|
- 25 sep · Pre-fab (inglés): hardware/v6-prefab-audit.md
|
||||||
|
|
||||||
- "🔬 Taller":
|
- Cuenta:
|
||||||
- taller/index.md
|
- Mi cuenta: cuenta.html
|
||||||
- "Diagnóstico en banco":
|
- Administración: admin.html
|
||||||
- procedimientos/diagnostico-banco.md
|
- Salir: salir.html
|
||||||
- "Acta de diagnóstico en banco": formularios/banco.html
|
|
||||||
- "Reflasheo de unidades":
|
|
||||||
- procedimientos/reflasheo-unidades.md
|
|
||||||
- "Acta de reflasheo y configuración": formularios/reflasheo.html
|
|
||||||
- "Prueba: módem 4G en batería":
|
|
||||||
- procedimientos/prueba-bateria-modem.md
|
|
||||||
- "Acta: módem en batería": formularios/prueba-bateria.html
|
|
||||||
- "Prueba: Ethernet y puertos serie":
|
|
||||||
- procedimientos/v5-prueba-ethernet-serie.md
|
|
||||||
- "Acta: Ethernet y puertos serie": formularios/ethernet-serie.html
|
|
||||||
- "V4 — preparación y prueba con 0.6.0":
|
|
||||||
- procedimientos/v4-prueba-fw-0.5.6.md
|
|
||||||
- "Acta de prueba V4": formularios/v4.html
|
|
||||||
- "V4 — riel SW_5V y arranque del F9P (sin NMEA)": procedimientos/v4-prueba-riel-sw5v-f9p.md
|
|
||||||
|
|
||||||
- "🚜 Campo":
|
|
||||||
- campo/index.md
|
|
||||||
- "Diagnóstico en campo":
|
|
||||||
- diagnostico/procedimiento-diagnostico-campo.md
|
|
||||||
- "Acta de diagnóstico en campo": formularios/diagnostico.html
|
|
||||||
- "Instalación en el puerto":
|
|
||||||
- "Checklist de puesta en marcha (imprimir)": procedimientos/v5-checklist-puesta-en-marcha.md
|
|
||||||
- "Acta de instalación en campo": formularios/instalacion.html
|
|
||||||
- "Envío, recepción y daños":
|
|
||||||
- procedimientos/ciclo-de-vida.md
|
|
||||||
- "Acta de envío": formularios/envio.html
|
|
||||||
- "Acta de recepción e inspección": formularios/recepcion.html
|
|
||||||
- "Acta de daño o incidente": formularios/dano.html
|
|
||||||
- "📍 Sitios":
|
|
||||||
- sitios/index.md
|
|
||||||
- "Sitio — unidades instaladas y actas": sitios/sitio.html
|
|
||||||
- "Actas — índice y cómo se usan": actas/index.md
|
|
||||||
|
|
||||||
- "🔩 Tarjetas y hardware":
|
|
||||||
- hardware/index.md
|
|
||||||
- "▶ V5.6 — qué es crítico y qué se puede quedar (19-oct, tarde)": hardware/v56-clasificacion-19oct.md
|
|
||||||
- "▶ Re-auditoría V5.6 · V5.7 · v6 (19-oct)": hardware/re-auditoria-19oct.md
|
|
||||||
- "▶ Layout pasada 4 — cierre V5.6 y V5.7 (Michelle, 19-oct)": hardware/v56-v57-layout-pasada-4.md
|
|
||||||
- "▶ v6 lista para producción — plan de fines de semana (rev. 1.1)": hardware/v6-plan-fines-de-semana.md
|
|
||||||
- "Auditoría de producción v6 (16-oct)": hardware/v6-auditoria-produccion-16oct.md
|
|
||||||
- "Auditoría de producción V5.6 · V5.7 · v6 (16-oct)": hardware/auditoria-produccion-16oct.md
|
|
||||||
- "Manuales por tarjeta": manuales/index.md
|
|
||||||
- "V5.5 en campo · V5.6 en layout → pedido 113":
|
|
||||||
- "Manual de la tarjeta V5 (V5.5 y V5.6)": manuales/manual-tarjeta-V5.md
|
|
||||||
- "Hoja de datos V5.6 (1-oct)": hardware/v56-datasheet.md
|
|
||||||
- "Pinout de conectores V5.6 (29-sep)": hardware/v56-pinout.md
|
|
||||||
- "ECO V5.6 — requisitos R1–R5 (27-sep)": hardware/v56-eco.md
|
|
||||||
- "Cambios al esquemático V5.5 → V5.6 (28-sep)": hardware/v56-cambios-esquematico.md
|
|
||||||
- "Reglas de layout: esquemático → PCB (29-sep)": hardware/v56-layout-instrucciones.md
|
|
||||||
- "V5.7 fuente a bordo (9–32 V) → pedido 327 (en layout)":
|
|
||||||
- "ECO V5.7 — fuente 24 V → 5 V a bordo (v0.2, 13-oct)": hardware/v57-eco-fuente.md
|
|
||||||
- "v6 GNSS + 4G a bordo → pedido 130 (en diseño)":
|
|
||||||
- "Manual de la tarjeta v6 (marcador)": manuales/manual-tarjeta-v6.md
|
|
||||||
- "Próximos pasos para ordenar (29-sep, superado el 16-oct)": hardware/v6-proximos-pasos.md
|
|
||||||
- "Plan de corrección y lista de liberación (28-sep)": hardware/v6-plan-correccion.md
|
|
||||||
- "Esquemático final — resumen de las 3 pasadas (28-sep)": hardware/v6-cambios-resumen.md
|
|
||||||
- "Mapa de pines MCU para board_v6.h (28-sep)": hardware/v6-pinmap.md
|
|
||||||
- "Plan de cobre y colocación EMC (5-sep)": hardware/EinarBoard-v6_Plan-de-Cobre_ES.html
|
|
||||||
- "Plan de cobre y colocación EMC (inglés)": hardware/EinarBoard-v6_Copper-Plan_EN.html
|
|
||||||
- "v6 — auditoría del 28-sep y sus pasadas (detalle)":
|
|
||||||
- "Auditoría completa v6 (28-sep)": hardware/v6-auditoria-completa.md
|
|
||||||
- "Auditoría completa v6 (inglés, original)": hardware/v6-full-audit.md
|
|
||||||
- "Pasada 1 — cambios al esquemático (28-sep)": hardware/v6-cambios-esquematico.md
|
|
||||||
- "Pasada 2 — puertos y sin nueva versión (inglés)": hardware/v6-puertos-sin-nueva-version.md
|
|
||||||
- "Pasada 3 — revisión final independiente (inglés)": hardware/v6-revision-final.md
|
|
||||||
- "Revisión v6 contra lecciones V5.5 (28-sep, superada)": hardware/v6-revision-lecciones-v55.md
|
|
||||||
- "V4 y V3 — tarjetas anteriores aún en campo":
|
|
||||||
- "Manual de la tarjeta V4": manuales/manual-tarjeta-V4.md
|
|
||||||
- "Manual de la tarjeta V3": manuales/manual-tarjeta-V3.md
|
|
||||||
- "Pinout V3 (arneses y conectores, jul-2026)": montaje/pinout-arneses.md
|
|
||||||
|
|
||||||
- "📕 Manuales y descargas":
|
|
||||||
- "Manual del cliente (instalación y operación)": manuales/manual-cliente.md
|
|
||||||
- "Manual de instalación y operación v2.0 (PDF)": manuales/Drogheda-Manual-Instalacion-Operacion_v2.0.pdf
|
|
||||||
- "Ficha técnica HBOX v2.0 (PDF)": manuales/Drogheda-HBOX-Datasheet_v2.0.pdf
|
|
||||||
- "Descargas (firmware por tarjeta)":
|
|
||||||
- descargas/index.md
|
|
||||||
- "Notas de versión del firmware V4 / V5": descargas/notas-de-version.md
|
|
||||||
- "README firmware hbox_v5_bringup (inglés)": descargas/hbox-v5-bringup-README.md
|
|
||||||
- "Git y archivos de diseño (KiCad)":
|
|
||||||
- "Guía Git para colaboradores": git/guia-git-es.md
|
|
||||||
- "Guía Git (inglés)": git/git-guide-en.md
|
|
||||||
|
|
||||||
- "📚 Referencia e histórico":
|
|
||||||
- historia/index.md
|
|
||||||
- "Procedimientos anteriores (superados)":
|
|
||||||
- "Protocolo de diagnóstico remoto — unidades v1 (jun-2026)": diagnostico/protocolo-diagnostico-remoto.md
|
|
||||||
- "EinarBoard 1.0 — puesta en marcha (jul-2026)": procedimientos/einarboard-1.0-bringup.md
|
|
||||||
- "FW v3.0.3 — prueba de banco del ventilador (ago-2026)": procedimientos/fan-policy-bench-test.md
|
|
||||||
- "Prototipo v3 en protoboard — bring-up (jun-2026)": procedimientos/historico-proto-bench-v3.md
|
|
||||||
- "Prototipo v3 en protoboard — lista de compras (jun-2026)": procedimientos/historico-protoboard.md
|
|
||||||
- "V3 — producción de jul-2026 (superado)":
|
|
||||||
- "BOM V3 dividido para ensamble (jul-2026)": montaje/bom-split.md
|
|
||||||
- "Guía de ensamble y QC — borrador V3 (sep-2026)": montaje/guia-ensamble-qc.md
|
|
||||||
- "Guía de ensamble del gabinete 0007-1000 (PDF)": montaje/assembly-guide.md
|
|
||||||
- "Tarjetas — documentos superados":
|
|
||||||
- "V5.6/V5.7 layout pasada 3 (16-oct) — cerrada; lo pendiente pasó a la pasada 4": hardware/v56-v57-layout-pasada-3.md
|
|
||||||
- "V5.6 layout pasada 2 (Michelle, 13-oct) — superada por la pasada 3": hardware/v56-layout-pasada-2.md
|
|
||||||
- "V5.6 auditoría del layout 13-oct (inglés) — superada por la del 16-oct": hardware/v56-auditoria-layout-13oct.md
|
|
||||||
- "V5.7 guía de layout de la fuente (Leo, 13-oct) — superada: la fuente ya está colocada": hardware/v57-layout-guia.md
|
|
||||||
- "V5.7 colocación y ruteo (14-oct) — superada por la pasada 3": hardware/v57-colocacion-ruteo.md
|
|
||||||
- "V5.6 layout pasada 1 (Michelle, 12-oct) — superada por la pasada 2": hardware/v56-layout-pasada-1.md
|
|
||||||
- "v6 auditoría pre-fabricación (25-sep) — superada por la del 28-sep": hardware/v6-auditoria-prefab.md
|
|
||||||
- "v6 auditoría pre-fabricación (25-sep, inglés)": hardware/v6-prefab-audit.md
|
|
||||||
|
|||||||
1
overrides/.icons/helgen/actas.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 4.5h10a2 2 0 0 1 2 2V19a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6.5a2 2 0 0 1 2-2zM9 3.5h6v2.5H9zM8.5 11h7M8.5 15h5"/></svg>
|
||||||
|
After Width: | Height: | Size: 319 B |
1
overrides/.icons/helgen/admin.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.5 19 6v5.5c0 4.2-2.9 7.6-7 9-4.1-1.4-7-4.8-7-9V6zM9 12l2.2 2.2L15.5 10"/></svg>
|
||||||
|
After Width: | Height: | Size: 287 B |
1
overrides/.icons/helgen/arriba.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M6 11l6-6 6 6"/></svg>
|
||||||
|
After Width: | Height: | Size: 232 B |
1
overrides/.icons/helgen/aviso.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17zM12 8v5M12 16.2v.3"/></svg>
|
||||||
|
After Width: | Height: | Size: 275 B |
1
overrides/.icons/helgen/barras.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 20V11M10.5 20V5M16 20v-6M3 20h18"/></svg>
|
||||||
|
After Width: | Height: | Size: 246 B |
1
overrides/.icons/helgen/bitacora.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12h4l2.5-6 5 12L17 12h4"/></svg>
|
||||||
|
After Width: | Height: | Size: 237 B |
1
overrides/.icons/helgen/buscar.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 4.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13zM20 20l-4.4-4.4"/></svg>
|
||||||
|
After Width: | Height: | Size: 272 B |
1
overrides/.icons/helgen/campo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6.5l6-2.5 6 2.5 6-2.5v13.5l-6 2.5-6-2.5-6 2.5zM9 4v13.5M15 6.5V20"/></svg>
|
||||||
|
After Width: | Height: | Size: 279 B |
1
overrides/.icons/helgen/cerrar.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>
|
||||||
|
After Width: | Height: | Size: 231 B |
1
overrides/.icons/helgen/cobre.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h5l3 3h8M4 17h7l3-3h6M7 7v10"/></svg>
|
||||||
|
After Width: | Height: | Size: 243 B |
1
overrides/.icons/helgen/cuenta.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="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"/></svg>
|
||||||
|
After Width: | Height: | Size: 287 B |
1
overrides/.icons/helgen/derecha.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 5.5l6.5 6.5L9 18.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 232 B |
1
overrides/.icons/helgen/descargas.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v11M7.5 10.5 12 15l4.5-4.5M5 20h14"/></svg>
|
||||||
|
After Width: | Height: | Size: 250 B |
1
overrides/.icons/helgen/documento.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" 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>
|
||||||
|
After Width: | Height: | Size: 300 B |
1
overrides/.icons/helgen/equipo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 5.3a3.2 3.2 0 1 0 0 6.4 3.2 3.2 0 0 0 0-6.4zM3.5 19.5c.6-3.2 2.8-5 5.5-5s4.9 1.8 5.5 5M16.8 7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM15.8 14.6c2.3 0 4.2 1.6 4.7 4.4"/></svg>
|
||||||
|
After Width: | Height: | Size: 376 B |
1
overrides/.icons/helgen/fabrica.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2.5 20.5h19M4 20.5V11l5 3v-3l5 3V7.5h4.5L20 20.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 260 B |
1
overrides/.icons/helgen/fecha.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="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"/></svg>
|
||||||
|
After Width: | Height: | Size: 303 B |
1
overrides/.icons/helgen/historia.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4.5h16v4H4zM5.5 8.5V19a1.5 1.5 0 0 0 1.5 1.5h10a1.5 1.5 0 0 0 1.5-1.5V8.5M10 12.5h4"/></svg>
|
||||||
|
After Width: | Height: | Size: 297 B |
1
overrides/.icons/helgen/ingenieria.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM19 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM7 16h4l3-8h3"/></svg>
|
||||||
|
After Width: | Height: | Size: 293 B |
1
overrides/.icons/helgen/inicio.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 10.5 12 3l9 7.5M5 9.5V20h14V9.5M10 20v-6h4v6"/></svg>
|
||||||
|
After Width: | Height: | Size: 258 B |
1
overrides/.icons/helgen/inventario.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 4.5v9L12 21l-8-4.5v-9zM4 7.5l8 4.5 8-4.5M12 12v9"/></svg>
|
||||||
|
After Width: | Height: | Size: 267 B |
1
overrides/.icons/helgen/izquierda.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 5.5 8.5 12l6.5 6.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 233 B |
1
overrides/.icons/helgen/liga.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>
|
||||||
|
After Width: | Height: | Size: 309 B |
1
overrides/.icons/helgen/luna.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hg-i" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z"/></svg>
|
||||||
|
After Width: | Height: | Size: 260 B |