WME - UR Manager

Gestión de URs

目前为 2025-04-22 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name WME - UR Manager
  3. // @namespace http://waze.com/
  4. // @version 2025.04.17.15
  5. // @description Gestión de URs
  6. // @author Crotalo
  7. // @match https://www.waze.com/*/editor*
  8. // @match https://beta.waze.com/*/editor*
  9. // @grant GM_addStyle
  10. // @require https://code.jquery.com/jquery-3.6.0.min.js
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const CONFIG = {
  17. 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.",
  18. 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. ",
  19. 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!!",
  20. DEBUG: true,
  21. BOTON_ID: 'urna-btn-fecha-exacta',
  22. PANEL_ID: 'urna-panel-fecha-exacta',
  23. INTERVALO_VERIFICACION: 5000,
  24. UMBRAL_VIEJO: 7,
  25. UMBRAL_RECIENTE: 3,
  26. RETRASO_ENTRE_ACCIONES: 800
  27. };
  28.  
  29. GM_addStyle(`
  30. #${CONFIG.BOTON_ID} {
  31. position: fixed !important;
  32. bottom: 20px !important;
  33. left: 20px !important;
  34. z-index: 99999 !important;
  35. padding: 10px 15px !important;
  36. background: #3498db !important;
  37. color: white !important;
  38. font-weight: bold !important;
  39. border: none !important;
  40. border-radius: 5px !important;
  41. cursor: pointer !important;
  42. font-family: Arial, sans-serif !important;
  43. box-shadow: 0 2px 5px rgba(0,0,0,0.2) !important;
  44. }
  45. #${CONFIG.PANEL_ID} {
  46. position: fixed;
  47. top: 80px;
  48. right: 20px;
  49. width: 500px;
  50. max-height: 70vh;
  51. min-height: 200px;
  52. display: flex;
  53. flex-direction: column;
  54. background: white;
  55. border: 2px solid #999;
  56. z-index: 99998;
  57. font-family: Arial, sans-serif;
  58. font-size: 13px;
  59. box-shadow: 2px 2px 15px rgba(0,0,0,0.3);
  60. border-radius: 5px;
  61. display: none;
  62. }
  63. #${CONFIG.PANEL_ID} .panel-content {
  64. flex: 1;
  65. overflow-y: auto;
  66. padding: 15px;
  67. max-height: calc(70vh - 60px);
  68. }
  69. #${CONFIG.PANEL_ID} table {
  70. width: 100%;
  71. border-collapse: collapse;
  72. margin-top: 10px;
  73. }
  74. #${CONFIG.PANEL_ID} th {
  75. position: sticky;
  76. top: 0;
  77. background-color: #f2f2f2;
  78. z-index: 10;
  79. }
  80. #${CONFIG.PANEL_ID} th, #${CONFIG.PANEL_ID} td {
  81. border: 1px solid #ddd;
  82. padding: 6px;
  83. text-align: left;
  84. }
  85. .ur-old { color: #d9534f; font-weight: bold; }
  86. .ur-recent { color: #5bc0de; }
  87. .ur-new { color: #5cb85c; }
  88. .ur-visitada { background-color: #fdf5d4 !important; }
  89. .ur-no-fecha { color: #777; font-style: italic; }
  90. .ur-cerrada { text-decoration: line-through; opacity: 0.6; }
  91. .btn-centrar {
  92. padding: 4px 8px;
  93. background: #3498db;
  94. color: white;
  95. border: none;
  96. border-radius: 3px;
  97. cursor: pointer;
  98. }
  99. .panel-footer {
  100. padding: 10px 15px;
  101. background: #f8f8f8;
  102. border-top: 1px solid #eee;
  103. display: flex;
  104. justify-content: center;
  105. gap: 10px;
  106. position: sticky;
  107. bottom: 0;
  108. z-index: 20;
  109. height: 60px;
  110. }
  111. .btn-global {
  112. padding: 8px 15px;
  113. border: none;
  114. border-radius: 5px;
  115. cursor: pointer;
  116. font-weight: bold;
  117. white-space: nowrap;
  118. }
  119. .btn-responder {
  120. background: #f0ad4e;
  121. color: white;
  122. }
  123. .btn-cerrar {
  124. background: #5cb85c;
  125. color: white;
  126. }
  127. .btn-resuelta {
  128. background: #5bc0de;
  129. color: white;
  130. }
  131. `);
  132.  
  133. let estado = {
  134. URsActuales: [],
  135. panelVisible: false,
  136. botonUR: null,
  137. intervaloVerificacion: null,
  138. timeouts: [],
  139. accionEnProgreso: false
  140. };
  141.  
  142. function debugLog(message) {
  143. if (CONFIG.DEBUG) console.log('[UR Script] ' + message);
  144. }
  145.  
  146. function limpiarTimeouts() {
  147. estado.timeouts.forEach(timeout => clearTimeout(timeout));
  148. estado.timeouts = [];
  149. }
  150.  
  151. function agregarTimeout(callback, delay) {
  152. const timeoutId = setTimeout(() => {
  153. callback();
  154. estado.timeouts = estado.timeouts.filter(id => id !== timeoutId);
  155. }, delay);
  156. estado.timeouts.push(timeoutId);
  157. return timeoutId;
  158. }
  159.  
  160. function togglePanelURs() {
  161. if (estado.panelVisible) {
  162. $(`#${CONFIG.PANEL_ID}`).fadeOut(300, function() {
  163. $(this).remove();
  164. });
  165. estado.panelVisible = false;
  166. limpiarTimeouts();
  167. } else {
  168. mostrarPanelURs();
  169. }
  170. }
  171.  
  172. function crearBoton() {
  173. if ($(`#${CONFIG.BOTON_ID}`).length > 0) return;
  174.  
  175. debugLog('Creando botón...');
  176. estado.botonUR = $(`<button id="${CONFIG.BOTON_ID}">📝 UR Manager</button>`)
  177. .appendTo('body')
  178. .on('click', togglePanelURs);
  179.  
  180. debugLog('Botón creado exitosamente');
  181. }
  182.  
  183. function parsearFecha(valor) {
  184. if (!valor) return null;
  185.  
  186. if (typeof valor === 'object' && '_seconds' in valor) {
  187. try {
  188. return new Date(valor._seconds * 1000 + (valor._nanoseconds / 1000000));
  189. } catch (e) {
  190. debugLog(`Error parseando Firebase Timestamp: ${JSON.stringify(valor)}`);
  191. }
  192. }
  193.  
  194. if (typeof valor === 'string' && valor.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)) {
  195. try {
  196. return new Date(valor);
  197. } catch (e) {
  198. debugLog(`Error parseando fecha ISO: ${valor}`);
  199. }
  200. }
  201.  
  202. if (/^\d+$/.test(valor)) {
  203. try {
  204. const num = parseInt(valor);
  205. return new Date(num > 1000000000000 ? num : num * 1000);
  206. } catch (e) {
  207. debugLog(`Error parseando timestamp numérico: ${valor}`);
  208. }
  209. }
  210.  
  211. return null;
  212. }
  213.  
  214. function obtenerFechaCreacionExacta(ur) {
  215. try {
  216. if (ur.attributes.driveDate) {
  217. const fecha = parsearFecha(ur.attributes.driveDate);
  218. if (fecha) return fecha;
  219. }
  220. return null;
  221. } catch (e) {
  222. debugLog(`Error obteniendo fecha: ${e}`);
  223. return null;
  224. }
  225. }
  226.  
  227. function clasificarUR(fecha) {
  228. if (!fecha) return { estado: "Sin fecha", clase: "ur-no-fecha" };
  229.  
  230. const hoy = new Date();
  231. const diff = hoy - fecha;
  232. const dias = Math.floor(diff / (1000 * 60 * 60 * 24));
  233.  
  234. if (dias > CONFIG.UMBRAL_VIEJO) return { estado: `Antigua (${dias}d)`, clase: "ur-old" };
  235. if (dias > CONFIG.UMBRAL_RECIENTE) return { estado: `Reciente (${dias}d)`, clase: "ur-recent" };
  236. return { estado: `Nueva (${dias}d)`, clase: "ur-new" };
  237. }
  238.  
  239. function mostrarPanelURs() {
  240. estado.panelVisible = true;
  241. limpiarTimeouts();
  242. $(`#${CONFIG.PANEL_ID}`).remove();
  243.  
  244. const panel = $(`<div id="${CONFIG.PANEL_ID}">`);
  245. const panelContent = $('<div class="panel-content">');
  246. const panelFooter = $(`
  247. <div class="panel-footer">
  248. <button class="btn-global btn-responder" id="responder-todas">Preguntar</button>
  249. <button class="btn-global btn-resuelta" id="resolver-todas">Resuelta</button>
  250. <button class="btn-global btn-cerrar" id="cerrar-todas">No Identificada</button>
  251. </div>
  252. `);
  253.  
  254. estado.URsActuales = obtenerURsSinAtender();
  255.  
  256. if (estado.URsActuales.length === 0) {
  257. panelContent.html('<div style="padding:15px;text-align:center;"><b>No hay URs sin atender visibles</b></div>');
  258. } else {
  259. let tablaHTML = `
  260. <h3 style="margin-top:0;">URs Activas: ${estado.URsActuales.length}</h3>
  261. <table>
  262. <thead>
  263. <tr>
  264. <th>ID</th>
  265. <th>Tipo</th>
  266. <th>Fecha Creación</th>
  267. <th>Estado</th>
  268. <th>Acción</th>
  269. </tr>
  270. </thead>
  271. <tbody>`;
  272.  
  273. estado.URsActuales.forEach(ur => {
  274. const id = ur.attributes.id;
  275. const tipo = ur.attributes.type || 'Desconocido';
  276. const fecha = obtenerFechaCreacionExacta(ur);
  277. const clasificacion = clasificarUR(fecha);
  278.  
  279. let fechaStr = 'No disponible';
  280. if (fecha) {
  281. fechaStr = fecha.toLocaleDateString('es-ES', {
  282. year: 'numeric',
  283. month: '2-digit',
  284. day: '2-digit',
  285. hour: '2-digit',
  286. minute: '2-digit'
  287. });
  288. }
  289.  
  290. tablaHTML += `
  291. <tr id="fila-ur-${id}">
  292. <td>${id}</td>
  293. <td>${tipo}</td>
  294. <td>${fechaStr}</td>
  295. <td class="${clasificacion.clase}">${clasificacion.estado}</td>
  296. <td><button class="btn-centrar" data-id="${id}">🗺️ Centrar</button></td>
  297. </tr>`;
  298. });
  299.  
  300. panelContent.html(`
  301. ${tablaHTML}
  302. </tbody>
  303. </table>
  304. `);
  305.  
  306. panelContent.on('click', '.btn-centrar', function() {
  307. if (estado.accionEnProgreso) return;
  308. const id = $(this).data('id');
  309. centrarYMostrarUR(id);
  310. $(`#fila-ur-${id}`).addClass('ur-visitada');
  311. });
  312. }
  313.  
  314. panelFooter.on('click', '#responder-todas', function() {
  315. if (estado.accionEnProgreso) return;
  316. estado.URsActuales.forEach((ur, index) => {
  317. agregarTimeout(() => responderUR(ur.attributes.id), index * CONFIG.RETRASO_ENTRE_ACCIONES);
  318. });
  319. });
  320.  
  321. panelFooter.on('click', '#resolver-todas', function() {
  322. if (estado.accionEnProgreso) return;
  323. estado.URsActuales.forEach((ur, index) => {
  324. agregarTimeout(() => resolverUR(ur.attributes.id), index * CONFIG.RETRASO_ENTRE_ACCIONES);
  325. });
  326. });
  327.  
  328. panelFooter.on('click', '#cerrar-todas', function() {
  329. if (estado.accionEnProgreso) return;
  330. estado.URsActuales.forEach((ur, index) => {
  331. agregarTimeout(() => cerrarUR(ur.attributes.id), index * CONFIG.RETRASO_ENTRE_ACCIONES);
  332. });
  333. });
  334.  
  335. panel.append(panelContent);
  336. panel.append(panelFooter);
  337. panel.appendTo('body').fadeIn(300);
  338. }
  339.  
  340. function obtenerURsSinAtender() {
  341. try {
  342. if (!W.model?.mapUpdateRequests?.objects) return [];
  343.  
  344. const bounds = W.map.getExtent();
  345. return Object.values(W.model.mapUpdateRequests.objects)
  346. .filter(ur => {
  347. const geom = ur.getOLGeometry?.();
  348. if (!geom) return false;
  349.  
  350. const center = geom.getBounds().getCenterLonLat();
  351. if (!bounds.containsLonLat(center)) return false;
  352.  
  353. if (ur.attributes.resolved) return false;
  354.  
  355. const comentarios = ur.attributes.comments || [];
  356. return !comentarios.some(c => c.type === 'user' && c.text?.trim().length > 0);
  357. });
  358. } catch (e) {
  359. debugLog('Error obteniendo URs: ' + e);
  360. return [];
  361. }
  362. }
  363.  
  364. function centrarYMostrarUR(id) {
  365. if (estado.accionEnProgreso) return;
  366. estado.accionEnProgreso = true;
  367.  
  368. limpiarTimeouts();
  369.  
  370. const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
  371. if (!ur) {
  372. debugLog(`UR ${id} no encontrada`);
  373. estado.accionEnProgreso = false;
  374. return;
  375. }
  376.  
  377. if (ur.attributes.resolved) {
  378. debugLog(`UR ${id} ya está resuelta`);
  379. $(`#fila-ur-${id}`).addClass('ur-cerrada').find('.btn-centrar').prop('disabled', true);
  380. estado.accionEnProgreso = false;
  381. return;
  382. }
  383.  
  384. const geom = ur.getOLGeometry?.();
  385. if (geom) {
  386. const center = geom.getBounds().getCenterLonLat();
  387. W.map.setCenter(center, 17);
  388.  
  389. agregarTimeout(() => {
  390. try {
  391. if (W.control?.MapUpdateRequest?.show) {
  392. W.control.MapUpdateRequest.show(ur);
  393. } else if (W.control?.MapProblem?.show) {
  394. W.control.MapProblem.show(ur);
  395. } else if (W.control?.UR?.show) {
  396. W.control.UR.show(ur);
  397. } else if (W.selectionManager) {
  398. W.selectionManager.select([ur]);
  399. }
  400. $(`#fila-ur-${id}`).addClass('ur-visitada');
  401. } catch (e) {
  402. debugLog(`Error al mostrar UR ${id}: ${e}`);
  403. } finally {
  404. estado.accionEnProgreso = false;
  405. }
  406. }, 300);
  407. } else {
  408. debugLog(`No se pudo obtener geometría para UR ${id}`);
  409. estado.accionEnProgreso = false;
  410. }
  411. }
  412.  
  413. function responderUR(id) {
  414. if (estado.accionEnProgreso) return;
  415. estado.accionEnProgreso = true;
  416.  
  417. limpiarTimeouts();
  418. const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
  419. if (!ur) {
  420. estado.accionEnProgreso = false;
  421. return;
  422. }
  423.  
  424. centrarYMostrarUR(id);
  425.  
  426. agregarTimeout(() => {
  427. const commentField = $('.new-comment-text');
  428. if (commentField.length) {
  429. commentField.val(CONFIG.MENSAJE_RESPUESTA);
  430. const sendButton = $('.send-button');
  431. if (sendButton.length) {
  432. sendButton.click();
  433. }
  434. }
  435. estado.accionEnProgreso = false;
  436. }, 1500);
  437. }
  438.  
  439. function resolverUR(id) {
  440. if (estado.accionEnProgreso) return;
  441. estado.accionEnProgreso = true;
  442.  
  443. limpiarTimeouts();
  444. const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
  445. if (!ur) {
  446. estado.accionEnProgreso = false;
  447. return;
  448. }
  449.  
  450. centrarYMostrarUR(id);
  451.  
  452. agregarTimeout(() => {
  453. const commentField = $('.new-comment-text');
  454. if (commentField.length) {
  455. commentField.val(CONFIG.MENSAJE_RESUELTA);
  456.  
  457. const resueltaButton = $('label[for="state-solved"]');
  458. if (resueltaButton.length) {
  459. resueltaButton.click();
  460.  
  461. agregarTimeout(() => {
  462. const sendButton = $('.send-button');
  463. if (sendButton.length) {
  464. sendButton.click();
  465. }
  466. const noIssueButton = document.querySelector('[data-status="NO_ISSUE"]');
  467. if (noIssueButton) noIssueButton.click();
  468.  
  469. const okButton = Array.from(document.querySelectorAll('button')).find(btn =>
  470. btn.textContent.trim().includes('OK') || btn.textContent.trim().includes('Aplicar')
  471. );
  472. if (okButton) okButton.click();
  473.  
  474. agregarTimeout(() => {
  475. $(`#fila-ur-${id}`).addClass('ur-cerrada').find('.btn-centrar').prop('disabled', true);
  476. estado.accionEnProgreso = false;
  477. }, 500);
  478. }, 300);
  479. } else {
  480. estado.accionEnProgreso = false;
  481. }
  482. } else {
  483. estado.accionEnProgreso = false;
  484. }
  485. }, 1500);
  486. }
  487.  
  488. function cerrarUR(id) {
  489. if (estado.accionEnProgreso) return;
  490. estado.accionEnProgreso = true;
  491.  
  492. limpiarTimeouts();
  493. const ur = W.model.mapUpdateRequests.getObjectById(Number(id));
  494. if (!ur) {
  495. estado.accionEnProgreso = false;
  496. return;
  497. }
  498.  
  499. centrarYMostrarUR(id);
  500.  
  501. agregarTimeout(() => {
  502. const commentField = $('.new-comment-text');
  503. if (commentField.length) {
  504. commentField.val(CONFIG.MENSAJE_CIERRE);
  505. const sendButton = $('.send-button');
  506. if (sendButton.length) {
  507. sendButton.click();
  508.  
  509. agregarTimeout(() => {
  510. const niButton = $('label[for="state-not-identified"]');
  511. if (niButton.length) {
  512. niButton.click();
  513.  
  514. agregarTimeout(() => {
  515. $(`#fila-ur-${id}`).addClass('ur-cerrada').find('.btn-centrar').prop('disabled', true);
  516. estado.accionEnProgreso = false;
  517. }, 500);
  518. } else {
  519. estado.accionEnProgreso = false;
  520. }
  521. }, 300);
  522. } else {
  523. estado.accionEnProgreso = false;
  524. }
  525. } else {
  526. estado.accionEnProgreso = false;
  527. }
  528. }, 1500);
  529. estado.accionEnProgreso = false;
  530. }
  531.  
  532. function inicializarScript() {
  533. debugLog('Inicializando script...');
  534. window.togglePanelURs = togglePanelURs;
  535. crearBoton();
  536.  
  537. estado.intervaloVerificacion = setInterval(() => {
  538. if ($(`#${CONFIG.BOTON_ID}`).length === 0) {
  539. debugLog('Botón no encontrado, recreando...');
  540. crearBoton();
  541. }
  542. }, CONFIG.INTERVALO_VERIFICACION);
  543.  
  544. debugLog('Script inicializado correctamente');
  545. }
  546.  
  547. function esperarWME() {
  548. if (typeof W === 'undefined' || !W.loginManager || !W.model || !W.map) {
  549. debugLog('WME no está completamente cargado, reintentando...');
  550. setTimeout(esperarWME, 1000);
  551. return;
  552. }
  553.  
  554. if (!W.model.mapUpdateRequests) {
  555. debugLog('Módulo mapUpdateRequests no está disponible, reintentando...');
  556. setTimeout(esperarWME, 1000);
  557. return;
  558. }
  559.  
  560. setTimeout(inicializarScript, 2000);
  561. }
  562.  
  563. debugLog('Script cargado, esperando WME...');
  564. esperarWME();
  565. })();