WME - UR Manager

Ultimate UR Management Toolkit with UI refinements and restored logic.

// ==UserScript==
// @name         WME - UR Manager
// @namespace    http://waze.com/
// @version      2025.07.02.6
// @description  Ultimate UR Management Toolkit with UI refinements and restored logic.
// @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!",
        ZOOM_INICIAL: 13,
        ZOOM_DETALLE: 18,
        PANEL_ID: 'urna-manager-sidebar-panel',
        RETRASO_ESPERA_UI: 1500,
        UMBRAL_VIEJO: 7,
        UMBRAL_RECIENTE: 3
    };

    let estado = {
        URsIniciales: [],
        urVisitadas: [],
        accionEnProgreso: false,
        guardadoAutomatico: true
    };

    GM_addStyle(`
        #sidebar .nav-tabs a[href="#${CONFIG.PANEL_ID}"] { padding: 15px 4px; font-size: 10px; text-align: center; line-height: 1.2; }
        #${CONFIG.PANEL_ID} { padding: 0 5px; font-family: Arial, sans-serif; font-size: 10px; }
        #${CONFIG.PANEL_ID} .panel-header, #${CONFIG.PANEL_ID} .panel-footer { padding: 10px 15px; background: #f8f8f8; }
        #${CONFIG.PANEL_ID} .panel-header { border-bottom: 1px solid #eee; }
        #${CONFIG.PANEL_ID} .panel-footer { border-top: 1px solid #eee; }
        #${CONFIG.PANEL_ID} .panel-header label { font-weight: bold; margin: 0; }
        #${CONFIG.PANEL_ID} .panel-content { padding: 15px; overflow-y: auto; max-height: calc(100vh - 380px); }

        /* --- ESTILOS DE TABLA CORREGIDOS --- */
        #${CONFIG.PANEL_ID} table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 10px;
            table-layout: fixed; /* CLAVE: Forza los anchos definidos */
        }
        #${CONFIG.PANEL_ID} th, #${CONFIG.PANEL_ID} td {
            border: 1px solid #ddd;
            padding: 5px;
            text-align: left;
            overflow: hidden; /* Evita que el texto largo rompa el diseño */
            text-overflow: ellipsis; /* Añade "..." si el texto no cabe */
            white-space: nowrap; /* Mantiene el texto en una sola línea */
        }
        /* -- Definición de anchos para cada columna -- */
        #${CONFIG.PANEL_ID} th:nth-child(1) { width: 25%; } /* Columna Actualizado: */
        #${CONFIG.PANEL_ID} th:nth-child(2) { width: 30%; } /* Columna Fecha Creación */
        #${CONFIG.PANEL_ID} th:nth-child(3) { width: 20%; } /* Columna Estado */
        #${CONFIG.PANEL_ID} th:nth-child(4) { width: 14%; text-align: center; }
        #${CONFIG.PANEL_ID} th:nth-child(5) { width: 13%; text-align: center; }

        #${CONFIG.PANEL_ID} th { position: sticky; top: -15px; background-color: #f2f2f2; z-index: 1; }
        .btn-centrar { padding: 4px 8px; background: #3498db; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 14px; }
        .panel-footer { display: flex; justify-content: space-around; flex-wrap: wrap; }
        .btn-global { padding: 8px; border: none; border-radius: 5px; cursor: pointer; font-weight: bold; text-align: center; flex: 1 1 45%; margin: 4px; }
        .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 {
            const updatedById = String(ur.attributes?.updatedBy || ur.attributes?.metaData?.updatedBy || ur.updatedBy || 'N/A');
            if (updatedById === 'N/A') return 'N/A'; if (updatedById === "-1") return "Wazer"; if (updatedById === "0") return "System";
            const userIdNum = parseInt(updatedById, 10);
            if (W.model.users) { const user = W.model.users.getObjectById(userIdNum); if (user && user.attributes && user.attributes.userName) return user.attributes.userName; }
            return updatedById;
        } catch (e) { debugLog(`Error en obtenerActualizadoPor para UR ${ur.attributes?.id}: ${e}`); return (ur.attributes?.updatedBy || 'N/A').toString(); }
    }
    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 crearSidebarTab() {
        if ($(`#${CONFIG.PANEL_ID}`).length) return;
        const tab = $(`<li><a href="#${CONFIG.PANEL_ID}" data-toggle="tab" title="UR Manager">UR Manager</a></li>`);
        $('#sidebar .nav-tabs').append(tab);
        const tabContent = $(`<div class="tab-pane" id="${CONFIG.PANEL_ID}"></div>`);
        $('#sidebar .tab-content').append(tabContent);
        renderizarPanel();
    }
    function renderizarPanel() {
        const panel = $(`#${CONFIG.PANEL_ID}`);
        panel.empty();
        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">Resolver</button><button class="btn-global btn-cerrar" id="cerrar-todas">Cerrar UR</button><button class="btn-global btn-actualizar" id="actualizar-lista">Actualizar</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($(`#${CONFIG.PANEL_ID} .panel-content`)), 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, panelContent, panelFooter);
    }
    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:</th><th>Fecha Creación</th><th>Estado</th><th>UC</th><th>Acción</th></tr></thead><tbody>`;
            estado.URsIniciales.forEach(ur => {
                const id = ur.attributes?.id, fechaCreacion = obtenerFechaCreacionExacta(ur), fechaUC = obtenerFechaUC(ur), clasificacion = clasificarUR(fechaCreacion), diasDesdeUC = calcularDiferenciaDias(fechaUC), esVisitada = estado.urVisitadas.includes(id) ? 'ur-visitada' : '', 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' : 'S/C'}</td><td><button class="btn-centrar" data-id="${id}">📍</button></td></tr>`;
            });
            panelContent.html(tablaHTML + '</tbody></table>');
            panelContent.off('click', '.btn-centrar').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) { debugLog(`UR con ID ${id} no encontrada.`); return; }
        const geom = ur.getOLGeometry();
        if (!geom) { debugLog(`Geometría no encontrada para UR ${id}.`); 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); }
            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; debugLog("Campo de comentario no encontrado."); return; }
            let mensaje = '', 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).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, .wz-button.primary.save-button').click();
                                setTimeout(() => {
                                    if (estado.guardadoAutomatico) {
                                        guardarCambios();
                                    } else {
                                        debugLog('[UR Manager] Guardado automático desactivado.');
                                        estado.accionEnProgreso = false;
                                    }
                                }, 1000);
                            }, 500);
                        } else { estado.accionEnProgreso = false; debugLog("Botón de estado no encontrado."); }
                    }, 1000);
                } else { estado.accionEnProgreso = false; }
            }, 500);
        }, CONFIG.RETRASO_ESPERA_UI);
    }
    function guardarCambios() {
        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() {
        crearSidebarTab();
    }
    function esperarWME() {
        if (typeof W === 'undefined' || !W.loginManager || !W.model || !W.map || !W.controller || !$('#sidebar .nav-tabs').length) {
            setTimeout(esperarWME, 1000);
            return;
        }
        if (!W.model.actionManager || !W.model.mapUpdateRequests) {
            setTimeout(esperarWME, 1000);
            return;
        }
        setTimeout(inicializarScript, 2000);
    }
    esperarWME();
})();