Synaplan
開源 AI 平台,供企業使用:自行託管的 AI 代理、符合 GDPR 的聊天機器人小工具、RAG 知識庫、WhatsApp 與 Outlook AI、MCP、多模型路由。Docker、Kubernetes 或託管於 Germ
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Este documento explica cómo funciona por dentro index.html (una SPA de un solo archivo, sin build ni backend propio), con foco especial en la pestaña Producción, que es el corazón de la app: cruza el Programa de Producción con el árbol de producción de cada producto (listas de materiales de SAP) y los RMD de SAP para decirte, producto por producto y etapa por etapa, qué le falta autorización.
Está escrito para que puedas decidir qué tocar al mejorar Producción sin romper el resto. Cada sección indica el rango de líneas aproximado en index.html para ubicarte rápido.
<style> (CSS, variables de tema) + <script> (toda la lógica) + HTML de las 4 pestañas. No hay bundler, no hay módulos ES, no hay backend propio: todo corre en el navegador.xlsx-js-style (leer/escribir Excel), Day.js (fechas), SweetAlert2 (todos los diálogos/modales), GSAP (animaciones de transición), html2canvas (exportar gráficos como imagen), localForage (IndexedDB), Supabase JS SDK (persistencia + realtime), y un script externo arbol-motor.js (motor de árbol de materiales BOM de SAP, con el que se calcula el árbol de producción de cada producto).currentTab / switchTab() (orden en TAB_ORDER): produccion (la principal, va primero), status (Status RMD, segunda y con su nombre siempre visible), forecast, rmd (llamada en la UI "OP sin RMD") y acuerdos. Las tres últimas van minimizadas en la barra (li.tab-mini): solo el ícono, con el nombre en el tooltip y cuando están activas. (La antigua pestaña Seguimiento RMD se quitó porque no se usaba; su Consultar RMD —historial de versiones de una descripción— ahora se arma con los datos de SAP ya cargados, rmdAutorizadosDetalle, sin consultar otra tabla.)storeSet(key, value) / se lee con storeGet(key) (línea ~3038). Este par de funciones son el único punto de entrada a la persistencia en toda la app — nunca se llama a Supabase o a IndexedDB directamente desde el resto del código.pedirDNI, línea ~3248) que se valida contra un directorio fijo en el código (DNI_DIRECTORIO) y queda registrado en un historial (registrarHistorial). La sincronización con SAP (botón "Enviar a Status RMD" del portal, script ≥ v1.19) no pide DNI: queda registrada con el usuario con el que se inició sesión en el portal (sección 4.9). Lo que la app hace sola (p. ej. autorizaciones manuales que caducan) queda como "Status RMD (automático)".El tema oscuro/claro se resuelve con variables CSS (--accent, --slate-*, etc.) redefinidas bajo html[data-theme="light"], y Tailwind está configurado (script inline justo después de <script src=".../tailwind.js">) para que sus clases de color (slate, amber, emerald...) resuelvan a esas mismas variables — así una sola clase de Tailwind cambia de color solo con el atributo data-theme, sin tocar el HTML.
Producción cruza 2 fuentes (cada una con su botón de carga en la barra superior, #inlineUploadProd) con el árbol de producción de cada producto (listas de materiales de SAP):
| Fuente | Variable(s) | De dónde sale | Qué aporta |
|---|---|---|---|
| Programa de Producción | productionData (array) | Un Excel por planta (Planta ATE / Planta LIMA), varias hojas (una por área) | Qué se va a producir y CUÁNDO (fechas) |
| RMD de SAP | rmdPrecisoDatos / rmdPreciso (+ rmdAutorizadosDetalle para la observación) | Recomendado: sin archivo, con el botón "Enviar a Status RMD" del portal (trae todas las versiones y todas las recetas). También acepta el Exportado RMD (sección 4.8) | El estado de cada RMD y con qué materiales (recetas) se usa |
| Árbol de producción | motorArboles, arbolDeProduccion() | Motor local de "Listas de materiales" (arbol-motor.js + 4 tablas, caché 12 h) | Qué etapas tiene cada producto y con qué material exacto se hace cada una |
La Base de Datos (material → Código Agrupador) y el Código Agrupador ya no existen en la app: se quitaron su botón, su carga, el antiguo "filtrado por árbol" por agrupador y el autocompletado desde el árbol. Sus claves guardadas (val_database_materials, val_agrupador_etapas, val_rmd_autorizados_raw, val_rmd_autorizados_etapas_por_agrupador, val_descartados_por_arbol*) ya no se leen ni se escriben (no se borraron).
La función que lo cruza todo es runValidacionMateriales() — es, con diferencia, la función más importante de toda la pestaña. Todo lo demás (KPIs, tabla, calendario, gráficos) se recalcula a partir de su resultado.
Excel Programa de Producción ──┐
├─► runValidacionMateriales() ──► ultimoResultadoValidacion
RMD de SAP (con recetas) ──┤ por cada producto: { pendientes[], autorizadosManual[],
│ evaluarMaterialPreciso() revisados, autorizados, rmdPendientes }
Árbol de producción (BOM SAP) ──┘ │
▼
renderTablaPendientes() + KPIs + calendario + gráficos
Declaradas cerca de la línea 2900 y 5300:
let currentPlanta = 'planta2'; // 'planta1' (ATE) | 'planta2' (LIMA) | 'consolidado'
let productionData = []; // [{ codigo, producto, area, fechas:[...], planta? }]
let availableAreas = new Set(); // nombres de hoja/área presentes en el Excel cargado
let currentAreaFilter = 'TODAS'; // filtro de pestañas de área (chips debajo del header)
let rmdPrecisoDatos = null; // maestro de RMD que se guarda (val_rmd_preciso): una fila compacta por RMD
let rmdPreciso = null; // su índice en memoria: porMatEtapa ("material::ETAPA" -> RMD), etapasPropias, descDeMaterial
const motorArboles = { estado }; // 'pendiente' | 'cargando' | 'listo' | 'no-disponible' (motor de árboles)
let rmdAutorizadosDetalle = []; // una fila por RMD del Exportado (de aquí sale la Observación de cada RMD)
let manualAutorizados = new Set(); // claves "material::ETAPA_FIJA" marcadas ✓ a mano (val_manual_autorizados)
let manualAutorizadosMeta = new Map(); // clave -> { f: fecha, c: huella de lo pendiente en SAP } (val_manual_autorizados_meta)
let estatusEtapa = new Map(); // "material::ETAPA_FIJA" -> 'PENDIENTE-PRO' | 'POR INGRESAR' | ...
let responsablesEtapa = new Map(); // "material::ETAPA_FIJA" -> 'PRO' | 'DOC' | 'ASC' | 'IDE'
let ultimosCambiosSap = null; // qué cambió con la última sincronización (val_cambios_sap)
const filtrosEtapaProd = { etapa, sap, edad }; // filtros por etapa de la tabla (solo vista)
let ultimoResultadoValidacion = { revisados, autorizados, pendientes:[...], autorizadosManual:[...], rmdPendientes };
Todas estas colecciones (salvo productionData, que vive bajo prod_data_<planta>) se guardan bajo claves val_* en Supabase/IndexedDB y se recargan al arrancar con loadValidacionData() (línea ~6946).
Esta es la lógica más frágil de toda la app, porque el formato del Excel de origen no es estable: cambia entre Planta ATE y Planta LIMA, y dentro de la misma planta puede haber hojas con formato antiguo y nuevo mezcladas. Vale la pena entenderla bien antes de tocar nada aquí.
.xlsx → handleFileProd(e) (línea ~4441) pide el DNI (autorizarYRegistrar) y, si se aprueba, llama a processFileProd(file) (línea ~4504).processFileProd captura currentPlanta en una variable local (plantaObjetivo) antes de leer el archivo — como la lectura es asíncrona (FileReader), si el usuario cambia de planta mientras se procesa, el archivo se sigue tratando como de la planta que estaba activa al iniciar la carga. Este patrón ("capturar el contexto antes de un await") se repite en varias partes de la app — es la defensa estándar contra condiciones de carrera con el usuario interactuando durante una operación async.XLSX.read, y se recorre hoja por hoja (wb.SheetNames.forEach). Cada hoja = un área de producción (SOL, ACO, INY, COS, CAP BLAN, SOL HORM, SOL COLOR, SEM, MEN, REA, PEF...).Map (pmap) con clave `${codigo}_${areaHoja}` — es decir, el mismo código de material en dos áreas distintas cuenta como dos entradas separadas (porque puede tener etapas pendientes distintas en cada área).storeSet('prod_data_<planta>', { data, areas }) y, si ya había una versión anterior de esa planta, se genera un snapshot de trazabilidad (sección 6).El Programa de Producción no trae "una fecha por celda" de forma simple. Hay dos formatos según la planta:
Planta ATE (planta1Format = true) — buildDateColumnMapPlanta1() (línea ~4244):
S37, S38... (formato corto).isoWeekMonday(37) + offset días, no se lee un valor de fecha literal de la celda.CODIGO; el código viene en PRODUCTO y la descripción en NOMBRE. findProductionHeaderRowIndex solo usa la fila PRODUCTO + NOMBRE si ninguna fila tiene CODIGO (así el formato anterior se lee igual). En BEB, SEL, PV1-x, CARB, CEFA y ORA-SOL-CAP las fechas vienen en la misma fila de encabezados y la etiqueta S43 dos filas arriba: la fila S.. se busca hasta 4 filas arriba y la fila de días se elige por conteo de celdas de día (la siguiente, salvo que la de encabezados tenga más).Planta LIMA (planta1Format = false) — buildDateColumnMap() (línea ~4400), con dos variantes que se auto-detectan:
Date reales, directamente en la fila de "Código" o en la siguiente.SEM31, etc.) más cercano arriba.scoreDirectDates), y se usa la que tenga más.El "mapa global de semanas" (buildGlobalWeekMondayMap(), línea ~4274) es una pieza aparte: como una misma etiqueta de semana (ej. S37) puede aparecer en varias hojas del mismo libro, y no todas tienen una fecha directa ancla, se hace una pasada completa por todo el workbook primero para capturar cualquier fecha real y asociarla a su número de semana, y solo después (segunda pasada) se rellenan por cálculo ISO las semanas que sigan sin fecha. El comentario en el código documenta un bug real de versiones anteriores donde el orden de las hojas hacía que un respaldo calculado (equivocado) se escribiera antes de que apareciera la fecha real correcta en otra hoja — por eso ahora está separado en dos pasadas explícitas.
containsExcludedTermPlanta1 / containsExcludedTermPlanta2 (línea ~4152): celdas cuyo contenido es un término como LIMP, VAL, SETUP, MTTO (ATE) o MMTO, MTO, MAN (LIMA) se ignoran — no cuentan como fecha programada aunque estén dentro del rango de columnas de fecha.REA se excluye solo en Planta LIMA; SOP BEB, SOP P1, SOP P2 (desde la S43/2026 SOPLADO FCO P1 / SOPLADO FCO P2) se excluyen solo en Planta ATE.getHiddenRowSet / getHiddenColSet); para eso el Programa de Producción se lee con cellStyles: true, sin lo cual SheetJS no informa qué está oculto.^[56]\d{9}$ (10 dígitos, empieza con 5 o 6) — cualquier otra cosa en la columna "Código" se descarta.filtrarFechasFuturas() → isDentroDeVentanaProduccion() — solo se conservan fechas desde el lunes de la semana actual; lo programado solo en semanas anteriores se descarta como si ya no aplicara.buildDateColumnMap): algunas hojas de Planta LIMA (MENT, SEM, COS) traen el historial de todo el año, desde "SEMANA N° 45" del año anterior hasta la semana actual, con "SEMANA N° 53/54/55" en el cambio de año y números de día sueltos. El número de semana no dice el año y un bloque viejo caía en el año actual o el siguiente (caso real: el bloque "N° 53" de fin de 2025 terminaba en 01/01/2027 y MENTHOLATUM 6000003525 aparecía programado a futuro). Ahora el número de día del encabezado desambigua: si no coincide con la fecha reconstruida y sí con la misma semana un año antes (o después), se usa esa. Comprobado contra una lectura independiente en Python de ambos programas: mismos productos por hoja.switchPlanta(planta) (línea ~4837) cambia currentPlanta y llama a loadProdData():
'planta1' / 'planta2' → lee directamente prod_data_planta1 / prod_data_planta2.'consolidado' → loadProdDataConsolidado() (línea ~4792): trae ambas claves en paralelo y las fusiona en un solo array, etiquetando cada producto con planta: 'planta1' | 'planta2' (usado solo para mostrar de dónde viene, no afecta el cruce por código).updateConsolidadoBtnState(), línea ~4823, se llama después de cada carga/borrado). Si el usuario está en consolidado y una planta se queda sin datos, la app lo saca automáticamente de vuelta a Planta LIMA.Cada carga guarda también el Excel original en base64 (prod_archivo_original_<planta>) para poder re-descargarlo tal cual se subió (botón "✓ Cargado"), junto a la fecha/hora de última carga.
runValidacionMateriales()Solo necesita el Programa de Producción y los RMD de SAP; si falta alguno deja la tabla vacía con un mensaje que dice cuál. La primera vez del día espera a que cargue el motor de árboles ("Cargando el árbol de materiales de SAP…", esperarMotorArboles) y, al terminar, recalcula las DOS pestañas (antes solo la visible: la otra se quedaba con el aviso de carga al cambiar de pestaña); después cada validación tarda ~50 ms. Forecast (runValidacionForecast) usa exactamente la misma lógica.
Recorre productionData (respetando currentAreaFilter si no es 'TODAS') y arma, por código: sus áreas (materialAreas), sus plantas (materialPlantas, relevante en consolidado), su fecha de inicio más próxima (materialFechaMin, para ordenar la tabla y el chip de urgencia) y su descripción del programa (respaldo).
calcularArbolProduccion)El árbol se calcula hacia atrás desde el propio código: un producto terminado arranca en Acondicionado; un semielaborado del programa (ampolla llena, granel…) arranca en su propia etapa, así que solo cuentan su etapa y las anteriores. Cada árbol se calcula al momento (163 productos en ~5 ms) y nunca se reutiliza uno de otra sesión: siempre corresponde a la lista de materiales vigente.
Un código puede tener varias alternativas de lista de materiales y cada una varias versiones de fabricación. Cuentan solo las rutas vigentes:
Antes se tomaba siempre la alternativa de menor número aunque estuviera inactiva (casos reales: AKA-PRED 6000000814, cuya alternativa 1 inactiva no tenía Fabricación; NISTATINA 6000000997, cuya alternativa 1 inactiva solo tenía Acondicionado y por eso no se revisaban su Envase ni su Fabricación). Si hay varias rutas vigentes (dos líneas, dos versiones de fabricación), cada etapa lleva los materiales de todas (ej. DOLORAL FTE 6000003645: Fabricación 5000003208 por la versión 1101 y 5000003663 por la 1201). Si ninguna ruta es vigente se usan las que haya y se avisa en la ventana de la etapa.
evaluarEtapaArbol + estadoDeCadenaPrecisa)Para cada material que el árbol asigna a la etapa, su cadena son todos los RMD de ese material (como receta o como Código por Defecto) en esa etapa, de cualquier máster:
0. Másters de reacondicionado (REA): si el código tiene su propio máster, los másters generales de reacondicionado donde solo es una receta más se apartan (esMasterReacondicionado): su descripción empieza por REA o REAC como palabra, o por REACONDICIONADO (141 en SAP, casi todos de Acondicionado, con hasta 242 recetas; "REALIM 10 MG INY…" es un producto, no un máster REA). Se ven en la ventana de la etapa ("no cuenta: máster de reacondicionado (REA)" y, si alguno falta autorizar, "Falta autorizar el reacondicionado (no cuenta como pendiente)"), pero no deciden. Si el código solo tiene másters REA en esa etapa, deciden ellos (56 etapas, todas del Forecast). Caso real: MENTHOLATUM NF UNG POT85g 6000003527, Acondicionado v2 Autorizado en su máster; el máster REAC - GENERICO - CONSU AACORA01 v2 Ingresado ya no lo deja pendiente. La descripción de cada código también sale de su propio máster, no de un REA.
La etapa está pendiente si el material de alguna de sus rutas vigentes lo está.
Si un código no tiene lista de materiales en SAP (o no se pudo cargar el motor), solo se revisa el RMD de su propio código. Si su etapa es Fabricación no hay nada antes y la revisión está completa; si no, el producto sale para revisar con el motivo "Sin árbol de producción: …" y la nota "etapas anteriores sin verificar" (nunca se da por autorizado a ciegas). Si el motor no carga, un aviso fijo sobre la tabla lo dice y ofrece Reintentar (avisoMotorArboles).
Las etapas pendientes se filtran contra manualAutorizados (botón ✓ de una etapa). Si no queda ninguna, el producto pasa a autorizadosManual; si quedan algunas, sigue pendiente mostrando las que faltan más las autorizadas a mano (tachadas, con su estado real en SAP, "a mano · DD/MM" y el botón Revertir).
Una autorización manual caduca sola, sin DNI, cuando SAP resuelve la etapa (la autoriza) o registra una versión nueva de su código (o aparece pendiente un código que no lo estaba): así una autorización vieja nunca tapa un RMD nuevo. Al marcar ✓ se guarda, junto a la clave de siempre (val_manual_autorizados), su fecha y la huella de lo pendiente en SAP en ese momento: código, versión y número de RMD de cada código pendiente de la etapa (val_manual_autorizados_meta / val_manual_autorizados_forecast_meta; metaNuevaManual). aplicarCaducidadManuales() (línea ~6203) revisa todas las de Producción y Forecast en cada validación (revisarAutorizacionesManuales) y con cada sincronización, solo con la evaluación exacta (datos de SAP con recetas y árbol cargado); motivoCaducidadManual decide:
Las autorizaciones anteriores a esta versión (sin huella) reciben la suya la primera vez que se revisan; las de etapas que ya no están en el árbol de su producto reciben una huella vacía (si esa etapa llega a aparecer pendiente, caducan). Las claves sin etapa de una versión muy antigua (solo el código, 21 hoy) no tienen efecto y no se tocan. Lo que caduca queda en el historial como "Status RMD (automático)", en Cambios de SAP (sección 4.10) y, si pasó fuera de una sincronización, con un aviso discreto abajo a la izquierda (avisoDiscreto). Con los datos del equipo del 23/09/2026: de 260 autorizaciones de Producción, 163 caducan la primera vez (SAP ya autorizó esas etapas: hoy no tapan nada y los KPI no cambian).
ultimoResultadoValidacion = {
revisados, // total de materiales únicos evaluados
autorizados: autorizados + autorizadosManualLista.length,
pendientes: pendientesReales, // [{ material, descripcion, areas, etapasPendientes:[...], detallePreciso, sinArbol?, fechaProxima, plantas, motivo }]
autorizadosManual: autorizadosManualLista,
rmdPendientes: rmdPendientesTotal // SUMA de etapas pendientes individuales, no de productos
};
rmdPendientes es el número del KPI destacado "RMD Pendientes" — cuenta etapas, no productos. La lista se ordena por fechaProxima ascendente (lo más urgente primero).
chipSapEtapa): estado real en SAP (Ingresado v4, Suspendido v2, Solicitado v9, Sin RMD); con varias rutas, +N. El tooltip lista cada código con su RMD y su fecha de registro.edadPendienteHtml): días desde la Fecha Registro en SAP del RMD que decide la etapa (gris hasta 30 d, ámbar de 31 a 90, rojo desde 91). También en la ventana de la etapa ("hace N días") y en la hoja Detalle SAP del Excel ("Antigüedad (días)").aplicarFiltrosEtapa, candidatoPasaFiltros).mostrarDetalleEtapa → mostrarEtapaPrecisa): solo el código (o los códigos, uno por ruta vigente) de esa etapa y el RMD que decide su estado — número de RMD o de solicitud, versión, estado, fecha de registro y Observación (filaDetalleDeEntrada) —, la explicación de por qué está pendiente, las rutas del árbol con la etapa resaltada y, plegado, el historial de versiones de ese mismo código.hojaDetalleSap).frescuraDatosSap). Los límites se ajustan en SEMAFORO_SAP_AMBAR_DESDE_DIAS / SEMAFORO_SAP_ROJO_DESDE_DIAS. Se recalcula cada 30 min y al volver a la pestaña; el tooltip trae las fechas.Linaje, Código Receta…): reemplaza el maestro completo (datosPrecisosDesdeFilas). Se guarda en val_rmd_preciso (≈2 MB): una fila por RMD [rmd, linaje, versión, estado, etapa, agrupador, código por defecto, descripción, [[material, versión de fabricación]…], fechaRegistroMs, códigoSolicitud].fusionarDatosPrecisos): se conservan las recetas de la última sincronización, un estado solo avanza (Solicitado → Solicitud Aprobada/Rechazada → Ingresado → Autorizado → Suspendido/Cancelado; un Exportado más viejo no retrocede nada) y los RMD nuevos cuentan por su Código por Defecto hasta la próxima sincronización. Si no había maestro, se usa ese Exportado solo y la fuente dice "sin recetas".datosPrecisosDesdeDetalle).Resultado medido con los programas ATE del 17/09/2026 y LIMA de las semanas 38–40 y el maestro de SAP del 23/09/2026 (autorizados / pendientes / etapas pendientes): ATE, 65 productos, 22 / 43 / 69 con la antigua lógica por agrupador → 51 / 14 / 21; LIMA, 98 productos, 68 / 30 / 51 → 68 / 30 / 48 (65 / 33 / 51 antes de apartar los másters de reacondicionado: las 3 etapas de Acondicionado de MENTHOLATUM 6000003524/26/27); Forecast 400 / 372 / 608 → 398 / 374 / 609 (7 etapas se resuelven y 8 quedan pendientes porque su propio máster está Suspendido y antes solo las resolvía un máster REA). Los 163 productos tienen árbol; en LIMA 4 etapas tienen dos rutas vigentes. Verificación independiente (misma regla, incluida la de los másters REA, recalculada en Python desde el Excel crudo): 0 diferencias en 513 etapas y 517 códigos.
El script de Tampermonkey del portal (rmd-ui-mejoras.user.js ≥ v1.17; v1.18 envía además la hora de registro; repo AUTOMATIZACION-DE-RMD) añade junto a "Exportar" el botón Enviar a Status RMD: abre esta página en otra pestaña, lee el maestro completo con sus recetas usando la sesión ya iniciada del portal (el mismo servicio OData que usa su propio botón Exportar) y lo pasa con postMessage. Protocolo: el portal envía STATUS_RMD_PING hasta que esta página responde STATUS_RMD_LISTO (solo cuando terminó de cargar), luego RMD_SAP_MAESTRO ({ v:1, generado, columnas, filas }) y esta página contesta STATUS_RMD_RECIBIDO ({ ok, resumen | motivo }). Desde el script v1.19 el mensaje trae también usuarioSap ({ id, nombre, email }, leído del propio launchpad con sap.ushell.Container.getUser(); nunca una contraseña): la sincronización queda registrada en el historial con ese usuario ("NOMBRE (usuario SAP: correo)", más su identificador) en un solo clic, sin DNI (identidadDeUsuarioSap). Con un script anterior, o si el usuario no llega bien, se pide el DNI como antes. Se procesa con el mismo procesarFilasRmd que un Excel subido (recibirMaestroDesdeSap, línea ~6648) y la respuesta al portal incluye qué cambió. Tiempo medido: ≈8 s para 12 732 RMD (prueba real del 23/09/2026, sin escribir nada).
Por qué no un GET directo desde esta página a SAP: la API del portal exige la sesión SSO de la persona (cookies del dominio de SAP), el navegador bloquea las llamadas entre dominios (CORS) y la alternativa —guardar un usuario técnico o credenciales en este repositorio público— sería un riesgo de seguridad. El enlace por pestañas reutiliza la sesión que la persona ya tiene abierta y no toca credenciales.
Seguridad de mensajes (listener de message): STATUS_RMD_PING y RMD_SAP_MAESTRO solo se aceptan desde el origen exacto del portal (PORTAL_SAP_ORIGIN) y cuando la app ya está lista. Antes cualquier página que abriera esta en una ventana podía enviarle mensajes.
Tiempo real: si otro dispositivo guarda datos de validación nuevos (val_rmd_preciso, val_rmd_autorizados_detalle, val_flags), esta página los recarga (con 1,5 s de espera para agrupar las escrituras) y vuelve a validar. Las autorizaciones manuales y su huella se recargan juntas (0,8 s de espera) y val_cambios_sap actualiza el botón "Cambios de SAP".
Con cada carga de RMD (sincronización con SAP o Exportado), registrarCambiosSap() (línea ~6372) compara, para los productos de los programas de ATE y LIMA y del Forecast, cada etapa de su árbol con los datos anteriores y con los nuevos (evaluarMaterialPreciso(material, P) acepta el índice a usar): etapas que SAP autorizó, nuevas etapas pendientes y etapas pendientes con otro RMD o estado, más las autorizaciones manuales que caducaron. Se guarda la última comparación en val_cambios_sap (con fecha, origen y quién sincronizó) y se ve:
cambiosSapDeVista): en Producción no aparecen los productos que solo están en el Forecast ni las autorizaciones manuales del Forecast, y al revés; un producto que está en los dos se ve en ambas. El resumen del aviso "¡Listo!" y el que vuelve al portal son los de Producción. El botón abre la lista por secciones y permite Descargar Excel (mostrarCambiosSap, descargarCambiosSapExcel).
Si antes no había datos, o no se pudo cargar el árbol, lo dice en vez de comparar. Si se pasa de un Exportado sin recetas a datos con recetas (o al revés) avisa de que parte de los cambios se deben al cambio de método.El botón Enviar a Status RMD del portal (script ≥ v1.95) trae, junto con el maestro (msg.opSap), las órdenes de producción de Planta ATE (centro 1020) y Planta LIMA (1021) que inician hoy y en los 14 días siguientes, sin las cerradas técnicamente (CTEC): orden, material, versión de fabricación, clase (FABR, ENVS, ACON, REAC…), inicio, fin, lote y, si ya lo tiene, el RMD de la orden. Se guardan en val_op_sap (con un script anterior no llegan y se conservan las de la última sincronización).
Una orden queda sin RMD con la misma regla del engranaje "OP sin RMD" del portal: ningún RMD Autorizado del maestro tiene como receta su material con esa versión de fabricación, y la orden todavía no tiene RMD (opsSinRmdDe, con el índice indiceVersionesFab). Se evalúa con el maestro guardado, así que un Exportado RMD posterior la actualiza; con un maestro sin recetas no se evalúa. Entran las de reacondicionado (clase REAC), que no van en el programa y deben estar en un máster genérico de reacondicionado.
En Producción (repartirOpsSinRmd):
No cambian los KPI. Cuentan en el filtro Pendientes, se buscan también por número de orden, el tooltip explica cada orden (RMD más reciente con esa receta, versiones de fabricación que sí están autorizadas) y el Excel lleva la hoja OP sin RMD (hojaOpsSinRmd). Prueba con datos reales del 25/10/2026: 549 órdenes de ATE y LIMA; 5 sin RMD en ATE y 15 en LIMA (una de reacondicionado, GENTS CLASICO 6000003038, Solo en SAP).
Todo el sistema de etapas gira en torno a una lista fija, en el orden de producción (en los 4.528 árboles de SAP revisados, Inspección va siempre DESPUÉS de Envase: inyectables Fabricación › Envase › Inspección › Acondicionado). Es también el orden de las etapas en la tabla y de las columnas del Excel:
const ETAPAS_FIJAS = ['FABRICACION', 'RECUBRIMIENTO', 'ENVASE', 'INSPECCION', 'ACONDICIONADO'];
Los Excel de origen escriben la etapa con texto libre y variable (ej. "Acondicionado Final", "ENV."), así que mapEtapaAFija() (línea ~5676) usa un diccionario de alias (ETAPA_ALIASES) más coincidencia parcial para normalizar cualquier variante al valor fijo correspondiente. Si el texto menciona más de una etapa fija a la vez (dato corrupto de origen), se trata como no reconocible en vez de adivinar.
En la tabla, cada etapa pendiente de un producto tiene un <select> de Estatus (línea ~5357):
const ESTATUS_ETAPA_OPCIONES = ['PENDIENTE-PRO', 'POR INGRESAR', 'FLUJO SAP', 'POR REVISION', 'PEND CC'];
const ESTATUS_ETAPA_RESPONSABLE_DEFECTO = {
'PENDIENTE-PRO': 'PRO', 'POR INGRESAR': 'DOC', 'FLUJO SAP': 'PRO', 'POR REVISION': 'DOC'
// 'PEND CC' NO tiene default: es responsabilidad compartida ASC/PRO, el usuario debe elegir a mano
};
POR REVISION se llamaba PEND RMD OFICIAL hasta oct 2026 (se cambió para que coincida con la casilla de la lámina 3 del status). Lo guardado con el nombre anterior en val_estatus_etapa y val_estatus_caracterizacion se lee como POR REVISION (estatusVigente) y se guarda así con el próximo cambio.
window.setEstatusEtapa(material, etapaFija, valor) (línea ~5379) es el único punto que escribe en estatusEtapa y responsablesEtapa a la vez: el responsable ya no es editable directamente (dejó de ser un <select> libre), se deriva siempre del estatus elegido, salvo en PEND CC donde queda un segundo <select> (ASC/PRO) obligatorio antes de poder autorizar esa etapa (toggleManualAutorizadoEtapa lo bloquea explícitamente si falta elegir).
Cada vez que se carga un Excel de Producción y ya existía una versión anterior guardada de esa misma planta, registrarSnapshotProduccion() (línea ~3344) compara ambas versiones material-por-área (claveMaterial = codigo::area) y clasifica cada diferencia en:
Se guardan hasta 30 snapshots por planta (prod_historial_planta1 / prod_historial_planta2). El botón "Historial" del calendario de Producción abre un modal (mostrarHistorialTrazabilidadProduccion) con pestañas Planta ATE / Planta LIMA / Consolidado (el consolidado simplemente intercala los snapshots de ambas por fecha, no tiene almacenamiento propio) y un selector para navegar entre snapshots.
Los snapshots solo guardan lo que cambió y se pierden pasado el número 30; lo que entró alguna vez al programa queda además en prod_acumulado_planta1/2 (sección 14, Programa acumulado). La primera carga tras «Eliminación de datos» no deja snapshot (no hay versión anterior con qué comparar).
El árbol es la base de la validación (sección 4.2). Esta sección describe de dónde sale.
Un producto real de SAP se descompone en los nodos de su lista de materiales (BOM): Fabricación (semielaborado, a veces compartido entre presentaciones), Recubrimiento, Envase, Inspección y Acondicionado, cada uno con su propio código. Se calcula con el motor local: se descargan 4 tablas (mm_bom, mm_bom_componentes, mm_vfab, mm_materiales) desde el proyecto de Supabase de "Listas de materiales" (distinto al de la app), se guardan hasta 12 h en IndexedDB y se corre aquí mismo el motor de cálculo de esa página (arbol-motor.js, cargado como <script> en el <head>; obtenerMotorArbolLocal, línea ~5559). precalentarMotorArbolLocal() (línea ~5611) dispara la descarga en segundo plano justo después de la carga inicial. La API remota (/api/arbol en Vercel) y el antiguo "filtrado por árbol" por Código Agrupador (descartes por vista, filtrarTodosPorArbol…) ya no existen: con el árbol como base de la validación no hacían falta. La caché del navegador que usaban (arbolMaterialesCacheV2) se libera al abrir la página.
renderTablaPendientes(filas) (línea ~7354) recibe la lista combinada de pendientes + autorizados manuales (ultimoResultadoValidacion) y:
window.__ultimasFilasValidacion (para que los filtros de texto/estado/fecha puedan re-renderizar sin recalcular la validación completa — ver refiltrarTablaPendientes()).validacionFiltroEstado: todos/pendientes/autorizados), búsqueda de texto (validacionBusqueda, con debounce de 150ms en el input), filtro de fechas puntuales (validacionFiltroFechas, un popup tipo Excel sobre la columna "Inicio de Producción") y los filtros por etapa (Etapa, Estado SAP, Antigüedad; sección 4.7).htmlEtapasDeFila(p, origen) (línea ~7307), la misma para Producción y Forecast: un bloque .etapa-fila-grid por etapa pendiente (chip de SAP + antigüedad, <select> de Estatus, responsable derivado, botón ✓) y otro por cada etapa autorizada a mano (tachada, con su chip de SAP, "a mano · DD/MM" y "Revertir"). En una fila "Autorizado manual" cada etapa se muestra una sola vez (antes salía repetida: como pendiente tachada y como autorizada).urgenciaInicioHtml() (línea ~7193): un chip visual según cuántos días faltan hasta la fecha de inicio de producción (urg-vencido si ya pasó, urg-hoy, urg-pronto si son 1–3 días, urg-normal en otro caso).gruposRmdPendientes, refsRmdPendientes): si la misma etapa de varios materiales está pendiente por el mismo RMD (un máster con varias recetas; p. ej. RMD 2202609142 de NITROFURANTOINA 100 mg, Acondicionado, para 6000003412, 6000005827 y 6000006138), va en UNA fila con todos los códigos («1 RMD · 3 materiales») y cuenta una vez en RMD PENDIENTES, Pendientes, Forecast, Ruta 1 y el Programa acumulado de INDICADORES. PRODUCTOS PENDIENTES sigue contando productos. La fila usa el Estatus del código del RMD (su Código por defecto si está entre ellos; si no, el menor), pero NO se borra nada de los demás códigos: las claves del grupo quedan como hermanas (hermanasClave, registrarGrupoClaves), lo que se elige en una (Estatus, responsable, autorización a mano) se escribe en todas, y la que no tenga Estatus toma el más avanzado del grupo. (Hasta el 2026-11-03 se movía todo al código del RMD; una vista que no juntaba esos materiales, como Producción de una planta frente a otra vista, perdía el estatus al recargar.) Caracterización sigue una fila por producto (agruparRmd: false) pero con el Estatus del código del RMD. El Excel lista todos los códigos en Material.actualizarContadoresPendientes) y el contador del panel (actualizarContadorPanelRmd).Todos se recalculan juntos desde updateDashboardProd() (línea ~4864), que es el punto de entrada que se llama tras cualquier cambio relevante (carga de archivo, cambio de filtro de área, cambio de planta):
function updateDashboardProd() {
runValidacionMateriales();
renderFiltersProd(); renderKPIsProd(); renderCalendarProd(); renderUpcomingProd();
renderAreaDistributionChart(); renderPie3DChart(); renderTrendChartProd(); renderSummaryProd();
// + actualiza el chip "N desde hoy" junto al título del panel
}
renderCalendarProd() (línea ~4930): un evento de FullCalendar por cada (producto × fecha programada). Color por área (getAreaColor). Si el material ya no está en la lista de pendientes actual, el evento lleva un ✓ verde superpuesto (yaAutorizado).renderUpcomingProd() (línea ~5039): lista lateral de las próximas fechas (desde "hoy" o la fecha que el usuario elija en el filtro "Desde"), con buscador por código/producto. Misma lógica de "✓ ya autorizado" que el calendario.renderAreaDistributionChart() / renderPie3DChart() / renderTrendChartProd() (línea ~5148 en adelante): barras horizontales por área, dona de distribución %, y línea de tendencia de los próximos 21 días. Los tres respetan currentAreaFilter y usan getAreaColor() para mantener el mismo color de área en toda la pestaña. Se redibujan también al alternar tema oscuro/claro (para tomar los colores correctos del tema nuevo).getAreaColor(area) (línea ~4147): busca coincidencia contra un mapa fijo de colores por prefijo de área (ordenado de la clave más larga a la más corta, para que "SOL HORM" no caiga en el color de "SOL"); si el área no está en el mapa, genera un color determinístico por hash sobre una paleta de respaldo — así un área nueva que aparezca en un Excel futuro siempre obtiene un color, y siempre el mismo.exportPendientesToExcel() (línea ~7992): genera un .xlsx con estilo (usando xlsx-js-style) a partir de la lista de pendientes actual, con una hoja de detalle y una hoja resumen (construirHojaResumenMacro, con una matriz Actividad × Responsable).copyPendientesToClipboard() (línea ~8256): copia la tabla como texto tabulado (pegable directo en Excel).mostrarGraficoResponsableModal('produccion') (línea ~8191): gráfico de barras + dona de carga de trabajo pendiente por responsable, descargable como imagen (generarImagenGraficoContraste, vía html2canvas/Chart.js render a imagen).renderSummaryProd() (línea ~8993): arma el bloque de "Resumen" textual/gráfico que aparece al pie de la pestaña, reutilizando los mismos canvases que los gráficos principales.Si vas a tocar Producción, estos son los patrones defensivos que ya están instaurados y conviene respetar:
await. processFileProd guarda plantaObjetivo = currentPlanta al entrar; varias funciones de guardado comparan un "token" incremental (__loadProdDataToken) para descartar respuestas que llegaron tarde tras un cambio de planta a mitad de camino.storeSet/storeGet son el único canal de persistencia. Nunca se llama a fetch(SUPABASE_URL...) ni a dbStore directamente fuera de esas dos funciones (y sus primas storeRemove, storePendingMark).runValidacionMateriales() (o updateDashboardProd() completo) en vez de parchear el DOM a mano — es más caro en CPU pero elimina toda una clase de bugs de estado desincronizado. Las excepciones puntuales (como refiltrarTablaPendientes, que re-renderiza sin recalcular la validación) están para los filtros de sólo-vista (texto, fechas, estado) que no cambian el resultado subyacente.`${material}::${etapaFija}` (responsableKey): Estatus, responsable, autorización manual y su huella usan la misma.avisoMotorArboles), en vez de guardar "sin árbol".availableAreas se recalcula en cada carga a partir de wb.SheetNames, así que agregar/quitar una hoja en el Excel de origen cambia automáticamente los filtros de área sin tocar código — pero también significa que un typo en el nombre de una hoja crea un área "fantasma" nueva en vez de fusionarse con la existente.| Quiero cambiar... | Mirar |
|---|---|
| Qué cuenta como "pendiente" / cómo se cruzan las fuentes | runValidacionMateriales() — línea 7005; evaluarEtapaArbol() (6111) y calcularArbolProduccion() (6060) |
| Cómo se detectan fechas/hojas en el Excel de Producción | processFileProd() — línea 4504, y los build*DateColumnMap* alrededor de 4244–4440 |
| Las etapas del proceso o sus alias | ETAPAS_FIJAS, ETAPA_ALIASES, mapEtapaAFija() — línea ~5660 |
| Estatus disponibles / responsable por defecto | ESTATUS_ETAPA_OPCIONES, ESTATUS_ETAPA_RESPONSABLE_DEFECTO — línea ~5357 |
| Cómo se ve cada fila de la tabla principal | renderTablaPendientes() (7354) y htmlEtapasDeFila() (7307) |
| Colores/urgencia de la tabla | urgenciaInicioHtml() (7193), edadPendienteHtml(), getAreaColor() (4147), clases CSS .urg-* / .edad-pend* / .val-kpi* en el <style> |
| Calendario / "Próximas a producir" / gráficos | renderCalendarProd(), renderUpcomingProd(), render*ChartProd() — línea ~4930–5280 |
| Másters de reacondicionado (REA) | esMasterReacondicionado() / PATRON_MASTER_REA y estadoDeCadenaPrecisa() |
| Cuándo caduca una autorización manual | motivoCaducidadManual() y aplicarCaducidadManuales() — línea ~6203 |
| Qué cambió con una sincronización | registrarCambiosSap() (6372), compararEvaluaciones(), mostrarCambiosSap() (6410) |
| Semáforo de los datos de SAP | frescuraDatosSap() (6468) y las constantes SEMAFORO_SAP_* |
| Enlace con el portal SAP / usuario de SAP | recibirMaestroDesdeSap() (6648), identidadDeUsuarioSap() |
| Historial de cambios entre cargas | registrarSnapshotProduccion() / renderHistorialTrazabilidadModal() — línea ~3344–3460 |
Reemplaza los Word «TODAS LAS ETAPAS» y «CAMBIOS DE CALIDAD DE OPERACIONES»: los acuerdos se escriben a mano (no se cargan de los Word), aquí o en el portal SAP (Configuración Maestra › Acuerdos). Cada acuerdo tiene un texto libre y puede llevar pasos de referencia e imágenes. Dice qué paso debe ir en qué sección y etapa del RMD (con Tipo dato, R. Por, V°B° y Estado CC), para qué planta (Planta 1 ATE, Planta 2 LIMA o ambas), la fase a la que aplica (cualquiera, solo fase 1, o fase 2, que incluye 2R; la fase sale de la 1.ª línea de Observaciones) y una condición opcional en texto y quién lo pidió.
rmd_dashboard_kv, una fila por acuerdo con la key acuerdo:<id> (así dos personas editando acuerdos distintos no se pisan). No hace falta crear tablas nuevas. Realtime avisa a las otras PCs y recargan la lista (Acuerdos.recargarPronto, llamado desde onRemoteKeyChanged). La lista también queda en la caché local (acuerdos_cache) por si no hay red.localStorage (acuerdos_usuario). Cada cambio queda en el historial del acuerdo.imagenes: [{ id, nombre, src }], hasta 8).Acuerdos, Pasos, Historial e Imagenes (cada imagen en trozos de 30 000 caracteres; el portal usa el mismo formato). Al importar se compara por ID: los nuevos se agregan y los existentes solo se reemplazan si el archivo trae una edición más reciente.acuerdo:* (pestaña «Acuerdos» de Configuración Maestra, desde la v1.103.0) y avisa si un RMD no cumple.const Acuerdos = (() => { … })() al final de index.html.Arma lo mismo que la plantilla STATUS ACTUALIZACIONES RMD – PROGRAMA Y CARACTERIZACIONES y la descarga llena (Descargar PowerPoint). El periodo del status se elige arriba (por defecto, del lunes de esta semana a hoy); el mes del status es el de la fecha final. Todo el código está en el bloque const StatusRmd = (() => { … })() al final de index.html.
META_AUTORIZADOS). En el área solo el Acumulado y los Autorizados se escriben a mano; aquí todo sale del portal RMD al pulsar «Enviar a Status RMD» (no hace falta ningún Excel), con las fechas de SAP de cada versión de RMD Ingresada, Autorizada o Suspendida (sin Cancelados ni solicitudes):
cuadroDesdeVersiones, cuadroPrograma).produccionEstado que manda el script ≥ 1.109.0) u Observación «PEND JEFE/GER», y los de control de cambio (Observación «PEND CC», o Ingresado con Fecha Autorización: se reabrió). Es una foto: el de cada mes cerrado queda fijo con la última sincronización hecha en ese mes (); los meses cerrados antes de esto se aproximan con lo de hoy. Meta 80 % para los dos.| Exportar a Excel / copiar tabla | exportPendientesToExcel() (7992), copyPendientesToClipboard() (8256) |
| Persistencia / sync multi-dispositivo | storeSet/storeGet (3038), iniciarSupabaseRealtime()/onRemoteKeyChanged() (3145–3240) |
fijoval_rmd_indicadores_mes ({ v: 3, generado, calculado, porMes: { 'AAAA-MM': { P1: { aut, autBk, sol, susp, pend, fuera, fijo, reg }, P2 } }, det: { 'AAAA-MM': { P1: { pend, aut, sol, susp, fuera: [ids] } } }, vers: { id: [planta, código, solicitud, versión, etapa, estado, descripción, fSol, fReg, fIng, fAut, fSusp, producciónEstado, códigoPorDefecto, observación (300), fuera, conIngreso] } }), comprimido. El detalle de cada número sale de ahí (ya no depende de val_rmd_autorizados_detalle). Con datos v: 2 se recalcula al vuelo con el detalle guardado si trae la planta; si no, pide «Enviar a Status RMD».val_rmd_autorizados_detalle pasó de 5 MB al traer planta y fechas y Supabase dejó de aceptarlo sin avisar (se quedó con el del 29/10/2026, sin planta: por eso el detalle del cuadro no abría). storeSet sube comprimidas (gzip + base64, { __gz }) las claves de CLAVES_COMPRIMIDAS (ese detalle y el cuadro) y storeGet las descomprime.CONTAR_SUSPENDIDOS = false, CONTAR_SOLICITUDES_ABIERTAS = false, REGLAS_CUADRO = 3) más los masivos pendientes que el área escribe a mano por mes y planta en la tabla manual del final (ind_horas_efectivas[mes][planta].ma); se suman al mostrar el cuadro, también en meses congelados. Cuando el área acuerde marcar los masivos en SAP, la casilla manual dejará de hacer falta.CONTAR_SUSPENDIDOS_SIN_FECHA = false). Una fecha de ingreso real futura (error de tipeo en la Observación) se ignora.reglas = REGLAS_CUADRO; si cambian las reglas, se vuelve a calcular) (porMes[mes][planta].cong, con su detalle) y cada sincronización lo copia igual; lo que cambie después (una autorización con fecha atrasada, una corrección) entra al mes siguiente por «± Otros».ind_horas_efectivas: { 'AAAA-MM': { P1: { an, as } } }, se relee antes de guardar), con el total, los autorizados del cuadro y autorizados por 100 h, para mostrar lo que se autorizó con las horas que hubo.evaluarMaterialPreciso, autorizaciones manuales incluidas). Autorizado = etapas resueltas; los pendientes se reparten por su Estatus: PENDIENTE-PRO = Borrador (PRO), POR INGRESAR = Ingresar (QA-DOC), FLUJO SAP (PRO), POR REVISION (DOC), PEND CC = Control de cambio (PRO o ASC, el elegido). Los que no tienen Estatus van como «sin estatus» bajo PENDIENTES. Cada planta empieza con dos totales de color: Programa (pendientes + autorizados del mes) y Backlog (los pendientes del cuadro al cierre del mes anterior). El % de avance va en dos cuadritos: Programa del mes (autorizados / total del programa) y Backlog DOC (autorizados del backlog en el mes / backlog). La fecha de la lámina es el rango del Programa de PLA: el horizonte de 2 semanas desde la semana en que se cargó el Programa de Producción de cada planta (lunes de la semana de prod_archivo_original_<planta>.fechaCarga + 13 días; si las plantas difieren, uno por planta). Los totales salen siempre del último programa cargado: si se quitan o cambian productos, cambian. A la derecha del flujo, el backlog por semana (de lunes a domingo, S40, S41…) de las 4 semanas anteriores y la del status, del Programa acumulado: también cuenta lo que sigue pendiente de productos que ya salieron del programa, así que puede ser mayor que PENDIENTES. En la lámina las semanas van en columnas (número y lunes, en dos líneas) y los conceptos en filas: con las semanas en filas, PowerPoint partía los títulos de las columnas.StatusRmd.caracterizacionesDesdeFilas, llamado desde procesarFilasRmd) en val_rmd_caracterizaciones ({ items: [{ id, codigo, sol, version, estado, descripcion, etapa, planta, fechaSolicitud, fechaAutorizacion }] }, id = Código de Solicitud o, si no tiene, Código). Autorizado o Suspendido en SAP = AUTORIZADO; una Solicitud Rechazada no cuenta; el resto es pendiente con el Estatus que se elige en la tabla «Caracterizaciones» de la propia pestaña (mismos estatus que Producción; en PEND CC se elige PRO o ASC). Lo elegido se guarda en val_estatus_caracterizacion ({ id: { e: estatus, r: responsable de PEND CC, f: fecha } }; antes de escribir se vuelve a leer para no pisar lo que otra persona cambió). Sin Estatus = «sin estatus» bajo PENDIENTES. Si los datos de SAP no traen ningún RMD con ese motivo, la sección lo avisa.prod_acumulado_planta1 / prod_acumulado_planta2 (acumularCargaPrograma): { v: 1, inicio, detalleDesde, actualizado, cargas: [ISO…], items: { código: { p: producto, a: [áreas], e: entró, u: última carga en que estaba, s: primera carga en que ya no estaba (null si sigue), n: cargas } } }. Mientras no exista se reconstruye, sin guardar, desde prod_historial_*, prod_data_* y las cargas anotadas en historial_trazabilidad (reconstruirAcumulado); la primera carga con esta versión guarda lo reconstruido más esa carga. Cada etapa del árbol de cada producto que alguna vez entró cuenta como pendiente desde que el producto entró hasta que su RMD se autoriza en SAP o se autoriza a mano en Producción (StatusRmd.calcularAcumulado: el mismo estadoDeCadenaPrecisa de Producción, aplicado al estado que tenía cada RMD en cada fecha). Si vuelve a quedar pendiente (versión nueva o suspensión) es un pendiente nuevo, salvo que se reabra en menos de 24 h; si se cierra sin autorización (versión nueva cancelada, solicitud rechazada) va a «Sin autorizar». Por mes o por semana (botones Por mes / Por semana): Backlog + Nuevos = Total pendiente; Total − Autorizados − Sin autorizar = Nuevo backlog, que es el backlog del periodo siguiente (flujosPorPeriodo). Con gráfico (barra apilada backlog + nuevos, barra de autorizados y línea del nuevo backlog), tabla y una lista del backlog de hoy (estado en SAP y motivo leído de la Observación: Control de cambio, Jefe / Gerencia, Fase I, Borrador, Por ingresar u Otro) o de los autorizados de un mes o una semana. Las fechas de cada cambio salen de la Trazabilidad de SAP, que manda el script ≥ v1.108.0 con «Enviar a Status RMD» y se guarda en val_rmd_trazabilidad ({ v: 1, generado, desde, items: { 'código|solicitud|versión|ETAPA': [[segundos, letra]…] } }, letras S/A/R/I/U/X/C = Solicitado, Solicitud Aprobada, Solicitud Rechazada, Ingresado, Autorizado, Suspendido, Cancelado; el script la lee desde el 01/01/2026 y cada envío se suma al anterior). Un RMD sin cambios en la Trazabilidad leída ya estaba como hoy. Sin ella (script anterior) se aproximan con la Fecha Solicitud, Registro y Autorización de SAP —en un Suspendido la Fecha Autorización es la de su última autorización y se toma suspendido cuando se autorizó una versión posterior del mismo máster (suspensionesAproximadas)— y la sección lo avisa.StatusRmd._ultimo()): las rutas y el acumulado guardan qué cuentan (items, periodos) y el cuadro repite sus cuentas sobre las versiones de RMD guardadas (rmdAutorizadosDetalle, que desde esta versión trae la planta de cada fila, cuadroDesdeVersiones); si los números no coinciden (datos guardados de otra sincronización), la ventana lo avisa. Con datos guardados de antes de esta versión, el detalle del cuadro aparece con el próximo «Enviar a Status RMD».StatusRmd.exportarBacklogMes): pide una fecha de corte (por defecto hoy; el mes vigente es el de esa fecha) y descarga Backlog_RMD_<AAAA-MM>_corte_<fecha>.xlsx con el formato del Excel del área: hoja Resumen con 5 tablas (1 Pendientes por planta; 2 Composición: Ingresados / Solicitados / Suspendidos, con meses anteriores + mes vigente; 3 Suspendidos vs INACTIVOS; 4 Suspendidos por prioridad; 5 Autorizados del mes: Backlog / Solicitados del mes) y una hoja de detalle por tabla, con filtros y una columna «Explicación» que dice por qué cada RMD cuenta donde cuenta. Sale de las versiones de la última sincronización (rmdAutorizadosDetalle) y de la Trazabilidad (val_rmd_trazabilidad). Reglas: un Ingresado cuenta en el mes de su Fecha Registro, salvo que su versión anterior siga Suspendida desde un mes anterior (entonces cuenta en el mes de esa suspensión; si la anterior se reactivó, manda la Fecha Registro); un Solicitado (solo estado «Solicitado») igual con la Fecha Solicitud; un suspendido que el equipo mantiene cuenta en el mes de su suspensión, salvo que ya tenga una versión nueva Ingresada o Solicitada (cuenta esa, no los dos) o una versión nueva Autorizada después de su suspensión (ya se resolvió). La versión anterior es la de mayor versión registrada con la misma planta, etapa y descripción (o el mismo código por defecto si no hay descripción igual). Prioridad = año de la suspensión. Autorizados del 1 del mes al corte que siguen Autorizados: Backlog si se registraron en un mes anterior (regla 2) o si su versión anterior se suspendió en un mes anterior (regla 1); si no, Solicitados del mes (regla 3). La fecha de suspensión de los suspendidos revisados por el equipo es la de su Excel; la de los demás, la de la Trazabilidad (desde 2026; sin fecha = antes de 2026).DECISION_EQUIPO_TXT, números finales del 07/11/2026, Excel «REVISAR 8», columna CONCLUSION): 1 806 masters, 1 336 a INACTIVO (ATE 900, LIMA 436) y 470 siguen suspendidos (ATE 316, LIMA 154), con su fecha de suspensión, último lote, lotes, forecast, OP y sugerencia inicial. De ahí salen RMD_INACTIVOS y RMD_SUSPENDIDOS_EQUIPO (antes 1 328 / 478), que usan también el cuadro del mes y los indicadores.StatusRmd.recargarPronto, 2,5 s de espera). Las claves nuevas (val_rmd_indicadores_mes, val_rmd_caracterizaciones, val_estatus_caracterizacion, prod_acumulado_planta1/2, val_rmd_trazabilidad) van en el respaldo JSON.index.html (STATUS_RMD_PLANTILLA_B64): es la del equipo con marcadores {{…}} en lugar de los números, sin la diapositiva 4 (era una copia de la 3) y con los recuadros de % de avance y de control de cambio un poco más anchos para que no se partan. En la diapositiva 2 hay tres indicadores por planta (solicitados, autorizados y % autorizados con la meta), el gráfico es más bajo y debajo va el cuadro del mes, con el mismo margen que a los lados; en la 3 el flujo es más angosto y a su derecha va el backlog por semana (y el mes del subtítulo ya no va en rojo). Esas tablas son recuadros marcadores {{TABLA_S2_P1}}… que tablaEnDiapositiva cambia por tablas de PowerPoint con la misma posición y tamaño; signo y palabra van unidos con espacio duro y los anchos están medidos con Raleway para que PowerPoint no parta las palabras. Para cambiar el diseño, se edita la plantilla en PowerPoint conservando los marcadores y se vuelve a pasar a base64. JSZip se carga de cdnjs solo al descargar.