// ==UserScript==
// @name WME - UR Manager
// @namespace http://waze.com/
// @version 2025.07.30.1
// @description Ultimate UR Management Toolkit designed for use with the Colombian Wazeopedia
// @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!",
ZOOM_INICIAL: 13,
ZOOM_DETALLE: 18,
PANEL_ID: 'urna-manager-sidebar-panel',
RETRASO_ESPERA_UI: 1500,
UMBRAL_VIEJO: 7,
UMBRAL_RECIENTE: 3
};
let estado = {
URsIniciales: [],
urVisitadas: [],
accionEnProgreso: false,
guardadoAutomatico: true
};
GM_addStyle(`
#sidebar .nav-tabs a[href="#${CONFIG.PANEL_ID}"] { padding: 15px 4px; font-size: 10px; text-align: center; line-height: 1.2; }
#${CONFIG.PANEL_ID} { padding: 0 5px; font-family: Arial, sans-serif; font-size: 10px; }
#${CONFIG.PANEL_ID} .panel-header, #${CONFIG.PANEL_ID} .panel-footer { padding: 10px 15px; background: #f8f8f8; }
#${CONFIG.PANEL_ID} .panel-header { border-bottom: 1px solid #eee; }
#${CONFIG.PANEL_ID} .panel-footer { border-top: 1px solid #eee; }
#${CONFIG.PANEL_ID} .panel-header label { font-weight: bold; margin: 0; }
#${CONFIG.PANEL_ID} .panel-content { padding: 15px; overflow-y: auto; max-height: calc(100vh - 380px); }
#${CONFIG.PANEL_ID} table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
table-layout: fixed;
}
#${CONFIG.PANEL_ID} th {
border: 1px solid #ddd;
padding: 4px;
text-align: center; /* Centramos el texto del encabezado */
white-space: normal; /* Permitimos el salto de línea */
vertical-align: middle;
line-height: 1.2; /* Ajustamos el interlineado para texto en dos líneas */
position: sticky;
top: -15px;
background-color: #f2f2f2;
z-index: 1;
}
#${CONFIG.PANEL_ID} td {
border: 1px solid #ddd;
padding: 5px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
#${CONFIG.PANEL_ID} td.uc-cell {
white-space: normal; /* Permite el salto de línea para esta celda específica */
text-align: center;
line-height: 1.1;
}
#${CONFIG.PANEL_ID} th:nth-child(1) { width: 25%; } /* Columna Actualizado Por */
#${CONFIG.PANEL_ID} th:nth-child(2) { width: 30%; } /* Columna Fecha Creación */
#${CONFIG.PANEL_ID} th:nth-child(3) { width: 20%; } /* Columna Estado */
#${CONFIG.PANEL_ID} th:nth-child(4) { width: 14%; } /* UC */
#${CONFIG.PANEL_ID} th:nth-child(5) { width: 13%; } /* Acción */
.btn-centrar { padding: 4px 8px; background: #3498db; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 14px; }
.panel-footer { display: flex; justify-content: space-around; flex-wrap: wrap; }
.btn-global { padding: 8px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; text-align: center; flex: 1 1 45%; margin: 4px; }
.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 {
const updatedById = String(ur.attributes?.updatedBy || ur.attributes?.metaData?.updatedBy || ur.updatedBy || 'N/A');
if (updatedById === 'N/A') return 'N/A'; if (updatedById === "-1") return "Wazer"; if (updatedById === "0") return "System";
const userIdNum = parseInt(updatedById, 10);
if (W.model.users) { const user = W.model.users.getObjectById(userIdNum); if (user && user.attributes && user.attributes.userName) return user.attributes.userName; }
return updatedById;
} catch (e) { debugLog(`Error en obtenerActualizadoPor para UR ${ur.attributes?.id}: ${e}`); return (ur.attributes?.updatedBy || 'N/A').toString(); }
}
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 crearSidebarTab() {
if ($(`#${CONFIG.PANEL_ID}`).length) return;
const tab = $(`<li><a href="#${CONFIG.PANEL_ID}" data-toggle="tab" title="UR Manager">UR Manager</a></li>`);
$('#sidebar .nav-tabs').append(tab);
const tabContent = $(`<div class="tab-pane" id="${CONFIG.PANEL_ID}"></div>`);
$('#sidebar .tab-content').append(tabContent);
renderizarPanel();
}
function renderizarPanel() {
const panel = $(`#${CONFIG.PANEL_ID}`);
panel.empty();
const panelHeader = $(`<div class="panel-header"><label for="auto-save-checkbox"><input type="checkbox" id="auto-save-checkbox" ${estado.guardadoAutomatico ? '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">Resolver</button><button class="btn-global btn-cerrar" id="cerrar-todas">Cerrar UR</button><button class="btn-global btn-actualizar" id="actualizar-lista">Actualizar</button></div>`);
panelHeader.on('change', '#auto-save-checkbox', function() { estado.guardadoAutomatico = $(this).is(':checked'); debugLog(`Guardado automático: ${estado.guardadoAutomatico ? 'activado' : 'desactivado'}`); });
actualizarContenidoPanel(panelContent);
panelFooter.on('click', '#actualizar-lista', () => { W.map.getOLMap().zoomTo(CONFIG.ZOOM_INICIAL); setTimeout(() => actualizarContenidoPanel($(`#${CONFIG.PANEL_ID} .panel-content`)), 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, panelContent, panelFooter);
}
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>');
return;
}
let tablaHTML = `<h3>URs Visibles: ${estado.URsIniciales.length}</h3><table><thead><tr><th>Actualizado<br>Por</th><th>Fecha<br>Creación</th><th>Estado</th><th>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);
const contenidoUC = diasDesdeUC !== null ? `${diasDesdeUC}<div>días</div>` : 'S/C';
tablaHTML += `<tr id="ur-row-${id}" class="${esVisitada}">
<td>${actualizadoPor}</td>
<td>${formatearFecha(fechaCreacion)}</td>
<td class="${clasificacion.clase}">${clasificacion.estado}</td>
<td class="uc-cell">${contenidoUC}</td>
<td><button class="btn-centrar" data-id="${id}">📍</button></td>
</tr>`;
});
tablaHTML += '</tbody></table>';
const resumenEditores = {};
estado.URsIniciales.forEach(ur => {
const editor = obtenerActualizadoPor(ur);
resumenEditores[editor] = (resumenEditores[editor] || 0) + 1;
});
let resumenHTML = '<h3 style="margin-top: 20px;">Resumen por Editor</h3><table><thead><tr><th>Editor</th><th style="text-align: center;">Total URs</th></tr></thead><tbody>';
Object.keys(resumenEditores).sort().forEach(editor => {
resumenHTML += `<tr><td>${editor}</td><td style="text-align: center;">${resumenEditores[editor]}</td></tr>`;
});
resumenHTML += '</tbody></table>';
tablaHTML += resumenHTML;
panelContent.html(tablaHTML);
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) {
debugLog(`UR con ID ${id} no encontrada.`);
return;
}
const geom = ur.getOLGeometry();
if (!geom) {
debugLog(`Geometría no encontrada para UR ${id}.`);
return;
}
const center = geom.getBounds().getCenterLonLat();
W.map.setCenter(center);
W.map.getOLMap().zoomTo(CONFIG.ZOOM_DETALLE);
setTimeout(() => {
if (W.problemsController && typeof W.problemsController.showProblem === 'function') {
W.problemsController.showProblem(ur);
debugLog(`Abriendo panel para UR ${id}`);
} else {
debugLog(`Error: W.problemsController.showProblem no está disponible.`);
}
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) return;
estado.accionEnProgreso = true;
const ur = estado.URsIniciales[0];
centrarUR(ur.attributes.id);
setTimeout(() => {
const commentField = $('.new-comment-text');
if (!commentField.length) { estado.accionEnProgreso = false; debugLog("Campo de comentario no encontrado."); return; }
let mensaje = '', 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).trigger('input').trigger('change');
setTimeout(() => {
$('.send-button:not(:disabled)').click();
if (estadoUR) {
setTimeout(() => {
const statusButton = $(`[data-status="${estadoUR}"], [data-testid="${estadoUR.toLowerCase().replace('_', '-')}-button"], label[for="state-${estadoUR.toLowerCase().replace('_', '-')}"]`).first();
if (statusButton.length) {
statusButton.click();
setTimeout(() => {
$('.button-primary, .wz-button.primary.save-button').click();
setTimeout(() => {
if (estado.guardadoAutomatico) {
guardarCambios();
} else {
debugLog('[UR Manager] Guardado automático desactivado.');
estado.accionEnProgreso = false;
}
}, 1000);
}, 500);
} else { estado.accionEnProgreso = false; debugLog("Botón de estado no encontrado."); }
}, 1000);
} else { estado.accionEnProgreso = false; }
}, 500);
}, CONFIG.RETRASO_ESPERA_UI);
}
function guardarCambios() {
try {
if (W.controller?.save) {
W.controller.save();
debugLog('[UR Manager] Cambios guardados con W.controller.save()');
setTimeout(() => {
estado.accionEnProgreso = false;
debugLog('[UR Manager] Operación completada');
}, 2000);
} 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;
}
}
function inicializarScript() {
crearSidebarTab();
}
function esperarWME() {
if (typeof W === 'undefined' || !W.loginManager || !W.model || !W.map || !W.controller || !$('#sidebar .nav-tabs').length) {
setTimeout(esperarWME, 1000);
return;
}
if (!W.model.actionManager || !W.model.mapUpdateRequests) {
setTimeout(esperarWME, 1000);
return;
}
setTimeout(inicializarScript, 2000);
}
esperarWME();
})();