WME - UR Manager

Ultimate UR Management Toolkit with zoom refresh and panel update

// ==UserScript==
// @name         WME - UR Manager
// @namespace    http://waze.com/
// @version      2025.04.28.13
// @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>
        `);

        // Función para actualizar el contenido del panel
        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);
                    }
                });
            }
        };

        // Configurar el evento de actualización
        panelFooter.on('click', '#actualizar-lista', function() {
            if (estado.accionEnProgreso) return;

            // Ajustar el zoom a 13
            W.map.getOLMap().zoomTo(CONFIG.ZOOM_ACTUALIZACION);


            // Pequeña espera antes de actualizar para que el mapa se estabilice
            agregarTimeout(() => {
                actualizarContenidoPanel();
                debugLog(`Panel actualizado después de ajustar zoom a ${CONFIG.ZOOM_ACTUALIZACION}`);
            }, 1000);
        });

        // Configurar otros eventos
        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);
            });
        });

        // Cargar contenido inicial
        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`);
            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 {
                    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');
                    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;
        }

        //centrarYMostrarUR(id);

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

                                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);
         if (estado.accionEnProgreso) return;
    }

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

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