WME - UR Manager

Ultimate UR Management Toolkit

当前为 2025-05-21 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         WME - UR Manager
// @namespace    http://waze.com/
// @version      2025.05.20.03
// @description  Ultimate UR Management Toolkit 
// @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: 5000,
    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);

    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) {
    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;
            return;
        }

        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;
        }

        // Enviar comentario
        commentField.val(mensaje);
        commentField.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('_', '-')}"]`);
                    if (statusButton.length) {
                        statusButton.click();

                        setTimeout(() => {
                            const confirmButton = $('.button-primary:visible, [data-testid="confirm-button"]:visible');
                            if (confirmButton.length) {
                                confirmButton.click();
                                setTimeout(() => {
                                    guardarURviaFetch(ur);
                                    finalizarAccionUR();
                                }, 500);
                            } else {
                                debugLog('[UR Manager] Botón de confirmación no encontrado. Continuando...');
                                setTimeout(() => {
                                    guardarURviaFetch(ur);
                                    finalizarAccionUR();
                                }, 1000);
                            }
                        }, 500);
                    } else {
                        debugLog('[UR Manager] Botón de estado no encontrado:', estadoUR);
                        guardarURviaFetch(ur);
                        finalizarAccionUR();
                    }
                }, 1000);
            } else {
                // Solo responder sin cambio de estado
                setTimeout(() => {
                    guardarURviaFetch(ur);
                    finalizarAccionUR();
                }, 1000);
            }
        }, 500);
    }, CONFIG.RETRASO_ESPERA_UI);
}

// Guardado vía fetch usando el CSRF token
function guardarURviaFetch(ur) {
try {
    if (W.controller?.save) {
        W.controller.save();
        debugLog('[UR Manager] Cambios guardados con W.controller.save()');
    } else {
        debugLog('[UR Manager] No se pudo guardar automáticamente: W.controller.save() no disponible');
    }
} catch (e) {
    debugLog('[UR Manager] Error al guardar automáticamente: ' + e.message);
}


}
// Finalizar acción y actualizar panel
function finalizarAccionUR() {
    estado.accionEnProgreso = false;
    if ($(`#${CONFIG.PANEL_ID}`).is(':visible')) {
        actualizarContenidoPanel($(`#${CONFIG.PANEL_ID} .panel-content`));
    }
}

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

})();