Greasy Fork 还支持 简体中文。

WME - UR Manager

Ultimate UR Management Toolkit

目前為 2025-05-21 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 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.05.20.04
// @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);
                                   
                                }, 500);
                            } else {
                                debugLog('[UR Manager] Botón de confirmación no encontrado. Continuando...');
                                setTimeout(() => {
                                    guardarURviaFetch(ur);
                                   
                                }, 1000);
                            }
                        }, 500);
                    } else {
                        debugLog('[UR Manager] Botón de estado no encontrado:', estadoUR);
                        guardarURviaFetch(ur);
                       
                    }
                }, 1000);
            } else {
                // Solo responder sin cambio de estado
                setTimeout(() => {
                    guardarURviaFetch(ur);
                    }, 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);
}


}

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

})();