WME - UR Manager

Ultimate UR Management Toolkit for Waze Map Editors! Track unresolved URs, auto-classify by age, bulk respond/close, and quick-center with ease. Saves hours of manual work with intelligent organization and lightning-fast actions. Your new essential WME companion! Colombian´s Wazeopedia terms!!

目前為 2025-04-22 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         WME - UR Manager
// @namespace    http://waze.com/
// @version      2025.04.17.15
// @description  Ultimate UR Management Toolkit for Waze Map Editors! Track unresolved URs, auto-classify by age, bulk respond/close, and quick-center with ease. Saves hours of manual work with intelligent organization and lightning-fast actions. Your new essential WME companion! Colombian´s Wazeopedia terms!!
// @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();
})();