// ==UserScript==
// @name WME - UR Manager
// @namespace http://waze.com/
// @version 2025.05.20.05
// @description Ultimate UR Management Toolkit
// @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: 17,
PANEL_ID: 'urna-panel-fecha-exacta',
BOTON_ID: 'urna-btn-fecha-exacta',
INTERVALO_VERIFICACION: 4000,
RETRASO_ESPERA_UI: 1500,
UMBRAL_VIEJO: 7,
UMBRAL_RECIENTE: 3
};
// Estado persistente
let estado = {
URsIniciales: [],
panelVisible: false,
botonUR: null,
intervaloVerificacion: null,
urVisitadas: [],
accionEnProgreso: false
};
// 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;
}
#${CONFIG.PANEL_ID} .panel-content {
padding: 15px;
overflow-y: auto;
max-height: calc(70vh - 60px);
}
#${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;
}
.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; }
`);
// Funciones de utilidad
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);
}
}
// Si no hay comentarios, devolver null
return null;
} catch (e) {
debugLog(`Error obteniendo fecha UC para UR ${ur.attributes?.id}: ${e}`);
return null;
}
}
function calcularDiferenciaDias(fecha) {
if (!fecha) return null;
const hoy = new Date();
const diffTiempo = hoy.getTime() - 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);
});
}
// Funciones de interfaz
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}">`);
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>
`);
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(panelContent);
panel.append(panelFooter);
panel.appendTo('body').fadeIn(300);
estado.panelVisible = true;
}
function actualizarContenidoPanel(panelContent) {
W.map.getOLMap().zoomTo(CONFIG.ZOOM_INICIAL);
debugLog('[UR Manager] se activo zoom 13');
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>');
return;
}
let tablaHTML = `
<h3>URs Visibles: ${estado.URsIniciales.length}</h3>
<table>
<thead>
<tr>
<th>ID</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' : '';
tablaHTML += `
<tr id="ur-row-${id}" class="${esVisitada}">
<td>${id}</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>');
panelContent.on('click', '.btn-centrar', function() {
const id = $(this).data('id');
centrarUR(id);
});
}, 1000);
}
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) {
// Verificar si ya hay una acción en progreso o no hay URs
if (estado.accionEnProgreso) {
debugLog('[UR Manager] Acción bloqueada - ya hay una operación en progreso');
return;
}
if (!estado.URsIniciales.length) {
debugLog('[UR Manager] No hay URs para procesar');
return;
}
// Configurar timeout de seguridad para evitar bloqueos permanentes
const timeoutSeguridad = setTimeout(() => {
if (estado.accionEnProgreso) {
debugLog('[UR Manager] Timeout de seguridad - liberando bloqueo');
estado.accionEnProgreso = false;
// if ($(`#${CONFIG.PANEL_ID} .panel-content`).length) {
// actualizarContenidoPanel($(`#${CONFIG.PANEL_ID} .panel-content`));
//}
}
}, 1000); // 30 segundos
try {
estado.accionEnProgreso = true;
debugLog(`[UR Manager] Iniciando acción: ${accion}`);
const ur = estado.URsIniciales[0];
if (!ur || !ur.attributes?.id) {
throw new Error('UR no válida');
}
// Centrar la UR en el mapa
centrarUR(ur.attributes.id);
setTimeout(() => {
try {
const commentField = $('.new-comment-text');
if (!commentField.length) {
throw new Error('No se encontró el campo de comentarios');
}
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;
default:
throw new Error(`Acción no reconocida: ${accion}`);
}
// Enviar comentario
commentField.val(mensaje);
commentField.trigger('input').trigger('change');
setTimeout(() => {
try {
const sendButton = $('.send-button:not(:disabled)');
if (!sendButton.length) {
throw new Error('Botón de enviar no disponible');
}
sendButton.click();
if (estadoUR) {
setTimeout(() => {
try {
// Cambiar estado de la UR
const statusButton = $(`
[data-status="${estadoUR}"],
[data-testid="${estadoUR.toLowerCase().replace('_', '-')}-button"],
label[for="state-${estadoUR.toLowerCase().replace('_', '-')}"]
`).first();
if (!statusButton.length) {
throw new Error(`Botón de estado ${estadoUR} no encontrado`);
}
statusButton.click();
setTimeout(() => {
try {
// Guardar cambios
guardarURviaFetch(ur);
// Mover a la siguiente UR después de guardar
// estado.URsIniciales.shift();
//estado.urVisitadas.push(ur.attributes.id);
// Si quedan URs, procesar la siguiente
//if (estado.URsIniciales.length > 0) {
// setTimeout(() => gestionarURs(accion), 1500);
//} else {
// estado.accionEnProgreso = false;
//clearTimeout(timeoutSeguridad);
//actualizarContenidoPanel($(`#${CONFIG.PANEL_ID} .panel-content`));
// }
} catch (e) {
manejarError(e);
}
}, 1000);
} catch (e) {
manejarError(e);
}
}, 1000);
} else {
// Solo comentario, no cambio de estado
estado.URsIniciales.shift();
estado.urVisitadas.push(ur.attributes.id);
if (estado.URsIniciales.length > 0) {
setTimeout(() => gestionarURs(accion), 1500);
} else {
estado.accionEnProgreso = false;
clearTimeout(timeoutSeguridad);
//actualizarContenidoPanel($(`#${CONFIG.PANEL_ID} .panel-content`));
}
}
} catch (e) {
// manejarError(e);
}
}, 500);
} catch (e) {
// manejarError(e);
}
}, CONFIG.RETRASO_ESPERA_UI);
function manejarError(error) {
debugLog(`[UR Manager] Error: ${error.message}`);
estado.accionEnProgreso = false;
clearTimeout(timeoutSeguridad);
// actualizarContenidoPanel($(`#${CONFIG.PANEL_ID} .panel-content`));
// Mostrar alerta al usuario
alert(`Error en UR Manager: ${error.message}`);
}
} catch (e) {
manejarError(e);
}
}
function guardarURviaFetch(ur) {
try {
if (W.controller?.save) {
W.controller.save();
debugLog('[UR Manager] Cambios guardados con W.controller.save()');
// Esperar a que se complete el guardado
setTimeout(() => {
estado.accionEnProgreso = false;
debugLog('[UR Manager] Operación completada');
// Actualizar el panel después de guardar
//if ($(`#${CONFIG.PANEL_ID} .panel-content`).length) {
// actualizarContenidoPanel($(`#${CONFIG.PANEL_ID} .panel-content`));
//}
}, 2000); // Aumentar tiempo de espera para guardado
} else {
debugLog('[UR Manager] No se pudo guardar automáticamente: W.controller.save() no disponible');
estado.accionEnProgreso = false;
}
} catch (e) {
debugLog('[UR Manager] Error al guardar automáticamente: ' + e.message);
estado.accionEnProgreso = false;
}
}
// Inicialización
function inicializarScript() {
crearBotonPrincipal();
estado.intervaloVerificacion = setInterval(() => {
if ($(`#${CONFIG.BOTON_ID}`).length === 0) {
crearBotonPrincipal();
}
}, CONFIG.INTERVALO_VERIFICACION);
}
function esperarWME() {
if (typeof W === 'undefined' || !W.loginManager || !W.model || !W.map) {
setTimeout(esperarWME, 1000);
return;
}
if (!W.model.mapUpdateRequests) {
setTimeout(esperarWME, 1000);
return;
}
setTimeout(inicializarScript, 2000);
}
// Iniciar el script
esperarWME();
})();