WME - UR Manager

Ultimate UR Management Toolkit with zoom refresh and panel update

目前为 2025-05-08 提交的版本。查看 最新版本

// ==UserScript==
// @name         WME - UR Manager
// @namespace    http://waze.com/
// @version      2025.05.08.01
// @description  Ultimate UR Management Toolkit with zoom refresh and panel update
// @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,
    RETRASO_ESPERA_UI: 1000,
    MAX_REINTENTOS: 3,
    ZOOM_ACTUALIZACION: 13
};

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; }
    .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;
    }
    .btn-reiniciar {
        background: #d9534f;
        color: white;
    }
`);

let estado = {
    URsActuales: [],
    panelVisible: false,
    botonUR: null,
    intervaloVerificacion: null,
    timeouts: [],
    accionEnProgreso: false,
    reintentos: 0,
    urVisitadas: [],
    urCentradas: [],
    bloqueado: 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 resetearEstado() {
    estado.accionEnProgreso = false;
    estado.bloqueado = false;
    estado.reintentos = 0;
    limpiarTimeouts();
    debugLog('Estado del script reiniciado');
}

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 obtenerFechaUC(ur) {
    try {
        if (ur.attributes.createdOn) {
            const fecha = parsearFecha(ur.attributes.createdOn);
            if (fecha) return fecha;
        }

        if (ur.attributes.updatedOn) {
            const fecha = parsearFecha(ur.attributes.updatedOn);
            if (fecha) return fecha;
        }

        if (ur.attributes.comments && ur.attributes.comments.length > 0) {
            const primerComentario = ur.attributes.comments[0];
            if (primerComentario.createdOn) {
                const fecha = parsearFecha(primerComentario.createdOn);
                if (fecha) return fecha;
            }
        }

        return null;
    } catch (e) {
        debugLog(`Error obteniendo fecha UC para UR ${ur.id}: ${e}`);
        return null;
    }
}

function calcularDiferenciaDias(fecha) {
    if (!fecha) return null;

    const hoy = new Date();
    const diffTiempo = hoy.getTime() - fecha.getTime();
    const diffDias = Math.floor(diffTiempo / (1000 * 60 * 60 * 24));

    return diffDias;
}

function formatearDiferenciaDias(ur) {
    const fechaUC = obtenerFechaUC(ur);
    if (!fechaUC) return "No disponible";

    const dias = calcularDiferenciaDias(fechaUC);
    if (dias === null) return "Error cálculo";

    return `${dias} días`;
}

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 obtenerURsSinAtender() {
    try {
        if (!W.model?.mapUpdateRequests?.objects) return [];

        const bounds = W.map.getExtent();
        return Object.values(W.model.mapUpdateRequests.objects)
            .filter(ur => {
                // Filtrar URs cerradas (open: false o resolved: true)
                if (ur.attributes.open === false || ur.attributes.resolved) {
                    return false;
                }

                const geom = ur.getOLGeometry?.();
                if (!geom) return false;

                const center = geom.getBounds().getCenterLonLat();
                if (!bounds.containsLonLat(center)) 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 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>
            <button class="btn-global btn-reiniciar" id="actualizar-lista">Actualizar Lista</button>
        </div>
    `);

    const actualizarContenidoPanel = () => {
        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>Fecha Creación</th>
                            <th>Estado</th>
                            <th>UC (Días)</th>
                            <th>Acción</th>
                        </tr>
                    </thead>
                    <tbody>`;

            estado.URsActuales.forEach(ur => {
                const id = ur.attributes.id;
                const fecha = obtenerFechaCreacionExacta(ur);
                const clasificacion = clasificarUR(fecha);
                const diferenciaDias = formatearDiferenciaDias(ur);

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

                const esVisitada = estado.urVisitadas.includes(id) ? 'ur-visitada' : '';
                const fueCentrada = estado.urCentradas.includes(id);

                tablaHTML += `
                    <tr id="fila-ur-${id}" class="${esVisitada}">
                        <td>${id}</td>
                        <td>${fechaStr}</td>
                        <td class="${clasificacion.clase}">${clasificacion.estado}</td>
                        <td>${diferenciaDias}</td>
                        <td><button class="btn-centrar" data-id="${id}" ${fueCentrada ? 'data-centered="true"' : ''}>🗺️ Centrar</button></td>
                    </tr>`;
            });

            panelContent.html(`
                ${tablaHTML}
                    </tbody>
                </table>
            `);

            panelContent.on('click', '.btn-centrar', function() {
                if (estado.accionEnProgreso || estado.bloqueado) {
                    debugLog('Acción de centrar bloqueada temporalmente');
                    return;
                }

                const id = $(this).data('id');
                const $btn = $(this);

                if ($btn.attr('data-centered') === 'true') {
                    const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
                    if (ur) {
                        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);
                            }
                        } catch (e) {
                            debugLog(`Error al mostrar UR ${id}: ${e}`);
                        }
                    }
                } else {
                    centrarYMostrarUR(id);
                    $btn.attr('data-centered', 'true');
                }

                $(`#fila-ur-${id}`).addClass('ur-visitada');
                if (!estado.urVisitadas.includes(id)) {
                    estado.urVisitadas.push(id);
                }
            });
        }
    };

    panelFooter.on('click', '#actualizar-lista', function() {
        if (estado.accionEnProgreso) return;

        W.map.getOLMap().zoomTo(CONFIG.ZOOM_ACTUALIZACION);

        agregarTimeout(() => {
            actualizarContenidoPanel();
            debugLog(`Panel actualizado después de ajustar zoom a ${CONFIG.ZOOM_ACTUALIZACION}`);
        }, 1000);
    });

    panelFooter.on('click', '#responder-todas', function() {
        if (estado.accionEnProgreso || estado.bloqueado) 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 || estado.bloqueado) 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 || estado.bloqueado) return;
        estado.URsActuales.forEach((ur, index) => {
            agregarTimeout(() => cerrarUR(ur.attributes.id), index * CONFIG.RETRASO_ENTRE_ACCIONES);
        });
    });

    actualizarContenidoPanel();

    panel.append(panelContent);
    panel.append(panelFooter);
    panel.appendTo('body').fadeIn(300);
}

