// ==UserScript==
// @name WME - UR Manager
// @namespace http://waze.com/
// @version 2025.04.17.15
// @description Gestión de URs
// @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';
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!!",
DEBUG: true,
BOTON_ID: 'urna-btn-fecha-exacta',
PANEL_ID: 'urna-panel-fecha-exacta',
INTERVALO_VERIFICACION: 5000,
UMBRAL_VIEJO: 7,
UMBRAL_RECIENTE: 3,
RETRASO_ENTRE_ACCIONES: 800
};
GM_addStyle(`
#${CONFIG.BOTON_ID} {
position: fixed !important;
bottom: 20px !important;
left: 20px !important;
z-index: 99999 !important;
padding: 10px 15px !important;
background: #3498db !important;
color: white !important;
font-weight: bold !important;
border: none !important;
border-radius: 5px !important;
cursor: pointer !important;
font-family: Arial, sans-serif !important;
box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important;
}
#${CONFIG.PANEL_ID} {
position: fixed;
top: 80px;
right: 20px;
width: 500px;
max-height: 70vh;
min-height: 200px;
display: flex;
flex-direction: column;
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;
}
#${CONFIG.PANEL_ID} .panel-content {
flex: 1;
overflow-y: auto;
padding: 15px;
max-height: calc(70vh - 60px);
}
#${CONFIG.PANEL_ID} table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
#${CONFIG.PANEL_ID} th {
position: sticky;
top: 0;
background-color: #f2f2f2;
z-index: 10;
}
#${CONFIG.PANEL_ID} th, #${CONFIG.PANEL_ID} td {
border: 1px solid #ddd;
padding: 6px;
text-align: left;
}
.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; }
.ur-cerrada { text-decoration: line-through; opacity: 0.6; }
.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;
position: sticky;
bottom: 0;
z-index: 20;
height: 60px;
}
.btn-global {
padding: 8px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
white-space: nowrap;
}
.btn-responder {
background: #f0ad4e;
color: white;
}
.btn-cerrar {
background: #5cb85c;
color: white;
}
.btn-resuelta {
background: #5bc0de;
color: white;
}
`);
let estado = {
URsActuales: [],
panelVisible: false,
botonUR: null,
intervaloVerificacion: null,
timeouts: [],
accionEnProgreso: false
};
function debugLog(message) {
if (CONFIG.DEBUG) console.log('[UR Script] ' + message);
}
function limpiarTimeouts() {
estado.timeouts.forEach(timeout => clearTimeout(timeout));
estado.timeouts = [];
}
function agregarTimeout(callback, delay) {
const timeoutId = setTimeout(() => {
callback();
estado.timeouts = estado.timeouts.filter(id => id !== timeoutId);
}, delay);
estado.timeouts.push(timeoutId);
return timeoutId;
}
function togglePanelURs() {
if (estado.panelVisible) {
$(`#${CONFIG.PANEL_ID}`).fadeOut(300, function() {
$(this).remove();
});
estado.panelVisible = false;
limpiarTimeouts();
} else {
mostrarPanelURs();
}
}
function crearBoton() {
if ($(`#${CONFIG.BOTON_ID}`).length > 0) return;
debugLog('Creando botón...');
estado.botonUR = $(`<button id="${CONFIG.BOTON_ID}">📝 UR Manager</button>`)
.appendTo('body')
.on('click', togglePanelURs);
debugLog('Botón creado exitosamente');
}
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;
}
return null;
} catch (e) {
debugLog(`Error obteniendo fecha: ${e}`);
return null;
}
}
function clasificarUR(fecha) {
if (!fecha) return { estado: "Sin fecha", clase: "ur-no-fecha" };
const hoy = new Date();
const diff = hoy - fecha;
const dias = Math.floor(diff / (1000 * 60 * 60 * 24));
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 mostrarPanelURs() {
estado.panelVisible = true;
limpiarTimeouts();
$(`#${CONFIG.PANEL_ID}`).remove();
const panel = $(`<div id="${CONFIG.PANEL_ID}">`);
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>
</div>
`);
estado.URsActuales = obtenerURsSinAtender();
if (estado.URsActuales.length === 0) {
panelContent.html('<div style="padding:15px;text-align:center;"><b>No hay URs sin atender visibles</b></div>');
} else {
let tablaHTML = `
<h3 style="margin-top:0;">URs Activas: ${estado.URsActuales.length}</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Tipo</th>
<th>Fecha Creación</th>
<th>Estado</th>
<th>Acción</th>
</tr>
</thead>
<tbody>`;
estado.URsActuales.forEach(ur => {
const id = ur.attributes.id;
const tipo = ur.attributes.type || 'Desconocido';
const fecha = obtenerFechaCreacionExacta(ur);
const clasificacion = clasificarUR(fecha);
let fechaStr = 'No disponible';
if (fecha) {
fechaStr = fecha.toLocaleDateString('es-ES', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
tablaHTML += `
<tr id="fila-ur-${id}">
<td>${id}</td>
<td>${tipo}</td>
<td>${fechaStr}</td>
<td class="${clasificacion.clase}">${clasificacion.estado}</td>
<td><button class="btn-centrar" data-id="${id}">🗺️ Centrar</button></td>
</tr>`;
});
panelContent.html(`
${tablaHTML}
</tbody>
</table>
`);
panelContent.on('click', '.btn-centrar', function() {
if (estado.accionEnProgreso) return;
const id = $(this).data('id');
centrarYMostrarUR(id);
$(`#fila-ur-${id}`).addClass('ur-visitada');
});
}
panelFooter.on('click', '#responder-todas', function() {
if (estado.accionEnProgreso) return;
estado.URsActuales.forEach((ur, index) => {
agregarTimeout(() => responderUR(ur.attributes.id), index * CONFIG.RETRASO_ENTRE_ACCIONES);
});
});
panelFooter.on('click', '#resolver-todas', function() {
if (estado.accionEnProgreso) return;
estado.URsActuales.forEach((ur, index) => {
agregarTimeout(() => resolverUR(ur.attributes.id), index * CONFIG.RETRASO_ENTRE_ACCIONES);
});
});
panelFooter.on('click', '#cerrar-todas', function() {
if (estado.accionEnProgreso) return;
estado.URsActuales.forEach((ur, index) => {
agregarTimeout(() => cerrarUR(ur.attributes.id), index * CONFIG.RETRASO_ENTRE_ACCIONES);
});
});
panel.append(panelContent);
panel.append(panelFooter);
panel.appendTo('body').fadeIn(300);
}
function obtenerURsSinAtender() {
try {
if (!W.model?.mapUpdateRequests?.objects) return [];
const bounds = W.map.getExtent();
return Object.values(W.model.mapUpdateRequests.objects)
.filter(ur => {
const geom = ur.getOLGeometry?.();
if (!geom) return false;
const center = geom.getBounds().getCenterLonLat();
if (!bounds.containsLonLat(center)) return false;
if (ur.attributes.resolved) return false;
const comentarios = ur.attributes.comments || [];
return !comentarios.some(c => c.type === 'user' && c.text?.trim().length > 0);
});
} catch (e) {
debugLog('Error obteniendo URs: ' + e);
return [];
}
}
function centrarYMostrarUR(id) {
if (estado.accionEnProgreso) return;
estado.accionEnProgreso = true;
limpiarTimeouts();
const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
if (!ur) {
debugLog(`UR ${id} no encontrada`);
estado.accionEnProgreso = false;
return;
}
if (ur.attributes.resolved) {
debugLog(`UR ${id} ya está resuelta`);
$(`#fila-ur-${id}`).addClass('ur-cerrada').find('.btn-centrar').prop('disabled', true);
estado.accionEnProgreso = false;
return;
}
const geom = ur.getOLGeometry?.();
if (geom) {
const center = geom.getBounds().getCenterLonLat();
W.map.setCenter(center, 17);
agregarTimeout(() => {
try {
if (W.control?.MapUpdateRequest?.show) {
W.control.MapUpdateRequest.show(ur);
} else if (W.control?.MapProblem?.show) {
W.control.MapProblem.show(ur);
} else if (W.control?.UR?.show) {
W.control.UR.show(ur);
} else if (W.selectionManager) {
W.selectionManager.select([ur]);
}
$(`#fila-ur-${id}`).addClass('ur-visitada');
} catch (e) {
debugLog(`Error al mostrar UR ${id}: ${e}`);
} finally {
estado.accionEnProgreso = false;
}
}, 300);
} else {
debugLog(`No se pudo obtener geometría para UR ${id}`);
estado.accionEnProgreso = false;
}
}
function responderUR(id) {
if (estado.accionEnProgreso) return;
estado.accionEnProgreso = true;
limpiarTimeouts();
const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
if (!ur) {
estado.accionEnProgreso = false;
return;
}
centrarYMostrarUR(id);
agregarTimeout(() => {
const commentField = $('.new-comment-text');
if (commentField.length) {
commentField.val(CONFIG.MENSAJE_RESPUESTA);
const sendButton = $('.send-button');
if (sendButton.length) {
sendButton.click();
}
}
estado.accionEnProgreso = false;
}, 1500);
}
function resolverUR(id) {
if (estado.accionEnProgreso) return;
estado.accionEnProgreso = true;
limpiarTimeouts();
const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
if (!ur) {
estado.accionEnProgreso = false;
return;
}
centrarYMostrarUR(id);
agregarTimeout(() => {
const commentField = $('.new-comment-text');
if (commentField.length) {
commentField.val(CONFIG.MENSAJE_RESUELTA);
const resueltaButton = $('label[for="state-solved"]');
if (resueltaButton.length) {
resueltaButton.click();
agregarTimeout(() => {
const sendButton = $('.send-button');
if (sendButton.length) {
sendButton.click();
}
const noIssueButton = document.querySelector('[data-status="NO_ISSUE"]');
if (noIssueButton) noIssueButton.click();
const okButton = Array.from(document.querySelectorAll('button')).find(btn =>
btn.textContent.trim().includes('OK') || btn.textContent.trim().includes('Aplicar')
);
if (okButton) okButton.click();
agregarTimeout(() => {
$(`#fila-ur-${id}`).addClass('ur-cerrada').find('.btn-centrar').prop('disabled', true);
estado.accionEnProgreso = false;
}, 500);
}, 300);
} else {
estado.accionEnProgreso = false;
}
} else {
estado.accionEnProgreso = false;
}
}, 1500);
}
function cerrarUR(id) {
if (estado.accionEnProgreso) return;
estado.accionEnProgreso = true;
limpiarTimeouts();
const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
if (!ur) {
estado.accionEnProgreso = false;
return;
}
centrarYMostrarUR(id);
agregarTimeout(() => {
const commentField = $('.new-comment-text');
if (commentField.length) {
commentField.val(CONFIG.MENSAJE_CIERRE);
const sendButton = $('.send-button');
if (sendButton.length) {
sendButton.click();
agregarTimeout(() => {
const niButton = $('label[for="state-not-identified"]');
if (niButton.length) {
niButton.click();
agregarTimeout(() => {
$(`#fila-ur-${id}`).addClass('ur-cerrada').find('.btn-centrar').prop('disabled', true);
estado.accionEnProgreso = false;
}, 500);
} else {
estado.accionEnProgreso = false;
}
}, 300);
} else {
estado.accionEnProgreso = false;
}
} else {
estado.accionEnProgreso = false;
}
}, 1500);
estado.accionEnProgreso = false;
}
function inicializarScript() {
debugLog('Inicializando script...');
window.togglePanelURs = togglePanelURs;
crearBoton();
estado.intervaloVerificacion = setInterval(() => {
if ($(`#${CONFIG.BOTON_ID}`).length === 0) {
debugLog('Botón no encontrado, recreando...');
crearBoton();
}
}, CONFIG.INTERVALO_VERIFICACION);
debugLog('Script inicializado correctamente');
}
function esperarWME() {
if (typeof W === 'undefined' || !W.loginManager || !W.model || !W.map) {
debugLog('WME no está completamente cargado, reintentando...');
setTimeout(esperarWME, 1000);
return;
}
if (!W.model.mapUpdateRequests) {
debugLog('Módulo mapUpdateRequests no está disponible, reintentando...');
setTimeout(esperarWME, 1000);
return;
}
setTimeout(inicializarScript, 2000);
}
debugLog('Script cargado, esperando WME...');
esperarWME();
})();