WME - UR Manager

Ultimate UR Management Toolkit designed for use with the Colombian Wazeopedia

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();