function centrarYMostrarUR(id) {
    if (estado.accionEnProgreso || estado.bloqueado) {
        debugLog(`Acción bloqueada - accionEnProgreso: ${estado.accionEnProgreso}, bloqueado: ${estado.bloqueado}`);
        return;
    }

    estado.accionEnProgreso = true;
    limpiarTimeouts();

    const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
    if (!ur) {
        debugLog(`UR ${id} no encontrada - probablemente fue cerrada`);
        estado.urCentradas = estado.urCentradas.filter(urId => urId !== id);
        estado.urVisitadas = estado.urVisitadas.filter(urId => urId !== id);
        $(`#fila-ur-${id}`).remove();
        estado.accionEnProgreso = false;
        return;
    }

    if (!estado.urCentradas.includes(id)) {
        estado.urCentradas.push(id);
    }

    const geom = ur.getOLGeometry?.();
    if (geom) {
        const center = geom.getBounds().getCenterLonLat();
        W.map.setCenter(center, 17);

        agregarTimeout(() => {
            try {
                let shown = false;
                if (W.control?.MapUpdateRequest?.show) {
                    W.control.MapUpdateRequest.show(ur);
                    shown = true;
                }
                if (!shown && W.control?.MapProblem?.show) {
                    W.control.MapProblem.show(ur);
                    shown = true;
                }
                if (!shown && W.control?.UR?.show) {
                    W.control.UR.show(ur);
                    shown = true;
                }
                if (!shown && W.selectionManager?.select) {
                    W.selectionManager.select([ur]);
                    shown = true;
                }

                if (!shown) {
                    throw new Error('No se pudo encontrar método para mostrar la UR');
                }

                $(`#fila-ur-${id}`).addClass('ur-visitada');
                if (!estado.urVisitadas.includes(id)) {
                    estado.urVisitadas.push(id);
                }
            } 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 || estado.bloqueado) return;
    estado.accionEnProgreso = true;

    limpiarTimeouts();
    const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
    if (!ur) {
        resetearEstado();
        return;
    }

    centrarYMostrarUR(id);

    agregarTimeout(() => {
        try {
            const commentField = $('.new-comment-text');
            if (!commentField.length) {
                throw new Error('Campo de comentario no encontrado');
            }

            commentField.val(CONFIG.MENSAJE_RESPUESTA);
            commentField.trigger('input').trigger('change');

            agregarTimeout(() => {
                const sendButton = $('.send-button:not(:disabled)');
                if (!sendButton.length) {
                    throw new Error('Botón enviar no encontrado o deshabilitado');
                }

                sendButton[0].click();
                resetearEstado();
            }, 500);
        } catch (error) {
            debugLog(`Error en responderUR: ${error.message}`);
            resetearEstado();
        }
    }, CONFIG.RETRASO_ESPERA_UI);
}

function resolverUR(id) {
    if (estado.accionEnProgreso || estado.bloqueado) return;

    estado.accionEnProgreso = true;
    estado.bloqueado = true;
    limpiarTimeouts();

    const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
    if (!ur) {
        resetearEstado();
        return;
    }

    agregarTimeout(() => {
        try {
            const commentField = $('.new-comment-text');
            if (!commentField.length) {
                throw new Error('Campo de comentario no encontrado');
            }

            commentField.val(CONFIG.MENSAJE_RESUELTA);
            commentField.trigger('input').trigger('change');

            agregarTimeout(() => {
                const sendButton = $('.send-button:not(:disabled)');
                if (!sendButton.length) {
                    throw new Error('Botón enviar no encontrado o deshabilitado');
                }

                sendButton[0].click();

                agregarTimeout(() => {
                    const solvedButton = document.querySelector('[data-status="SOLVED"], label[for="state-solved"], [data-testid="solved-button"]');
                    if (!solvedButton) {
                        throw new Error('Botón "Resuelta" no encontrado');
                    }

                    solvedButton.click();

                    agregarTimeout(() => {
                        const confirmButton = document.querySelector('.buttons .button-primary, .dialog-footer .button-primary');
                        if (confirmButton) {
                            confirmButton.click();
                        }

                        agregarTimeout(() => {
                            $(`#fila-ur-${id}`).remove();
                            estado.URsActuales = estado.URsActuales.filter(u => u.attributes.id !== id);
                            estado.urCentradas = estado.urCentradas.filter(urId => urId !== id);
                            estado.urVisitadas = estado.urVisitadas.filter(urId => urId !== id);

                            const contador = $('h3').first();
                            if (contador.length) {
                                contador.text(`URs Activas: ${estado.URsActuales.length}`);
                            }

                            resetearEstado();
                            debugLog('Estado desbloqueado después de resolver UR');
                        }, 500);
                    }, 500);
                }, 500);
            }, 500);
        } catch (error) {
            debugLog(`Error en resolverUR: ${error.message}`);
            resetearEstado();
        }
    }, CONFIG.RETRASO_ESPERA_UI);
}

function cerrarUR(id) {
    if (estado.accionEnProgreso || estado.bloqueado) return;

    estado.accionEnProgreso = true;
    estado.bloqueado = true;
    limpiarTimeouts();

    const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
    if (!ur) {
        resetearEstado();
        return;
    }

    centrarYMostrarUR(id);

    agregarTimeout(() => {
        try {
            const commentField = $('.new-comment-text');
            if (!commentField.length) {
                throw new Error('Campo de comentario no encontrado');
            }

            commentField.val(CONFIG.MENSAJE_CIERRE);
            commentField.trigger('input').trigger('change');

            agregarTimeout(() => {
                const sendButton = $('.send-button:not(:disabled)');
                if (!sendButton.length) {
                    throw new Error('Botón enviar no encontrado o deshabilitado');
                }

                sendButton[0].click();

                agregarTimeout(() => {
                    const notIdentifiedButton = document.querySelector('[data-status="NOT_IDENTIFIED"], label[for="state-not-identified"], [data-testid="not-identified-button"]');
                    if (!notIdentifiedButton) {
                        throw new Error('Botón "No Identificado" no encontrado');
                    }

                    notIdentifiedButton.click();

                    agregarTimeout(() => {
                        const confirmButton = document.querySelector('.buttons .button-primary, .dialog-footer .button-primary');
                        if (confirmButton) {
                            confirmButton.click();
                        }

                        agregarTimeout(() => {
                            $(`#fila-ur-${id}`).remove();
                            estado.URsActuales = estado.URsActuales.filter(u => u.attributes.id !== id);
                            estado.urCentradas = estado.urCentradas.filter(urId => urId !== id);
                            estado.urVisitadas = estado.urVisitadas.filter(urId => urId !== id);

                            const contador = $('h3').first();
                            if (contador.length) {
                                contador.text(`URs Activas: ${estado.URsActuales.length}`);
                            }

                            resetearEstado();
                            debugLog('Estado desbloqueado después de cerrar UR');
                        }, 500);
                    }, 500);
                }, 500);
            }, 500);
        } catch (error) {
            debugLog(`Error en cerrarUR: ${error.message}`);

            if (estado.reintentos < CONFIG.MAX_REINTENTOS) {
                estado.reintentos++;
                debugLog(`Reintentando (${estado.reintentos}/${CONFIG.MAX_REINTENTOS})...`);
                agregarTimeout(() => cerrarUR(id), 1000);
            } else {
                resetearEstado();
            }
        }
    }, CONFIG.RETRASO_ESPERA_UI);
}

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

})();