V43 + Optimización extrema de velocidad de detección (textContent + 10ms polling).
// ==UserScript==
// @name Reporte Vacunación MSPAS (V44 - Zero Latency)
// @namespace http://tampermonkey.net/
// @version 44.0
// @description V43 + Optimización extrema de velocidad de detección (textContent + 10ms polling).
// @author Gemini AI
// @match *://*.oraclecloudapps.com/ords/r/vacunacion/vacunacion/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
/* jshint esversion: 6 */
// ================= CONFIGURACIÓN =================
// PEGA AQUÍ TU URL DE GOOGLE APPS SCRIPT
const URL_GOOGLE_SHEET = "https://script.google.com/macros/s/AKfycbzT3rWlONdKIOQfbaTqABKA1Y4PNj4iZ1LvVZsOVIV_Xi_IKIUv5_99gKIOrzj9XlQQ/exec";
// =================================================
const ID_NOMBRE_DISPLAY = "P701_NOMBRE_DISPLAY";
const ID_CUI_DISPLAY = "P701_CUI_P_DISPLAY";
const ID_SEXO_DISPLAY = "P701_SEXO_DISPLAY";
const ID_NAC_DISPLAY = "P701_NACIONALIDAD_DISPLAY";
const ID_FEC_NAC_DISP = "P701_FECHA_NACIMIENTO_DISPLAY";
const ID_FECHA_INPUT = "P702_FECHA_VACUNA_input";
const ID_VACUNA_SELECT = "P702_ID_CONFIGURACION_VACUNA_DOSIS";
const TEXTO_EXITO = "El registro ha sido procesado exitosamente";
const ID_INPUT_SERVICIO_PARAM = "P216_IDTS";
// --- UTILS ---
async function obtenerFechaHoraReal() {
try {
const response = await fetch(window.location.href, { method: 'HEAD', cache: 'no-store' });
const dateHeader = response.headers.get('Date');
if (dateHeader) return new Date(dateHeader);
} catch (e) { }
return new Date();
}
function obtenerDatos(tipo) {
const key = tipo === 'regular' ? 'reporte_regular_diario' : 'reporte_vacunacion_diario';
return JSON.parse(localStorage.getItem(key) || '[]');
}
function obtenerAcumulado(tipo) {
const key = tipo === 'regular' ? 'reporte_regular_acumulado' : 'reporte_vacunacion_acumulado';
return JSON.parse(localStorage.getItem(key) || '[]');
}
function getServicioGuardado() { return localStorage.getItem('mspas_servicio_actual') || "Detectando..."; }
function guardarRegistro(nuevoRegistro, tipo) {
nuevoRegistro.synced = false;
nuevoRegistro.wasEdited = false;
const diario = obtenerDatos(tipo);
diario.push(nuevoRegistro);
const keyDiario = tipo === 'regular' ? 'reporte_regular_diario' : 'reporte_vacunacion_diario';
localStorage.setItem(keyDiario, JSON.stringify(diario));
const acumulado = obtenerAcumulado(tipo);
acumulado.push(nuevoRegistro);
const keyAcum = tipo === 'regular' ? 'reporte_regular_acumulado' : 'reporte_vacunacion_acumulado';
localStorage.setItem(keyAcum, JSON.stringify(acumulado));
localStorage.setItem('mspas_fecha_activa', new Date().toLocaleDateString('es-GT'));
window.dispatchEvent(new Event('storage'));
}
function mostrarNotificacion(mensaje, colorFondo = "#27ae60", duracion = 3000) {
const notif = document.createElement('div');
notif.innerText = mensaje;
notif.style = `position: fixed; top: 20px; right: 20px; background: ${colorFondo}; color: white; padding: 12px 20px; border-radius: 5px; box-shadow: 0 4px 10px rgba(0,0,0,0.3); z-index: 999999; font-family: Arial; font-weight: bold; opacity: 0; transition: opacity 0.3s;`;
document.body.appendChild(notif);
requestAnimationFrame(() => { notif.style.opacity = "1"; });
setTimeout(() => { notif.style.opacity = "0"; setTimeout(() => notif.remove(), 300); }, duracion);
}
function capturarDatosCompletos() {
const leer = (id) => {
let el = document.getElementById(id);
if (!el && window.parent && window.parent.document) {
el = window.parent.document.getElementById(id);
}
return el ? el.innerText.trim() : "";
};
const rawNac = leer(ID_FEC_NAC_DISP);
let fechaNac = rawNac;
let edad = "";
if (rawNac.includes("(")) {
const p = rawNac.split("(");
fechaNac = p[0].trim();
edad = p[1].replace(")", "").trim();
}
if (edad === "") {
const hiddenEdad = document.getElementById("P701_EDAD_ANOS");
if(hiddenEdad) edad = hiddenEdad.value;
}
return {
servicio: getServicioGuardado(),
cui: leer(ID_CUI_DISPLAY),
nombre: leer(ID_NOMBRE_DISPLAY),
sexo: leer(ID_SEXO_DISPLAY),
nacionalidad: leer(ID_NAC_DISPLAY),
nacimiento_fecha: fechaNac,
nacimiento_edad: edad
};
}
// --- SINCRONIZACIÓN GLOBAL ---
window.sincronizarDrive = function() {
if (URL_GOOGLE_SHEET.includes("PON_AQUI")) return alert("⚠️ Configura la URL de Google Sheets en el script.");
const regularAll = obtenerAcumulado('regular').map(r => ({...r, synced: r.synced === true}));
const otrasAll = obtenerAcumulado('otras').map(r => ({...r, synced: r.synced === true}));
const pendientesRegular = regularAll.filter(r => !r.synced);
const pendientesOtras = otrasAll.filter(r => !r.synced);
const payload = [...pendientesRegular, ...pendientesOtras];
if (payload.length === 0) return mostrarNotificacion("✅ Todo sincronizado.", "#2980b9", 3000);
if(!confirm(`Se enviarán ${payload.length} registros a Drive. ¿Continuar?`)) return;
mostrarNotificacion("☁️ Enviando...", "#f39c12", 10000);
fetch(URL_GOOGLE_SHEET, {
method: "POST", mode: "no-cors", headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
}).then(() => {
const updateSync = (key) => {
let data = JSON.parse(localStorage.getItem(key)||'[]');
data = data.map(item => {
if (payload.some(p => p.cui === item.cui && p.hora === item.hora && p.vacuna === item.vacuna)) {
item.synced = true; item.wasEdited = false;
}
return item;
});
localStorage.setItem(key, JSON.stringify(data));
};
updateSync('reporte_regular_diario'); updateSync('reporte_regular_acumulado');
updateSync('reporte_vacunacion_diario'); updateSync('reporte_vacunacion_acumulado');
mostrarNotificacion(`🚀 Éxito! ${payload.length} enviados.`, "#27ae60", 5000);
window.dispatchEvent(new Event('storage'));
}).catch(err => alert("Error red."));
};
// ========================================================================
// LOGICA MAESTRA V44: DETECCIÓN ZERO LATENCY (textContent + 10ms)
// ========================================================================
function agendarGuardado(datos, tipo) {
const datosPaciente = capturarDatosCompletos();
localStorage.setItem('mspas_pendiente_guardar', JSON.stringify({
datos: datos, tipo: tipo, paciente: datosPaciente, timestamp: Date.now()
}));
mostrarNotificacion("⏳ Esperando respuesta...", "#f39c12", 5000);
}
async function procesarExito(pendiente) {
if (!pendiente) return;
const fechaReal = await obtenerFechaHoraReal();
const datosBase = pendiente.paciente || capturarDatosCompletos();
datosBase.servicio = getServicioGuardado();
datosBase.fecha_registro = fechaReal.toLocaleDateString('es-GT');
datosBase.hora = fechaReal.toLocaleTimeString('es-GT');
if (Array.isArray(pendiente.datos)) {
pendiente.datos.forEach(v => {
const reg = { ...datosBase, vacuna: v.nombre, fecha_vacuna: v.fecha };
guardarRegistro(reg, 'regular');
});
mostrarNotificacion(`✅ REGISTRADO EXCEL: ${datosBase.nombre} (${pendiente.datos.length})`, "#27ae60", 6000);
} else {
const reg = { ...datosBase, vacuna: pendiente.datos.vacuna, fecha_vacuna: pendiente.datos.fecha };
guardarRegistro(reg, 'otras');
mostrarNotificacion(`✅ REGISTRADO EXCEL: ${datosBase.nombre}`, "#27ae60", 6000);
}
localStorage.removeItem('mspas_pendiente_guardar');
}
function verificarGuardadoPendiente() {
const pendienteRaw = localStorage.getItem('mspas_pendiente_guardar');
if (!pendienteRaw) return;
const pendiente = JSON.parse(pendienteRaw);
if (Date.now() - pendiente.timestamp > 300000) { localStorage.removeItem('mspas_pendiente_guardar'); return; }
let procesado = false;
const chequearAhora = () => {
if (procesado) return true;
// 1. BUSQUEDA QUIRURGICA (Optimizado con textContent)
const successEl = document.getElementById("APEX_SUCCESS_MESSAGE");
if (successEl && !successEl.classList.contains("u-hidden")) {
if (successEl.textContent.includes(TEXTO_EXITO)) {
procesado = true; procesarExito(pendiente); return true;
}
}
// 2. BUSQUEDA POPUP (Optimizado)
const popupMsg = document.querySelector(".a-AlertMessage-details");
if (popupMsg && popupMsg.textContent.includes(TEXTO_EXITO)) {
procesado = true; procesarExito(pendiente); return true;
}
// 3. BUSQUEDA GLOBAL RAPIDA (textContent es mas rapido que innerText)
if (document.body.textContent.includes(TEXTO_EXITO)) {
procesado = true; procesarExito(pendiente); return true;
}
// DETECCION ERROR
const bodyText = document.body.textContent; // Leer una sola vez por ciclo
if (bodyText.includes("Error") || bodyText.includes("ORA-")) {
procesado = true; localStorage.removeItem('mspas_pendiente_guardar');
mostrarNotificacion("❌ Error detectado.", "#c0392b", 5000); return true;
}
return false;
};
if (chequearAhora()) return;
// OBSERVER MAS AGRESIVO (Detecta cambios de atributos)
const observer = new MutationObserver(() => { if (chequearAhora()) observer.disconnect(); });
observer.observe(document.body, { attributes: true, childList: true, subtree: true, characterData: true });
// INTERVALO HIPER-RAPIDO (10ms)
let intentos = 0;
const intervalo = setInterval(() => {
intentos++;
if (chequearAhora() || intentos > 1000) { // 10 segs max
clearInterval(intervalo); observer.disconnect();
}
}, 10);
}
verificarGuardadoPendiente();
// ========================================================================
// CAPTURAS
// ========================================================================
setInterval(() => {
const inputFecha = document.getElementById(ID_FECHA_INPUT);
const selectVacuna = document.getElementById(ID_VACUNA_SELECT);
if (inputFecha && selectVacuna) {
const botones = Array.from(document.querySelectorAll("button"));
const btnGuardar = botones.find(b => b.innerText.trim() === "Guardar");
if (btnGuardar && !btnGuardar.classList.contains('monitor-listo')) {
btnGuardar.classList.add('monitor-listo');
btnGuardar.style.border = "3px solid #e67e22";
btnGuardar.addEventListener('click', function() {
const vacunaTxt = selectVacuna.options[selectVacuna.selectedIndex].text;
const fechaTxt = inputFecha.value;
if (vacunaTxt && fechaTxt) agendarGuardado({ vacuna: vacunaTxt, fecha: fechaTxt }, 'otras');
});
}
}
}, 1000);
function parsearTextoConfirmacion(textoHTML) {
const textoLimpio = textoHTML.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>?/gm, '');
const lineas = textoLimpio.split('\n');
const vacunasDetectadas = [];
lineas.forEach(linea => {
if (linea.includes("Fecha Administración:")) {
const partes = linea.split("Fecha Administración:");
if (partes.length >= 2) vacunasDetectadas.push({ nombre: partes[0].trim(), fecha: partes[1].trim() });
}
});
return vacunasDetectadas;
}
const obsPopup = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
const dialogos = document.querySelectorAll('.ui-dialog-content');
dialogos.forEach(dialogContent => {
const textoContenedor = dialogContent.querySelector('.a-AlertMessage-details');
if (textoContenedor && !dialogContent.classList.contains('procesado')) {
let botonFinal = dialogContent.parentElement.querySelector('button.js-confirmBtn');
if (!botonFinal) {
const botones = dialogContent.parentElement.querySelectorAll('button');
for (let b of botones) { if (b.innerText.trim() === "Aceptar") { botonFinal = b; break; } }
}
if (botonFinal && !botonFinal.classList.contains('monitor-listo')) {
dialogContent.classList.add('procesado');
botonFinal.classList.add('monitor-listo');
const vacunasEnCola = parsearTextoConfirmacion(textoContenedor.innerHTML);
botonFinal.addEventListener('click', function() {
if (vacunasEnCola.length > 0) agendarGuardado(vacunasEnCola, 'regular');
});
}
}
});
}
});
});
obsPopup.observe(document.body, { childList: true, subtree: true });
// ========================================================================
// DASHBOARD PANEL V43
// ========================================================================
const elementoNombre = document.getElementById(ID_NOMBRE_DISPLAY) || document.querySelector(".t-Header-branding");
if (elementoNombre) {
const panel = document.createElement('div');
panel.style = "position: fixed; bottom: 10px; left: 10px; background: #2c3e50; color: white; padding: 10px; z-index: 99999; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.5); font-family: Arial; font-size: 11px; width: 230px; border: 2px solid #3498db;";
panel.innerHTML = `
<div style="font-weight:bold; border-bottom:1px solid #aaa; margin-bottom:5px;">💉 Panel de Control Vacunas
<button id="btnAbrirDash" style="background:#8e44ad; color:white; width:100%; border:none; padding:5px; margin-bottom:5px; cursor:pointer; font-weight:bold; border-radius:4px;">🖥️ ABRIR DASHBOARD</button>
<div style="font-size:10px; color:#bdc3c7;">Servicio: <span id="lblServicio" style="color:white; font-weight:bold;">...</span></div>
<div style="margin-top:5px; background:#2980b9; padding:4px; border-radius:4px;">
<div style="font-weight:bold; color:white;">👶 Esquema Regular</div>
<div style="display:flex; justify-content:space-between;"><span>Hoy: <b id="cntRegHoy" style="color:#f1c40f">0</b></span><span>Total: <b id="cntRegTot">0</b></span></div>
<div style="margin-top:2px; display:flex; gap:2px;">
<button id="btnRegHoy" style="flex:1; background:#fff; color:#333; border:none; padding:1px; font-size:9px;">📥 HOY</button>
<button id="btnRegTot" style="flex:1; background:#fff; color:#333; border:none; padding:1px; font-size:9px;">📥 TOTAL</button>
</div>
</div>
<div style="margin-top:5px; background:#d35400; padding:4px; border-radius:4px;">
<div style="font-weight:bold; color:white;">💉 Otras Vacunas</div>
<div style="display:flex; justify-content:space-between;"><span>Hoy: <b id="cntOtrHoy" style="color:#f1c40f">0</b></span><span>Total: <b id="cntOtrTot">0</b></span></div>
<div style="margin-top:2px; display:flex; gap:2px;">
<button id="btnOtrHoy" style="flex:1; background:#fff; color:#333; border:none; padding:1px; font-size:9px;">📥 HOY</button>
<button id="btnOtrTot" style="flex:1; background:#fff; color:#333; border:none; padding:1px; font-size:9px;">📥 TOTAL</button>
</div>
</div>
<div style="margin-top:8px;">
<button id="btnSyncDrive" style="width:100%; background:#27ae60; color:white; border:none; padding:5px; border-radius:4px; font-weight:bold; cursor:pointer;">☁️ Sincronizar Drive</button>
</div>
<div style="text-align:center; margin-top:5px; display:flex; justify-content:space-around;">
<small id="btnResetHoy" style="cursor:pointer; color:#bdc3c7;">⚙️ Reset Hoy</small>
<small id="btnResetAll" style="cursor:pointer; color:#e74c3c;">⚙️ Reset TOTAL</small>
</div>
`;
document.body.appendChild(panel);
document.getElementById('btnAbrirDash').onclick = abrirDashboard;
document.getElementById('btnSyncDrive').onclick = window.sincronizarDrive;
const updatePanel = () => {
document.getElementById('lblServicio').innerText = getServicioGuardado();
document.getElementById('cntRegHoy').innerText = obtenerDatos('regular').length;
document.getElementById('cntRegTot').innerText = obtenerAcumulado('regular').length;
document.getElementById('cntOtrHoy').innerText = obtenerDatos('otras').length;
document.getElementById('cntOtrTot').innerText = obtenerAcumulado('otras').length;
};
const generarCSV = (datos, nombre) => {
if(datos.length === 0) return alert("Sin datos");
let csv = "\uFEFFSERVICIO;FECHA_REGISTRO;HORA_REGISTRO;CUI/DPI;NOMBRE_PACIENTE;SEXO;NACIONALIDAD;FECHA_NACIMIENTO;EDAD;TIPO_VACUNA;FECHA_VACUNACION\n";
datos.forEach(d => {
const clean = (txt) => (txt || "").toString().replace(/;/g, " ");
const cuiFormateado = `="${clean(d.cui)}"`;
csv += `"${clean(d.servicio)}";"${clean(d.fecha_registro)}";"${clean(d.hora)}";${cuiFormateado};"${clean(d.nombre)}";"${clean(d.sexo)}";"${clean(d.nacionalidad)}";"${clean(d.nacimiento_fecha)}";"${clean(d.nacimiento_edad)}";"${clean(d.vacuna)}";"${clean(d.fecha_vacuna)}"\n`;
});
const blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = nombre;
link.click();
};
document.getElementById('btnRegHoy').onclick = () => generarCSV(obtenerDatos('regular'), `Regular_HOY_${new Date().toLocaleDateString('es-GT').replace(/\//g,'-')}.csv`);
document.getElementById('btnRegTot').onclick = () => generarCSV(obtenerAcumulado('regular'), `Regular_TOTAL.csv`);
document.getElementById('btnOtrHoy').onclick = () => generarCSV(obtenerDatos('otras'), `Otras_HOY_${new Date().toLocaleDateString('es-GT').replace(/\//g,'-')}.csv`);
document.getElementById('btnOtrTot').onclick = () => generarCSV(obtenerAcumulado('otras'), `Otras_TOTAL.csv`);
const logicaReset = (modo) => {
const seleccion = prompt(`⚠️ RESET ${modo}\n\nEscribe el número de la opción:\n1 - Solo Esquema Regular\n2 - Solo Otras Vacunas\n3 - Todo (Ambas)`);
if (!seleccion) return;
let borrarRegular = (seleccion === '1' || seleccion === '3');
let borrarOtras = (seleccion === '2' || seleccion === '3');
if (modo === 'HOY') {
if (borrarRegular) {
const diario = obtenerDatos('regular'); const acumulado = obtenerAcumulado('regular');
if (diario.length === acumulado.length) localStorage.setItem('reporte_regular_acumulado', '[]');
localStorage.setItem('reporte_regular_diario', '[]');
}
if (borrarOtras) {
const diario = obtenerDatos('otras'); const acumulado = obtenerAcumulado('otras');
if (diario.length === acumulado.length) localStorage.setItem('reporte_vacunacion_acumulado', '[]');
localStorage.setItem('reporte_vacunacion_diario', '[]');
}
}
else if (modo === 'TOTAL') {
if (confirm(`¿SEGURO QUE DESEAS BORRAR EL ACUMULADO?`)) {
if (borrarRegular) { localStorage.setItem('reporte_regular_diario', '[]'); localStorage.setItem('reporte_regular_acumulado', '[]'); }
if (borrarOtras) { localStorage.setItem('reporte_vacunacion_diario', '[]'); localStorage.setItem('reporte_vacunacion_acumulado', '[]'); }
}
}
window.dispatchEvent(new Event('storage')); alert(`✅ Reset ${modo} completado.`);
};
document.getElementById('btnResetHoy').onclick = () => logicaReset('HOY');
document.getElementById('btnResetAll').onclick = () => logicaReset('TOTAL');
window.addEventListener('storage', updatePanel);
setInterval(updatePanel, 1000);
updatePanel();
}
// ========================================================================
// DASHBOARD V43 - RESCUE MODE
// ========================================================================
function abrirDashboard() {
const win = window.open("", "DashboardVacunasV43", "width=1350,height=900");
if (!win) return alert("⚠️ Permite Pop-ups");
// Pasamos la URL_SHEET al dashboard para que pueda sincronizar por su cuenta
const sheetUrl = URL_GOOGLE_SHEET;
const htmlContent = `<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8"><title>🖥️ Dashboard Vacunas V43</title><link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"><style>body{background:#2c3e50;color:#ecf0f1;font-family:'Segoe UI',sans-serif}.section-card{background:#34495e;border-radius:10px;padding:15px;margin-bottom:20px;box-shadow:0 4px 6px rgba(0,0,0,0.3)}.card-header-custom{border-bottom:2px solid #7f8c8d;padding-bottom:10px;margin-bottom:15px;display:flex;justify-content:space-between;align-items:center}.table{color:#ecf0f1;font-size:0.85rem}.table thead{background:#2c3e50;position:sticky;top:0;z-index:2}.table-hover tbody tr:hover{color:#fff;background:#576574}.stat-box{background:rgba(0,0,0,0.2);border-radius:5px;padding:5px 10px;text-align:center;min-width:70px}.stat-num{font-size:1.1rem;font-weight:bold}.search-bar{width:300px}.filter-group{display:flex;gap:5px;align-items:center}.filter-count{font-size:0.9rem;font-weight:bold;padding:2px 8px;border-radius:4px}.dropdown-menu{max-height:250px;overflow-y:auto;background:#2c3e50;border:1px solid #7f8c8d;color:#ecf0f1}.dropdown-item:hover{background:#34495e;color:white}.form-check-input:checked{background-color:#3498db;border-color:#3498db}</style></head><body>
<div class="container-fluid pt-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="m-0">Dashboard Vacunas V43</h3>
<div class="input-group search-bar mx-3">
<input type="text" id="searchInput" class="form-control bg-dark text-white border-secondary" placeholder="Buscar por Nombre o CUI..." onkeyup="refrescarTablas()">
</div>
<div class="text-end">
<button onclick="sincronizarDriveDash()" class="btn btn-sm btn-success me-2">☁️ Sincronizar Drive</button>
<small class="d-block text-info" id="servName">...</small>
<button onclick="cargarDatos()" class="btn btn-sm btn-outline-light">🔄 Refrescar Datos</button>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="section-card" style="border-top:4px solid #3498db">
<div class="card-header-custom">
<h5 class="text-white m-0">Regular</h5>
<div class="d-flex gap-2">
<div class="stat-box"><small>HOY</small><div id="regHoy" class="stat-num text-info">0</div></div>
<div class="stat-box"><small>TOTAL</small><div id="regTotal" class="stat-num text-white">0</div></div>
</div>
</div>
<div class="filter-group mb-2" id="divFiltrosReg"></div>
<div class="mb-2 text-end">
<span id="regViendo" class="filter-count bg-info text-dark">Viendo: 0</span>
</div>
<div class="table-responsive" style="max-height:600px;overflow-y:auto">
<table class="table table-hover table-sm" id="tableReg">
<thead><tr><th>#</th><th>Hora</th><th>Servicio</th><th>Paciente</th><th>Vacuna / Fecha</th><th>Acción</th></tr></thead>
<tbody id="bodyRegular"></tbody>
</table>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="section-card" style="border-top:4px solid #e67e22">
<div class="card-header-custom">
<h5 class="text-white m-0">Otras</h5>
<div class="d-flex gap-2">
<div class="stat-box"><small>HOY</small><div id="otrHoy" class="stat-num text-warning">0</div></div>
<div class="stat-box"><small>TOTAL</small><div id="otrTotal" class="stat-num text-white">0</div></div>
</div>
</div>
<div class="filter-group mb-2" id="divFiltrosOtr"></div>
<div class="mb-2 text-end">
<span id="otrViendo" class="filter-count bg-warning text-dark">Viendo: 0</span>
</div>
<div class="table-responsive" style="max-height:600px;overflow-y:auto">
<table class="table table-hover table-sm" id="tableOtr">
<thead><tr><th>#</th><th>Hora</th><th>Servicio</th><th>Paciente</th><th>Vacuna / Fecha</th><th>Acción</th></tr></thead>
<tbody id="bodyOtras"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="editModal" tabindex="-1"><div class="modal-dialog"><div class="modal-content text-dark"><div class="modal-header bg-warning"><h5 class="modal-title">Editar</h5><button type="button" class="btn-close" onclick="cerrarModal()"></button></div><div class="modal-body"><input type="hidden" id="editType"><input type="hidden" id="editIndex"><div class="mb-2"><label>Paciente:</label><input type="text" id="editNombre" class="form-control" readonly></div><div class="mb-2"><label>Vacuna:</label><input type="text" id="editVacuna" class="form-control"></div><div class="mb-2"><label>Fecha:</label><input type="date" id="editFecha" class="form-control"></div></div><div class="modal-footer"><button class="btn btn-primary" onclick="guardarEdicion()">Guardar</button></div></div></div></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
let dataReg = [], dataOtr = [];
let acumReg = [], acumOtr = [];
const URL_SHEET = "${URL_GOOGLE_SHEET}"; // Inyectado
let activeFilters = {
regular: { vacuna: 'ALL', fecha_vacuna: 'ALL', servicio: 'ALL' },
otras: { vacuna: 'ALL', fecha_vacuna: 'ALL', servicio: 'ALL' }
};
// V43: Sanitizador ROBUSTO (Cura datos corruptos)
function safeLoad(key) {
try {
let raw = localStorage.getItem(key);
if (!raw) return [];
let parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
return parsed.map(d => ({
...d,
synced: (d.synced === true), // Si falta, es false
wasEdited: (d.wasEdited === true)
}));
} catch(e) {
console.error("Error cargando datos", e);
return [];
}
}
function cargarDatos(){
const servicio = localStorage.getItem('mspas_servicio_actual')||"N/A";
document.getElementById('servName').innerText = servicio;
// Usamos safeLoad en lugar de JSON.parse directo
dataReg = safeLoad('reporte_regular_diario');
dataOtr = safeLoad('reporte_vacunacion_diario');
acumReg = safeLoad('reporte_regular_acumulado');
acumOtr = safeLoad('reporte_vacunacion_acumulado');
document.getElementById('regHoy').innerText = dataReg.length;
document.getElementById('regTotal').innerText = acumReg.length;
document.getElementById('otrHoy').innerText = dataOtr.length;
document.getElementById('otrTotal').innerText = acumOtr.length;
// Reset Filtros
activeFilters = {
regular: { vacuna: 'ALL', fecha_vacuna: 'ALL', servicio: 'ALL' },
otras: { vacuna: 'ALL', fecha_vacuna: 'ALL', servicio: 'ALL' }
};
generarFiltros('regular', dataReg);
generarFiltros('otras', dataOtr);
renderizarTabla('regular');
renderizarTabla('otras');
}
function actualizarSoloTablas(){
// Re-leer de memoria
dataReg = safeLoad('reporte_regular_diario');
dataOtr = safeLoad('reporte_vacunacion_diario');
acumReg = safeLoad('reporte_regular_acumulado');
acumOtr = safeLoad('reporte_vacunacion_acumulado');
document.getElementById('regHoy').innerText = dataReg.length;
document.getElementById('regTotal').innerText = acumReg.length;
document.getElementById('otrHoy').innerText = dataOtr.length;
document.getElementById('otrTotal').innerText = acumOtr.length;
renderizarTabla('regular');
renderizarTabla('otras');
}
// V43: Sincronización AUTÓNOMA (Sin window.opener)
function sincronizarDriveDash() {
if (URL_SHEET.includes("PON_AQUI")) return alert("Configura URL en el script principal.");
// Leemos acumulados completos
const regAll = safeLoad('reporte_regular_acumulado');
const otrAll = safeLoad('reporte_vacunacion_acumulado');
const pendReg = regAll.filter(r => !r.synced);
const pendOtr = otrAll.filter(r => !r.synced);
const payload = [...pendReg, ...pendOtr];
if (payload.length === 0) return alert("✅ Todo sincronizado.");
if(!confirm(\`Enviar \${payload.length} registros a Drive?\`)) return;
fetch(URL_SHEET, {
method: "POST", mode: "no-cors", headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
}).then(() => {
const marcar = (key) => {
let data = safeLoad(key);
data = data.map(item => {
if (payload.some(p => p.cui === item.cui && p.hora === item.hora)) {
item.synced = true; item.wasEdited = false;
}
return item;
});
localStorage.setItem(key, JSON.stringify(data));
};
marcar('reporte_regular_diario'); marcar('reporte_regular_acumulado');
marcar('reporte_vacunacion_diario'); marcar('reporte_vacunacion_acumulado');
alert("🚀 Enviado con éxito!");
window.dispatchEvent(new Event('storage'));
cargarDatos();
}).catch(e => alert("Error red."));
}
function getUnique(data, key) {
return [...new Set(data.map(item => item[key]))].sort();
}
function generarFiltros(tipo, data) {
const containerId = tipo === 'regular' ? 'divFiltrosReg' : 'divFiltrosOtr';
const container = document.getElementById(containerId);
container.innerHTML = '';
const unicasVacunas = getUnique(data, 'vacuna');
const unicasFechas = getUnique(data, 'fecha_vacuna');
const unicosServicios = getUnique(data, 'servicio');
container.appendChild(crearDropdownFiltro(tipo, 'Vacuna', unicasVacunas, 'vacuna'));
container.appendChild(crearDropdownFiltro(tipo, 'Fecha', unicasFechas, 'fecha_vacuna'));
container.appendChild(crearDropdownFiltro(tipo, 'Servicio', unicosServicios, 'servicio'));
}
function crearDropdownFiltro(tipo, etiqueta, unicos, campoDatos) {
const groupId = \`grp_\${tipo}_\${campoDatos}\`;
const div = document.createElement('div');
div.className = 'dropdown d-inline-block';
const currentState = activeFilters[tipo][campoDatos];
const isAllSelected = currentState === 'ALL';
let html = \`
<button class="btn btn-sm btn-secondary dropdown-toggle text-light" type="button" id="\${groupId}" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
\${etiqueta}s
</button>
<div class="dropdown-menu p-2 shadow" aria-labelledby="\${groupId}" style="width: 250px;">
<div class="form-check mb-2 border-bottom pb-2">
<input class="form-check-input select-all" type="checkbox" id="all_\${groupId}" \${isAllSelected ? 'checked' : ''} onchange="toggleAll('\${groupId}')">
<label class="form-check-label text-white fw-bold" for="all_\${groupId}">Seleccionar Todo</label>
</div>
<div id="list_\${groupId}" style="max-height: 150px; overflow-y: auto;">\`;
if (unicos.length === 0) {
html += \`<div class="text-white small p-1">Sin datos aún</div>\`;
} else {
unicos.forEach((val, idx) => {
const chkId = \`chk_\${groupId}_\${idx}\`;
let isChecked = false;
if (currentState === 'ALL') {
isChecked = true;
} else if (Array.isArray(currentState) && currentState.includes(String(val))) {
isChecked = true;
}
html += \`
<div class="form-check">
<input class="form-check-input item-check" type="checkbox" value="\${val}" id="\${chkId}" \${isChecked ? 'checked' : ''} onchange="updateMaster('\${groupId}')">
<label class="form-check-label text-light" for="\${chkId}" style="font-size:0.85rem">\${val}</label>
</div>\`;
});
}
html += \`
</div>
<div class="mt-2 pt-2 border-top text-end">
<button class="btn btn-primary btn-sm w-100" onclick="confirmarFiltro('\${tipo}', '\${campoDatos}', '\${groupId}')">Aceptar</button>
</div>
</div>
\`;
div.innerHTML = html;
return div;
}
window.updateMaster = function(groupId) {
const master = document.getElementById('all_' + groupId);
const container = document.getElementById('list_' + groupId);
if(!container) return;
const totalItems = container.querySelectorAll('.item-check').length;
const checkedItems = container.querySelectorAll('.item-check:checked').length;
master.checked = (totalItems > 0 && totalItems === checkedItems);
};
window.toggleAll = function(groupId) {
const master = document.getElementById('all_' + groupId);
const items = document.querySelectorAll(\`#list_\${groupId} .item-check\`);
items.forEach(chk => chk.checked = master.checked);
};
function refrescarTablas() {
renderizarTabla('regular');
renderizarTabla('otras');
}
window.confirmarFiltro = function(tipo, campo, groupId) {
const containerList = document.getElementById(\`list_\${groupId}\`);
const masterCheck = document.getElementById('all_' + groupId);
if (masterCheck && masterCheck.checked) {
activeFilters[tipo][campo] = 'ALL';
} else if (containerList) {
const checkedBoxes = containerList.querySelectorAll('.item-check:checked');
if (checkedBoxes.length === 0) {
activeFilters[tipo][campo] = [];
} else {
activeFilters[tipo][campo] = Array.from(checkedBoxes).map(cb => String(cb.value));
}
} else {
activeFilters[tipo][campo] = 'ALL';
}
renderizarTabla(tipo);
const btnToggle = document.getElementById(groupId);
if(btnToggle) {
const dropdown = bootstrap.Dropdown.getOrCreateInstance(btnToggle);
dropdown.hide();
}
};
function renderizarTabla(tipo) {
const term = document.getElementById('searchInput').value.toLowerCase();
let rawData = (tipo === 'regular') ? dataReg : dataOtr;
let indexedData = rawData.map((item, idx) => ({...item, originalIndex: idx}));
const filtros = activeFilters[tipo];
let filtered = indexedData.filter(item => {
const matchText = (item.nombre && item.nombre.toLowerCase().includes(term)) || (item.cui && item.cui.includes(term));
const matchVac = filtros.vacuna === 'ALL' ? true : (Array.isArray(filtros.vacuna) ? filtros.vacuna.includes(String(item.vacuna)) : false);
const matchFec = filtros.fecha_vacuna === 'ALL' ? true : (Array.isArray(filtros.fecha_vacuna) ? filtros.fecha_vacuna.includes(String(item.fecha_vacuna)) : false);
const matchSer = filtros.servicio === 'ALL' ? true : (Array.isArray(filtros.servicio) ? filtros.servicio.includes(String(item.servicio)) : false);
return matchText && matchVac && matchFec && matchSer;
});
const spanViendo = document.getElementById(tipo === 'regular' ? 'regViendo' : 'otrViendo');
spanViendo.innerText = \`Viendo: \${filtered.length}\`;
const tbodyId = tipo === 'regular' ? 'bodyRegular' : 'bodyOtras';
const tbody = document.getElementById(tbodyId);
tbody.innerHTML = '';
filtered.slice().reverse().forEach((reg, i) => {
let statusDot = '🔴'; // Nuevo (synced=false)
if (reg.synced) {
statusDot = '🟢'; // Sincronizado
} else if (reg.wasEdited) {
statusDot = '🟡'; // Editado
}
const row = \`<tr>
<td>\${filtered.length - i} \${statusDot}</td>
<td><small>\${reg.hora}</small></td>
<td><small class="text-info">\${reg.servicio || '-'}</small></td>
<td><small>\${reg.cui}</small><br><strong>\${reg.nombre}</strong></td>
<td>\${reg.vacuna}<br><span class="badge bg-secondary">\${reg.fecha_vacuna}</span></td>
<td>
<button class="btn btn-warning btn-sm py-0" onclick="abrirEdit('\${tipo}', \${reg.originalIndex})">✎</button>
<button class="btn btn-danger btn-sm py-0" onclick="borrar('\${tipo}', \${reg.originalIndex})">×</button>
</td>
</tr>\`;
tbody.innerHTML += row;
});
}
function borrar(tipo, index){
if(!confirm("¿Borrar este registro?")) return;
const keyDiario = tipo === 'regular' ? 'reporte_regular_diario' : 'reporte_vacunacion_diario';
const keyAcum = tipo === 'regular' ? 'reporte_regular_acumulado' : 'reporte_vacunacion_acumulado';
let diario = JSON.parse(localStorage.getItem(keyDiario));
let acumulado = JSON.parse(localStorage.getItem(keyAcum));
const target = diario[index];
diario.splice(index, 1);
localStorage.setItem(keyDiario, JSON.stringify(diario));
if(target){
const idxAcum = acumulado.findIndex(r => r.cui === target.cui && r.hora === target.hora && r.vacuna === target.vacuna);
if(idxAcum !== -1){
acumulado.splice(idxAcum, 1);
localStorage.setItem(keyAcum, JSON.stringify(acumulado));
}
}
window.dispatchEvent(new Event('storage'));
cargarDatos();
}
let modal;
function abrirEdit(tipo, index){
const keyDiario = tipo === 'regular' ? 'reporte_regular_diario' : 'reporte_vacunacion_diario';
const diario = JSON.parse(localStorage.getItem(keyDiario));
const reg = diario[index];
document.getElementById('editType').value = tipo;
document.getElementById('editIndex').value = index;
document.getElementById('editNombre').value = reg.nombre;
document.getElementById('editVacuna').value = reg.vacuna;
let fechaIso = "";
if(reg.fecha_vacuna && reg.fecha_vacuna.includes('/')){
const partes = reg.fecha_vacuna.split('/');
if(partes.length === 3) fechaIso = \`\${partes[2]}-\${partes[1]}-\${partes[0]}\`;
}
document.getElementById('editFecha').value = fechaIso;
modal = new bootstrap.Modal(document.getElementById('editModal'));
modal.show();
}
function cerrarModal(){ if(modal) modal.hide(); }
function guardarEdicion(){
const tipo = document.getElementById('editType').value;
const idx = document.getElementById('editIndex').value;
const nVacuna = document.getElementById('editVacuna').value;
const nFechaIso = document.getElementById('editFecha').value;
let nFechaFinal = nFechaIso;
if(nFechaIso && nFechaIso.includes('-')){
const partes = nFechaIso.split('-');
nFechaFinal = \`\${partes[2]}/\${partes[1]}/\${partes[0]}\`;
}
const keyDiario = tipo === 'regular' ? 'reporte_regular_diario' : 'reporte_vacunacion_diario';
const keyAcum = tipo === 'regular' ? 'reporte_regular_acumulado' : 'reporte_vacunacion_acumulado';
let diario = JSON.parse(localStorage.getItem(keyDiario));
let acumulado = JSON.parse(localStorage.getItem(keyAcum));
const target = diario[idx];
if (target.synced === true) {
alert("⚠️ ALERTA: Registro editado. Se re-enviará a Drive.");
target.synced = false;
target.wasEdited = true;
}
target.vacuna = nVacuna;
target.fecha_vacuna = nFechaFinal;
localStorage.setItem(keyDiario, JSON.stringify(diario));
const idxAcum = acumulado.findIndex(r => r.cui === target.cui && r.hora === target.hora);
if(idxAcum !== -1){
acumulado[idxAcum].vacuna = nVacuna;
acumulado[idxAcum].fecha_vacuna = nFechaFinal;
if (acumulado[idxAcum].synced === true) {
acumulado[idxAcum].synced = false;
acumulado[idxAcum].wasEdited = true;
}
localStorage.setItem(keyAcum, JSON.stringify(acumulado));
}
window.dispatchEvent(new Event('storage'));
cerrarModal();
cargarDatos();
}
window.addEventListener('storage', actualizarSoloTablas);
window.onload = cargarDatos;
setInterval(actualizarSoloTablas, 3000);
</script></body></html>`;
win.document.write(htmlContent);
win.document.close();
}
// --- RESET DIARIO & DETECTOR SERVICIO ---
const hoy = new Date().toLocaleDateString('es-GT');
if (localStorage.getItem('mspas_fecha_activa') !== hoy) {
localStorage.setItem('reporte_regular_diario', '[]');
localStorage.setItem('reporte_vacunacion_diario', '[]');
localStorage.setItem('mspas_fecha_activa', hoy);
}
setInterval(() => {
// DETECTOR ORIGINAL POR TEXTO DE PÁGINA
if (document.body.innerText.includes("SERVICIO DE SALUD:")) {
try {
const match = document.body.innerText.match(/SERVICIO DE SALUD:\s*(.+)/i);
if (match && match[1]) {
const serv = match[1].trim();
if (serv !== localStorage.getItem('mspas_servicio_actual')) {
localStorage.setItem('mspas_servicio_actual', serv);
window.dispatchEvent(new Event('storage'));
}
}
} catch (e) {}
}
// NUEVA LOGICA V33: DETECCION CAMBIO DE SERVICIO (VENTANA PARAMETRIZACION)
const inputServicioManual = document.getElementById(ID_INPUT_SERVICIO_PARAM);
if (inputServicioManual) {
// Buscamos el botón Guardar si el input existe (Modal abierto)
const botones = Array.from(document.querySelectorAll("button"));
const btnGuardarParam = botones.find(b => b.innerText.trim() === "Guardar");
// Aseguramos que solo agregamos el listener una vez
if (btnGuardarParam && !btnGuardarParam.classList.contains('servicio-monitor')) {
btnGuardarParam.classList.add('servicio-monitor');
btnGuardarParam.addEventListener('click', function() {
const nuevoServicio = inputServicioManual.value;
if (nuevoServicio) {
localStorage.setItem('mspas_servicio_actual', nuevoServicio);
window.dispatchEvent(new Event('storage'));
mostrarNotificacion(`🏥 Servicio Actualizado: ${nuevoServicio}`, "#3498db", 4000);
}
});
}
}
}, 1000);
})();