您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Ultimate UR Management Toolkit with zoom refresh and panel update
当前为
// ==UserScript== // @name WME - UR Manager // @namespace http://waze.com/ // @version 2025.06.15.03 // @description Ultimate UR Management Toolkit with zoom refresh and panel update // @author Crotalo // @match https://www.waze.com/*/editor* // @match https://beta.waze.com/*/editor* // @grant GM_addStyle // @require https://code.jquery.com/jquery-3.6.0.min.js // ==/UserScript== (function() { 'use strict'; // Configuración constante const CONFIG = { MENSAJE_RESPUESTA: "¡Hola, Wazer! Gracias por tu reporte. Para resolverlo de forma efectiva, necesitamos un poco más de detalle sobre lo sucedido. Quedamos atentos a tu respuesta.", MENSAJE_CIERRE: "¡Hola Wazer! Buen día, lamentablemente no pudimos solucionar el error en esta ocasión. Por favor, déjanos más datos la próxima vez. Gracias por reportar.", MENSAJE_RESUELTA: "¡Hola Wazer! Buen día, el problema fue solucionado y se verá reflejado en la aplicación en la próxima actualización del mapa, esta tomará entre 3 y 5 días. ¡Gracias por reportar!", ZOOM_INICIAL: 13, ZOOM_DETALLE: 18, PANEL_ID: 'urna-panel-fecha-exacta', BOTON_ID: 'urna-btn-fecha-exacta', INTERVALO_VERIFICACION: 5000, RETRASO_ESPERA_UI: 1500, UMBRAL_VIEJO: 7, UMBRAL_RECIENTE: 3, GUARDADO_AUTOMATICO_POR_DEFECTO: true, INTERVALO_REINTENTO_BOTON_GUARDAR: 200, MAX_REINTENTOS_BOTON_GUARDAR: 20, INTERVALO_REINTENTO_BOTON_ESTADO: 100, MAX_REINTENTOS_BOTON_ESTADO: 30 }; // Estado persistente let estado = { URsIniciales: [], panelVisible: false, botonUR: null, intervaloVerificacion: null, urVisitadas: [], accionEnProgreso: false, guardadoAutomaticoActivado: CONFIG.GUARDADO_AUTOMATICO_POR_DEFECTO }; // Mapeo de IDs de editor a nombres const EDITOR_MAP = { "11284713": "Crotalo", "1635583402": "adjrgl", "160870866": "JTRIANA77", "1103607096": "NickFury00", "708605369": "Yamen8513", "166735875": "Marygt010", "-1": "Wazer", "212791153": "Walter-Bravo", "278787635": "Camacho_Luis", "1826319427": "dicasuca06" }; // Estilos CSS GM_addStyle(` #${CONFIG.BOTON_ID} { position: fixed; bottom: 20px; left: 20px; z-index: 99999; padding: 10px 15px; background: #3498db; color: white; font-weight: bold; border: none; border-radius: 5px; cursor: pointer; font-family: Arial, sans-serif; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } #${CONFIG.PANEL_ID} { position: fixed; top: 80px; right: 20px; width: 500px; max-height: 70vh; min-height: 200px; background: white; border: 2px solid #999; z-index: 99998; font-family: Arial, sans-serif; font-size: 13px; box-shadow: 2px 2px 15px rgba(0,0,0,0.3); border-radius: 5px; display: none; display: flex; flex-direction: column; } #${CONFIG.PANEL_ID} .panel-header { padding: 10px 15px; background: #f8f8f8; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; /* Alinea los elementos a los extremos */ align-items: center; } #${CONFIG.PANEL_ID} .panel-header label { font-weight: normal; display: flex; align-items: center; gap: 5px; /* margin-left: 15px; Removido para que el checkbox esté a la derecha del título */ } #${CONFIG.PANEL_ID} .panel-header input[type="checkbox"] { margin: 0; cursor: pointer; } #${CONFIG.PANEL_ID} .panel-content { padding: 15px; overflow-y: auto; flex-grow: 1; max-height: calc(70vh - 100px); } #${CONFIG.PANEL_ID} table { width: 100%; border-collapse: collapse; margin-top: 10px; } #${CONFIG.PANEL_ID} th, #${CONFIG.PANEL_ID} td { border: 1px solid #ddd; padding: 6px; text-align: left; } #${CONFIG.PANEL_ID} th { position: sticky; top: 0; background-color: #f2f2f2; } .btn-centrar { padding: 4px 8px; background: #3498db; color: white; border: none; border-radius: 3px; cursor: pointer; } .panel-footer { padding: 10px 15px; background: #f8f8f8; border-top: 1px solid #eee; display: flex; justify-content: center; gap: 10px; margin-top: auto; } .btn-global { padding: 8px 15px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; } .btn-responder { background: #f0ad4e; color: white; } .btn-resuelta { background: #5cb85c; color: white; } .btn-cerrar { background: #d9534f; color: white; } .btn-actualizar { background: #5bc0de; color: white; } .ur-old { color: #d9534f; font-weight: bold; } .ur-recent { color: #5bc0de; } .ur-new { color: #5cb85c; } .ur-visitada { background-color: #fdf5d4 !important; } .ur-no-fecha { color: #777; font-style: italic; } `); function debugLog(message) { console.log('[UR Manager]', message); } function parsearFecha(valor) { if (!valor) return null; if (typeof valor === 'object' && '_seconds' in valor) { try { return new Date(valor._seconds * 1000 + (valor._nanoseconds / 1000000)); } catch (e) { debugLog(`Error parseando Firebase Timestamp: ${JSON.stringify(valor)}`); } } if (typeof valor === 'string' && valor.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) { try { return new Date(valor); } catch (e) { debugLog(`Error parseando fecha ISO: ${valor}`); } } if (/^\d+$/.test(valor)) { try { const num = parseInt(valor); return new Date(num > 1000000000000 ? num : num * 1000); } catch (e) { debugLog(`Error parseando timestamp numérico: ${valor}`); } } return null; } function obtenerFechaCreacionExacta(ur) { try { if (ur.attributes?.driveDate) { const fecha = parsearFecha(ur.attributes.driveDate); if (fecha) return fecha; } if (ur.attributes?.createdOn) { const fecha = parsearFecha(ur.attributes.createdOn); if (fecha) return fecha; } if (ur.attributes?.comments?.[0]?.createdOn) { const fecha = parsearFecha(ur.attributes.comments[0].createdOn); if (fecha) return fecha; } return null; } catch (e) { debugLog(`Error obteniendo fecha para UR ${ur.attributes?.id}: ${e}`); return null; } } function obtenerFechaUC(ur) { try { if (ur.attributes?.updatedOn) { const fecha = parsearFecha(ur.attributes.updatedOn); if (fecha) return fecha; } if (ur.attributes?.comments?.length > 0) { const ultimoComentario = ur.attributes.comments[ur.attributes.comments.length - 1]; if (ultimoComentario?.createdOn) { return parsearFecha(ultimoComentario.createdOn); } } return null; } catch (e) { debugLog(`Error obteniendo fecha UC para UR ${ur.attributes?.id}: ${e}`); return null; } } function obtenerActualizadoPor(ur) { try { let updatedBy = ur.attributes?.updatedBy || ur.attributes?.metaData?.updatedBy || ur.updatedBy || 'N/A'; updatedBy = String(updatedBy); return EDITOR_MAP[updatedBy] || updatedBy; } catch (e) { debugLog(`Error obteniendo updatedBy para UR ${ur.attributes?.id}: ${e}`); return 'N/A'; } } function calcularDiferenciaDias(fecha) { if (!fecha) return null; const hoy = new Date(); const diffTiempo = (hoy.getTime() + 3600000) - fecha.getTime(); return Math.floor(diffTiempo / (1000 * 60 * 60 * 24)); } function clasificarUR(fecha) { if (!fecha) return { estado: "Sin fecha", clase: "ur-no-fecha" }; const dias = calcularDiferenciaDias(fecha); if (dias === null) return { estado: "Sin fecha", clase: "ur-no-fecha" }; if (dias > CONFIG.UMBRAL_VIEJO) return { estado: `Antigua (${dias}d)`, clase: "ur-old" }; if (dias > CONFIG.UMBRAL_RECIENTE) return { estado: `Reciente (${dias}d)`, clase: "ur-recent" }; return { estado: `Nueva (${dias}d)`, clase: "ur-new" }; } function formatearFecha(fecha) { if (!fecha) return 'N/A'; return fecha.toLocaleDateString('es-ES', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } function obtenerURsVisibles() { if (!W.model?.mapUpdateRequests?.objects) return []; const bounds = W.map.getExtent(); return Object.values(W.model.mapUpdateRequests.objects) .filter(ur => { if (ur.attributes?.open === false || ur.attributes?.resolved) return false; const geom = ur.getOLGeometry?.(); if (!geom) return false; const center = geom.getBounds().getCenterLonLat(); return bounds.containsLonLat(center); }); } function crearBotonPrincipal() { if ($(`#${CONFIG.BOTON_ID}`).length) return; estado.botonUR = $(`<button id="${CONFIG.BOTON_ID}">📝 UR Manager</button>`) .appendTo('body') .on('click', togglePanel); } function togglePanel() { if (estado.panelVisible) { cerrarPanel(); } else { abrirPanel(); } } function cerrarPanel() { $(`#${CONFIG.PANEL_ID}`).fadeOut(300); estado.panelVisible = false; } function abrirPanel() { if ($(`#${CONFIG.PANEL_ID}`).length) { $(`#${CONFIG.PANEL_ID}`).fadeIn(300); estado.panelVisible = true; return; } const panel = $(`<div id="${CONFIG.PANEL_ID}">`); // HTML modificado para el panelHeader: el checkbox está ahora después de h3 const panelHeader = $(` <div class="panel-header"> <h3>URs Visibles</h3> <label> <input type="checkbox" id="checkbox-guardado-automatico" ${estado.guardadoAutomaticoActivado ? 'checked' : ''}> Guardado automático </label> </div> `); const panelContent = $('<div class="panel-content">'); const panelFooter = $(` <div class="panel-footer"> <button class="btn-global btn-responder" id="responder-todas">Preguntar</button> <button class="btn-global btn-resuelta" id="resolver-todas">Resuelta</button> <button class="btn-global btn-cerrar" id="cerrar-todas">No Identificada</button> <button class="btn-global btn-actualizar" id="actualizar-lista">Actualizar Lista</button> </div> `); panelHeader.on('change', '#checkbox-guardado-automatico', function() { estado.guardadoAutomaticoActivado = $(this).is(':checked'); debugLog(`Guardado automático: ${estado.guardadoAutomaticoActivado ? 'Activado' : 'Desactivado'}`); }); // Asegúrate de que el panelContent sea actualizado actualizarContenidoPanel(panelContent); panelFooter.on('click', '#actualizar-lista', () => { W.map.getOLMap().zoomTo(CONFIG.ZOOM_INICIAL); setTimeout(() => actualizarContenidoPanel(panelContent), 1000); }); panelFooter.on('click', '#responder-todas', () => gestionarURs('responder')); panelFooter.on('click', '#resolver-todas', () => gestionarURs('resolver')); panelFooter.on('click', '#cerrar-todas', () => gestionarURs('cerrar')); panel.append(panelHeader); panel.append(panelContent); panel.append(panelFooter); panel.appendTo('body').fadeIn(300); estado.panelVisible = true; } function actualizarContenidoPanel(panelContent) { setTimeout(() => { estado.URsIniciales = obtenerURsVisibles(); estado.urVisitadas = estado.urVisitadas.filter(id => estado.URsIniciales.some(u => u.attributes?.id == id) ); if (estado.URsIniciales.length === 0) { panelContent.html('<p>No se encontraron URs visibles en el área actual</p>'); $(`#${CONFIG.PANEL_ID} .panel-header h3`).text(`URs Visibles: 0`); return; } let tablaHTML = ` <table> <thead> <tr> <th>Actualizado por</th> <th>Fecha Creación</th> <th>Estado</th> <th>Días desde UC</th> <th>Acción</th> </tr> </thead> <tbody>`; estado.URsIniciales.forEach(ur => { const id = ur.attributes?.id; const fechaCreacion = obtenerFechaCreacionExacta(ur); const fechaUC = obtenerFechaUC(ur); const clasificacion = clasificarUR(fechaCreacion); const diasDesdeUC = calcularDiferenciaDias(fechaUC); const esVisitada = estado.urVisitadas.includes(id) ? 'ur-visitada' : ''; const actualizadoPor = obtenerActualizadoPor(ur); tablaHTML += ` <tr id="ur-row-${id}" class="${esVisitada}"> <td>${actualizadoPor}</td> <td>${formatearFecha(fechaCreacion)}</td> <td class="${clasificacion.clase}">${clasificacion.estado}</td> <td>${diasDesdeUC !== null ? diasDesdeUC + ' días' : 'Sin comentarios'}</td> <td><button class="btn-centrar" data-id="${id}">Centrar</button></td> </tr>`; }); panelContent.html(tablaHTML + '</tbody></table>'); $(`#${CONFIG.PANEL_ID} .panel-header h3`).text(`URs Visibles: ${estado.URsIniciales.length}`); panelContent.off('click', '.btn-centrar').on('click', '.btn-centrar', function() { const id = $(this).data('id'); centrarUR(id); }); }, 1500); } function centrarUR(id) { if (estado.accionEnProgreso) return; const ur = estado.URsIniciales.find(u => u.attributes?.id == id); if (!ur) return; const geom = ur.getOLGeometry(); if (!geom) return; const center = geom.getBounds().getCenterLonLat(); W.map.setCenter(center); W.map.getOLMap().zoomTo(CONFIG.ZOOM_DETALLE); setTimeout(() => { if (W.control?.UR?.show) { W.control.UR.show(ur); } else if (W.control?.MapUpdateRequest?.show) { W.control.MapUpdateRequest.show(ur); } if (!estado.urVisitadas.includes(id)) { estado.urVisitadas.push(id); $(`#ur-row-${id}`).addClass('ur-visitada'); } }, 500); } function gestionarURs(accion) { if (estado.accionEnProgreso || !estado.URsIniciales.length) { debugLog("Acción en progreso o no hay URs iniciales."); return; } estado.accionEnProgreso = true; const ur = estado.URsIniciales[0]; centrarUR(ur.attributes.id); setTimeout(() => { const commentField = $('.new-comment-text'); if (!commentField.length) { debugLog("Campo de comentario no encontrado."); estado.accionEnProgreso = false; return; } let mensaje = ''; let estadoUR = ''; switch(accion) { case 'responder': mensaje = CONFIG.MENSAJE_RESPUESTA; break; case 'resolver': mensaje = CONFIG.MENSAJE_RESUELTA; estadoUR = 'SOLVED'; break; case 'cerrar': mensaje = CONFIG.MENSAJE_CIERRE; estadoUR = 'NOT_IDENTIFIED'; break; } commentField.val(mensaje); commentField.trigger('input').trigger('change'); setTimeout(() => { const sendButton = $('.send-button:not(:disabled)'); if (sendButton.length) { sendButton.click(); debugLog("Comentario enviado."); } else { debugLog("Botón de enviar deshabilitado o no encontrado."); } if (estadoUR) { let reintentosEstado = 0; const checkStatusButton = setInterval(() => { let statusButton; const lowerEstadoUR = estadoUR.toLowerCase().replace('_', '-'); // Intentar con data-testid para botones statusButton = $(`button[data-testid="${lowerEstadoUR}-button"]`).filter(':visible:not(:disabled)').first(); if (statusButton.length) { debugLog(`Encontrado botón de estado por data-testid: ${lowerEstadoUR}-button`); } else { // Intentar con data-status (selector original) statusButton = $(`button[data-status="${estadoUR}"]`).filter(':visible:not(:disabled)').first(); if (statusButton.length) { debugLog(`Encontrado botón de estado por data-status: ${estadoUR}`); } else { // Intentar buscar por texto visible en labels (para inputs radio si los hubiera) let textToSearch = (estadoUR === 'SOLVED' ? 'Resuelto' : 'No identificado'); statusButton = $(`label:contains("${textToSearch}")`).filter(function() { return $(this).attr('for') && $(`#${$(this).attr('for')}`).is('[type="radio"],[type="checkbox"]') && $(this).is(':visible'); }).first(); if (statusButton.length) { debugLog(`Encontrado label de estado por texto: ${textToSearch}`); statusButton = $(`#${statusButton.attr('for')}`); // Clicar el input, no el label } else { // Último recurso: buscar cualquier elemento visible con el texto y que sea clicable statusButton = $(`*:contains("${textToSearch}")`).filter(function() { return ($(this).is('button, div[role="button"], a') || $(this).css('cursor') === 'pointer') && $(this).is(':visible:not(:disabled)'); }).first(); if (statusButton.length) { debugLog(`Encontrado elemento genérico por texto: ${textToSearch}`); } } } } if (statusButton.length) { // Ya verificamos :not(:disabled) en el selector clearInterval(checkStatusButton); statusButton.click(); debugLog(`Estado de UR cambiado a: ${estadoUR}`); // Aquí la comprobación si la casilla de guardado automático está activada if (estado.guardadoAutomaticoActivado) { let reintentosGuardar = 0; const checkSaveButton = setInterval(() => { // Selectores del botón de guardar: se busca el que esté visible y no deshabilitado const saveButton = $('.button-primary:contains("Aplicar"):visible:not(:disabled), .button-primary:contains("Guardar"):visible:not(:disabled), button.save:visible:not(:disabled), a.save:visible:not(:disabled)').first(); if (saveButton.length) { clearInterval(checkSaveButton); saveButton.click(); debugLog("Botón de guardar principal clickeado y habilitado."); setTimeout(() => { guardarURviaFetch(ur); }, 500); } else if (reintentosGuardar >= CONFIG.MAX_REINTENTOS_BOTON_GUARDAR) { clearInterval(checkSaveButton); debugLog("Máximo de reintentos alcanzado. Botón de guardar no encontrado o no habilitado. Guardado manual necesario."); estado.accionEnProgreso = false; } else { debugLog(`Reintento ${reintentosGuardar + 1}: Buscando botón de guardar...`); reintentosGuardar++; } }, CONFIG.INTERVALO_REINTENTO_BOTON_GUARDAR); } else { debugLog("Guardado automático desactivado. Guardado manual necesario."); estado.accionEnProgreso = false; // La acción termina aquí si no hay guardado automático } } else if (reintentosEstado >= CONFIG.MAX_REINTENTOS_BOTON_ESTADO) { clearInterval(checkStatusButton); debugLog(`Máximo de reintentos alcanzado. Botón de estado "${estadoUR}" no encontrado o no habilitado.`); estado.accionEnProgreso = false; } else { debugLog(`Reintento ${reintentosEstado + 1}: Buscando botón de estado "${estadoUR}"...`); reintentosEstado++; } }, CONFIG.INTERVALO_REINTENTO_BOTON_ESTADO); } else { estado.accionEnProgreso = false; debugLog("Acción 'responder' completada (no requiere cambio de estado)."); } }, 500); }, CONFIG.RETRASO_ESPERA_UI); } function guardarURviaFetch(ur) { try { if (W.controller?.save) { W.controller.save(); debugLog('[UR Manager] Cambios guardados con W.controller.save()'); setTimeout(() => { estado.accionEnProgreso = false; debugLog('[UR Manager] Proceso de UR completado.'); // Quitar la UR procesada de la lista inicial estado.URsIniciales = estado.URsIniciales.filter(item => item.attributes.id !== ur.attributes.id); // Actualizar el panel para reflejar los cambios const panelContent = $(`#${CONFIG.PANEL_ID} .panel-content`); if (panelContent.length) { actualizarContenidoPanel(panelContent); } }, 1000); // Dar un poco de tiempo para que la operación de guardado se complete } else { debugLog('[UR Manager] W.controller.save() no disponible. Guardado manual necesario.'); estado.accionEnProgreso = false; } } catch (e) { debugLog(`[UR Manager] Error al intentar guardar con W.controller.save(): ${e}`); estado.accionEnProgreso = false; } } // Inicialización del script function inicializar() { if (typeof W === 'undefined' || !W.map || !W.model) { debugLog("Waze Map Editor no cargado. Reintentando en 1 segundo..."); setTimeout(inicializar, 1000); return; } debugLog("Waze Map Editor cargado. Inicializando UR Manager."); crearBotonPrincipal(); } $(document).ready(inicializar); })();