WME - UR Manager

Ultimate UR Management Toolkit with zoom refresh and panel update

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

    // 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,
        guardadoAutomatico: true // Nueva variable de estado para el guardado automático, activa por defecto
    };

    // 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-header {
            padding: 10px 15px;
            background: #f8f8f8;
            border-bottom: 1px solid #eee;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        #${CONFIG.PANEL_ID} .panel-header label {
            font-weight: bold;
        }

        #${CONFIG.PANEL_ID} .panel-content {
            padding: 15px;
            overflow-y: auto;
            max-height: calc(70vh - 110px); /* Ajustado para el header y footer */
        }

        #${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; }
    `);


    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 {
            // Obtener el valor de updatedBy de diferentes formas según la estructura del objeto
            let updatedBy = ur.attributes?.updatedBy ||
                            ur.attributes?.metaData?.updatedBy ||
                            ur.updatedBy ||
                            'N/A';

            // Convertir a string para asegurar la comparación
            updatedBy = String(updatedBy);

            // Reemplazar id del editor
            if (updatedBy === "11284713") {
                return "Crotalo";
            }
             if (updatedBy === "1635583402") {
                return "adjrgl";
            }
            if (updatedBy === "160870866") {
                return "JTRIANA77";
            }
            if (updatedBy === "1103607096") {
                return "NickFury00";
            }
            if (updatedBy === "708605369") {
                return "Yamen8513";
            }
            if (updatedBy === "166735875") {
                return "Marygt010";
            }
            if (updatedBy === "-1") {
                return "Wazer";
            }
            if (updatedBy === "212791153") {
                return "Walter-Bravo";
            }
            if (updatedBy === "278787635") {
                return "Camacho_Luis";
            }
            if (updatedBy === "1826319427") {
                return "dicasuca06";
            }
            if (updatedBy === "17761500") {
                return "leoguana85(2)";
            }
                
            return updatedBy;
        } catch (e) {
            debugLog(`Error obteniendo updatedBy para UR ${ur.attributes?.id}: ${e}`);
            return 'N/A';
        }
    }

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

        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(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(panelHeader); // Añadir el header
        panel.append(panelContent);
        panel.append(panelFooter);
        panel.appendTo('body').fadeIn(300);
        estado.panelVisible = true;
    }

    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 por</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' : '';
                const actualizadoPor = obtenerActualizadoPor(ur);

                tablaHTML += `
                    <tr id="ur-row-${id}" class="${esVisitada}">
                        <td>${actualizadoPor}</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);
            });
        }, 1500);
    }

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

            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('_', '-')}"]
                        `).first();

                        if (statusButton.length) {
                            statusButton.click();

                            setTimeout(() => {
                                $('.button-primary').click();

                                setTimeout(() => {
                                    // Solo guardar si el guardado automático está activado
                                    if (estado.guardadoAutomatico) {
                                        guardarURviaFetch(ur);
                                    } else {
                                        debugLog('[UR Manager] Guardado automático desactivado por el usuario.');
                                        estado.accionEnProgreso = false;
                                    }
                                }, 500);
                            }, 500);
                        } else {
                            estado.accionEnProgreso = false;
                        }
                    }, 1000);
                } else {
                    estado.accionEnProgreso = false;
                }
            }, 500);
        }, CONFIG.RETRASO_ESPERA_UI);
    }

    function guardarURviaFetch(ur) {
        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() {
        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();

})();