Auto Refresh Interface for Hotsauce

Auto refresh for HotSOS

目前为 2025-04-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Auto Refresh Interface for Hotsauce
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.0
  5. // @description Auto refresh for HotSOS
  6. // @author PC
  7. // @match https://na4.m-tech.com/service-optimization/operations/service-orders/*
  8. // @run-at document-idle
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_addStyle
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // CSS Styles - Modified for better spacing between sections
  19. GM_addStyle(`
  20. #auto-refresh-container {
  21. position: fixed;
  22. top: 20px;
  23. left: 20px;
  24. background-color: #f5f5f5;
  25. border: 1px solid #ddd;
  26. border-radius: 6px;
  27. box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  28. padding: 8px 10px;
  29. z-index: 9999;
  30. font-family: Arial, sans-serif;
  31. min-width: 150px;
  32. max-width: 200px;
  33. user-select: none;
  34. font-size: 12px;
  35. }
  36. #auto-refresh-header {
  37. display: flex;
  38. justify-content: space-between;
  39. align-items: center;
  40. margin-bottom: 10px; /* Increased margin */
  41. cursor: move;
  42. padding-bottom: 6px; /* Added padding */
  43. border-bottom: 1px solid #eee; /* Added separator */
  44. }
  45. #auto-refresh-title {
  46. font-weight: bold;
  47. color: #444;
  48. font-size: 12px;
  49. }
  50. #auto-refresh-controls {
  51. display: flex;
  52. align-items: center;
  53. gap: 8px;
  54. }
  55. #auto-refresh-collapse {
  56. cursor: pointer;
  57. font-size: 14px;
  58. color: #666;
  59. width: 16px;
  60. height: 16px;
  61. display: flex;
  62. align-items: center;
  63. justify-content: center;
  64. border-radius: 50%;
  65. }
  66. #auto-refresh-collapse:hover {
  67. background-color: #eee;
  68. }
  69. #auto-refresh-body {
  70. overflow: hidden;
  71. transition: max-height 0.3s ease;
  72. }
  73. #auto-refresh-presets {
  74. padding-bottom: 8px; /* Added padding */
  75. margin-bottom: 8px; /* Added margin */
  76. border-bottom: 1px solid #eee; /* Added separator */
  77. }
  78. .auto-refresh-preset {
  79. display: inline-block;
  80. background-color: #e9e9e9;
  81. border: none;
  82. border-radius: 4px;
  83. padding: 4px 8px;
  84. margin: 3px;
  85. cursor: pointer;
  86. transition: all 0.2s ease;
  87. font-size: 11px;
  88. }
  89. .auto-refresh-preset:hover {
  90. background-color: #d9d9d9;
  91. }
  92. .auto-refresh-preset.active {
  93. background-color: #4a89dc;
  94. color: white;
  95. }
  96. .auto-refresh-disabled .auto-refresh-preset {
  97. opacity: 0.5;
  98. cursor: default;
  99. }
  100. #auto-refresh-status {
  101. margin-top: 6px;
  102. font-size: 11px;
  103. color: #666;
  104. white-space: nowrap;
  105. overflow: hidden;
  106. text-overflow: ellipsis;
  107. padding-top: 2px; /* Added padding */
  108. }
  109. #auto-refresh-status.warning {
  110. color: #f44336;
  111. }
  112. #auto-refresh-status.info {
  113. color: #9e9e9e;
  114. }
  115. #auto-refresh-status.success {
  116. color: #4caf50;
  117. }
  118. #auto-refresh-edit-overlay {
  119. position: fixed;
  120. top: 0;
  121. left: 0;
  122. right: 0;
  123. bottom: 0;
  124. background-color: rgba(0,0,0,0.5);
  125. z-index: 10000;
  126. display: flex;
  127. align-items: center;
  128. justify-content: center;
  129. }
  130. #auto-refresh-edit-panel {
  131. background-color: white;
  132. border-radius: 6px;
  133. padding: 16px;
  134. width: 250px;
  135. box-shadow: 0 3px 14px rgba(0,0,0,0.2);
  136. }
  137. .auto-refresh-form-group {
  138. margin-bottom: 12px;
  139. }
  140. .auto-refresh-form-label {
  141. display: block;
  142. margin-bottom: 4px;
  143. font-weight: bold;
  144. font-size: 12px;
  145. }
  146. .auto-refresh-form-input {
  147. width: 100%;
  148. padding: 6px;
  149. border: 1px solid #ddd;
  150. border-radius: 4px;
  151. font-size: 12px;
  152. }
  153. .auto-refresh-form-buttons {
  154. display: flex;
  155. justify-content: flex-end;
  156. }
  157. .auto-refresh-form-button {
  158. padding: 6px 12px;
  159. border: none;
  160. border-radius: 4px;
  161. margin-left: 8px;
  162. cursor: pointer;
  163. font-size: 12px;
  164. }
  165. .auto-refresh-form-button.cancel {
  166. background-color: #f5f5f5;
  167. }
  168. .auto-refresh-form-button.save {
  169. background-color: #4a89dc;
  170. color: white;
  171. }
  172. .switch {
  173. position: relative;
  174. display: inline-block;
  175. width: 32px;
  176. height: 16px;
  177. }
  178. .switch input {
  179. opacity: 0;
  180. width: 0;
  181. height: 0;
  182. }
  183. .slider {
  184. position: absolute;
  185. cursor: pointer;
  186. top: 0;
  187. left: 0;
  188. right: 0;
  189. bottom: 0;
  190. background-color: #ccc;
  191. transition: .3s;
  192. border-radius: 16px;
  193. }
  194. .slider:before {
  195. position: absolute;
  196. content: "";
  197. height: 12px;
  198. width: 12px;
  199. left: 2px;
  200. bottom: 2px;
  201. background-color: white;
  202. transition: .3s;
  203. border-radius: 50%;
  204. }
  205. input:checked + .slider {
  206. background-color: #4a89dc;
  207. }
  208. input:checked + .slider:before {
  209. transform: translateX(16px);
  210. }
  211. `);
  212.  
  213. // Default presets with proper formatting
  214. const DEFAULT_PRESETS = [
  215. { name: '15s', seconds: 15 },
  216. { name: '30s', seconds: 30 },
  217. { name: '1min', seconds: 60 },
  218. { name: '5min', seconds: 300 }
  219. ];
  220.  
  221. // State management
  222. let state = {
  223. enabled: true,
  224. collapsed: false,
  225. activePreset: null,
  226. presets: GM_getValue('autoRefreshPresets', DEFAULT_PRESETS),
  227. position: GM_getValue('autoRefreshPosition', { x: 20, y: 20 }),
  228. refreshTimer: null,
  229. lastRefreshTime: null,
  230. editingPreset: null,
  231. onServiceOrdersPage: false,
  232. initialized: false,
  233. observerDebounce: false,
  234. refreshButtonObserved: false,
  235. isDragging: false
  236. };
  237.  
  238. // Check if we're on the service orders page
  239. function checkIfOnServiceOrdersPage() {
  240. try {
  241. // Check if URL matches the service orders page pattern
  242. const isServiceOrdersURL = window.location.href.includes('/service-optimization/operations/service-orders');
  243.  
  244. // Also check for refresh button as a fallback
  245. const refreshButton = findRefreshButton();
  246.  
  247. state.onServiceOrdersPage = isServiceOrdersURL && !!refreshButton;
  248.  
  249. // If we're on the service orders page, observe the refresh button for manual clicks
  250. if (state.onServiceOrdersPage && !state.refreshButtonObserved && refreshButton) {
  251. observeRefreshButton(refreshButton);
  252. }
  253.  
  254. return state.onServiceOrdersPage;
  255. } catch (error) {
  256. console.error('Error checking page type:', error);
  257. return false;
  258. }
  259. }
  260.  
  261. // Observe the refresh button for manual clicks
  262. function observeRefreshButton(button) {
  263. try {
  264. if (!button || state.refreshButtonObserved) return;
  265.  
  266. // Add click listener to track manual refreshes
  267. button.addEventListener('click', function manualRefreshHandler() {
  268. // Only update if auto-refresh is enabled
  269. if (state.enabled) {
  270. // Update time
  271. state.lastRefreshTime = new Date();
  272. updateStatus('success', `Last checked: ${formatTime(state.lastRefreshTime)}`);
  273.  
  274. // Reset timer if we have an active preset
  275. if (state.activePreset && state.refreshTimer) {
  276. clearAutoRefresh();
  277. state.refreshTimer = setInterval(() => {
  278. triggerRefresh();
  279. }, state.activePreset.seconds * 1000);
  280. }
  281. }
  282. });
  283.  
  284. state.refreshButtonObserved = true;
  285. console.log('Refresh button observed for manual clicks');
  286. } catch (error) {
  287. console.error('Error observing refresh button:', error);
  288. }
  289. }
  290.  
  291. // Find refresh button with more detailed search
  292. function findRefreshButton() {
  293. try {
  294. // Try different button selectors
  295. const selectors = [
  296. 'button[soe-data-cy="refresh"]',
  297. 'button[mat-icon-button] soe-icon[icon="refresh-dot"]',
  298. 'button[mat-icon-button] soe-icon[icon="refresh"]',
  299. 'button[aria-label="refresh"]',
  300. 'button.mat-icon-button mat-ripple.mat-button-ripple'
  301. ];
  302.  
  303. for (const selector of selectors) {
  304. const button = document.querySelector(selector);
  305. if (button) {
  306. return button;
  307. }
  308. }
  309.  
  310. // If we can't find with specific selectors, try a broader approach
  311. const buttons = document.querySelectorAll('button');
  312. for (const button of buttons) {
  313. const buttonHTML = button.innerHTML.toLowerCase();
  314. if (buttonHTML.includes('refresh') ||
  315. buttonHTML.includes('reload') ||
  316. buttonHTML.includes('refresh-dot')) {
  317. return button;
  318. }
  319. }
  320.  
  321. return null;
  322. } catch (error) {
  323. console.error('Error finding refresh button:', error);
  324. return null;
  325. }
  326. }
  327.  
  328. // Format time as HH:MM:SS
  329. function formatTime(date) {
  330. if (!date) return 'Never';
  331. return date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'});
  332. }
  333.  
  334. // Update status message - More concise now
  335. function updateStatus(type, message) {
  336. try {
  337. const statusEl = document.getElementById('auto-refresh-status');
  338. if (statusEl) {
  339. statusEl.className = type || '';
  340. statusEl.textContent = message || '';
  341. }
  342. } catch (error) {
  343. console.error('Error updating status:', error);
  344. }
  345. }
  346.  
  347. // Trigger refresh click
  348. function triggerRefresh() {
  349. try {
  350. const refreshButton = findRefreshButton();
  351. if (refreshButton) {
  352. // If we found the button itself, click it
  353. refreshButton.click();
  354. state.lastRefreshTime = new Date();
  355. updateStatus('success', `Last checked: ${formatTime(state.lastRefreshTime)}`);
  356. return true;
  357. } else {
  358. // If we couldn't find the button, show an error
  359. updateStatus('warning', 'Refresh button not found');
  360. return false;
  361. }
  362. } catch (error) {
  363. console.error('Error triggering refresh:', error);
  364. updateStatus('warning', 'Error refreshing');
  365. return false;
  366. }
  367. }
  368.  
  369. // Start auto refresh with given preset
  370. function startAutoRefresh(preset) {
  371. try {
  372. // Clear existing timer
  373. clearAutoRefresh();
  374.  
  375. // Set active preset
  376. state.activePreset = preset;
  377.  
  378. // Update UI
  379. updateActivePreset();
  380.  
  381. // Trigger a refresh immediately when setting a new preset
  382. if (state.onServiceOrdersPage) {
  383. triggerRefresh();
  384. }
  385.  
  386. // Only start timer if enabled and on service orders page
  387. if (state.enabled && state.onServiceOrdersPage) {
  388. state.refreshTimer = setInterval(() => {
  389. triggerRefresh();
  390. }, preset.seconds * 1000);
  391. }
  392.  
  393. // Save state
  394. saveState();
  395. } catch (error) {
  396. console.error('Error starting auto refresh:', error);
  397. }
  398. }
  399.  
  400. // Clear auto refresh timer
  401. function clearAutoRefresh() {
  402. try {
  403. if (state.refreshTimer) {
  404. clearInterval(state.refreshTimer);
  405. state.refreshTimer = null;
  406. }
  407. } catch (error) {
  408. console.error('Error clearing auto refresh:', error);
  409. }
  410. }
  411.  
  412. // Update active preset highlighting
  413. function updateActivePreset() {
  414. try {
  415. // Remove active class from all presets
  416. document.querySelectorAll('.auto-refresh-preset').forEach(btn => {
  417. btn.classList.remove('active');
  418. });
  419.  
  420. // Add active class to current preset
  421. if (state.activePreset) {
  422. const activeBtn = document.querySelector(`.auto-refresh-preset[data-seconds="${state.activePreset.seconds}"]`);
  423. if (activeBtn) {
  424. activeBtn.classList.add('active');
  425. }
  426. }
  427. } catch (error) {
  428. console.error('Error updating active preset:', error);
  429. }
  430. }
  431.  
  432. // Toggle auto refresh enabled state
  433. function toggleEnabled() {
  434. try {
  435. state.enabled = !state.enabled;
  436.  
  437. // Update UI
  438. const container = document.getElementById('auto-refresh-container');
  439. if (container) {
  440. if (state.enabled) {
  441. container.classList.remove('auto-refresh-disabled');
  442. if (state.activePreset && state.onServiceOrdersPage) {
  443. startAutoRefresh(state.activePreset);
  444. }
  445. } else {
  446. container.classList.add('auto-refresh-disabled');
  447. clearAutoRefresh();
  448. updateStatus('info', 'Auto refresh is off');
  449. }
  450. }
  451.  
  452. // Save state
  453. saveState();
  454. } catch (error) {
  455. console.error('Error toggling enabled state:', error);
  456. }
  457. }
  458.  
  459. // Toggle collapsed state
  460. function toggleCollapsed() {
  461. try {
  462. state.collapsed = !state.collapsed;
  463.  
  464. // Update UI
  465. const body = document.getElementById('auto-refresh-body');
  466. const collapseBtn = document.getElementById('auto-refresh-collapse');
  467.  
  468. if (body && collapseBtn) {
  469. if (state.collapsed) {
  470. body.style.maxHeight = '0';
  471. collapseBtn.textContent = '+';
  472. } else {
  473. body.style.maxHeight = '500px';
  474. collapseBtn.textContent = '-';
  475. }
  476. }
  477.  
  478. // Save state
  479. saveState();
  480. } catch (error) {
  481. console.error('Error toggling collapsed state:', error);
  482. }
  483. }
  484.  
  485. // Save state to GM storage
  486. function saveState() {
  487. try {
  488. GM_setValue('autoRefreshPresets', state.presets);
  489. GM_setValue('autoRefreshPosition', state.position);
  490. GM_setValue('autoRefreshEnabled', state.enabled);
  491. GM_setValue('autoRefreshCollapsed', state.collapsed);
  492. GM_setValue('autoRefreshActivePreset', state.activePreset);
  493. } catch (error) {
  494. console.error('Error saving state:', error);
  495. }
  496. }
  497.  
  498. // Load state from GM storage
  499. function loadState() {
  500. try {
  501. state.presets = GM_getValue('autoRefreshPresets', DEFAULT_PRESETS);
  502. state.position = GM_getValue('autoRefreshPosition', { x: 20, y: 20 });
  503. state.enabled = GM_getValue('autoRefreshEnabled', true);
  504. state.collapsed = GM_getValue('autoRefreshCollapsed', false);
  505.  
  506. // Get the previously saved active preset, or default to 30s preset on first launch
  507. const savedActivePreset = GM_getValue('autoRefreshActivePreset', null);
  508. if (savedActivePreset) {
  509. state.activePreset = savedActivePreset;
  510. } else {
  511. // Find the 30s preset and set it as default
  512. state.activePreset = state.presets.find(preset => preset.seconds === 30) || state.presets[1];
  513. }
  514. } catch (error) {
  515. console.error('Error loading state:', error);
  516. // Fallback to defaults
  517. state.presets = DEFAULT_PRESETS;
  518. state.position = { x: 20, y: 20 };
  519. state.enabled = true;
  520. state.collapsed = false;
  521. // Set default preset to 30s
  522. state.activePreset = state.presets.find(preset => preset.seconds === 30) || state.presets[1];
  523. }
  524. }
  525.  
  526. // Format preset name based on seconds with improved logic
  527. function formatPresetName(seconds) {
  528. if (seconds < 60) {
  529. return `${seconds}s`;
  530. } else {
  531. return `${Math.floor(seconds / 60)}min`;
  532. }
  533. }
  534.  
  535. // Show preset edit panel
  536. function showEditPanel(preset) {
  537. try {
  538. state.editingPreset = preset;
  539.  
  540. // Define your exact preset values
  541. const timeOptions = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 360, 420, 480, 540, 600];
  542.  
  543. // Create and append overlay
  544. const overlay = document.createElement('div');
  545. overlay.id = 'auto-refresh-edit-overlay';
  546.  
  547. const panel = document.createElement('div');
  548. panel.id = 'auto-refresh-edit-panel';
  549.  
  550. // Create options HTML
  551. const optionsHTML = timeOptions.map(value => {
  552. const selected = value === preset.seconds ? 'selected' : '';
  553. const label = value < 60 ? `${value}s` : `${Math.floor(value/60)}min`;
  554. return `<option value="${value}" ${selected}>${label}</option>`;
  555. }).join('');
  556.  
  557. panel.innerHTML = `
  558. <h3 style="font-size: 14px; margin-top: 0;">Edit Preset</h3>
  559. <div class="auto-refresh-form-group">
  560. <label class="auto-refresh-form-label">Select Interval:</label>
  561. <select id="edit-preset-seconds" class="auto-refresh-form-input" style="appearance: auto; background-color: white;">
  562. ${optionsHTML}
  563. </select>
  564. <div id="edit-preset-error" style="color: #f44336; font-size: 11px; margin: 8px 0; display: none;"></div>
  565. </div>
  566. <div class="auto-refresh-form-buttons">
  567. <button class="auto-refresh-form-button cancel">Cancel</button>
  568. <button class="auto-refresh-form-button save">Save</button>
  569. </div>
  570. `;
  571.  
  572. overlay.appendChild(panel);
  573. document.body.appendChild(overlay);
  574.  
  575. // Add event listeners
  576. overlay.querySelector('.cancel').addEventListener('click', hideEditPanel);
  577. overlay.querySelector('.save').addEventListener('click', saveEditedPreset);
  578.  
  579. // Handle pressing Enter key
  580. const selectInput = document.getElementById('edit-preset-seconds');
  581. selectInput.addEventListener('keydown', (e) => {
  582. if (e.key === 'Enter') {
  583. e.preventDefault();
  584. saveEditedPreset();
  585. }
  586. });
  587.  
  588. // Focus the select field
  589. selectInput.focus();
  590. } catch (error) {
  591. console.error('Error showing edit panel:', error);
  592. }
  593. }
  594. // Save edited preset with duplicate check
  595. function saveEditedPreset() {
  596. try {
  597. const secondsInput = document.getElementById('edit-preset-seconds');
  598. const errorElement = document.getElementById('edit-preset-error') ||
  599. (() => {
  600. // Create error element if it doesn't exist
  601. const el = document.createElement('div');
  602. el.id = 'edit-preset-error';
  603. el.style.cssText = 'color: #f44336; font-size: 11px; margin: 8px 0; display: none;';
  604. secondsInput.parentNode.insertBefore(el, secondsInput.nextSibling);
  605. return el;
  606. })();
  607.  
  608. if (!secondsInput) return;
  609.  
  610. const seconds = parseInt(secondsInput.value, 10);
  611.  
  612. if (isNaN(seconds)) {
  613. return;
  614. }
  615.  
  616. // Check if another preset already has this value
  617. const duplicatePreset = state.presets.find(p =>
  618. p.seconds === seconds &&
  619. !(p.name === state.editingPreset.name && p.seconds === state.editingPreset.seconds)
  620. );
  621.  
  622. if (duplicatePreset) {
  623. errorElement.textContent = `Preset "${duplicatePreset.name}" already uses this interval. Please choose another value.`;
  624. errorElement.style.display = 'block';
  625. return;
  626. }
  627.  
  628. // Auto-format name based on seconds value
  629. const name = formatPresetName(seconds);
  630.  
  631. // Find and update the preset
  632. const presetIndex = state.presets.findIndex(p =>
  633. p.name === state.editingPreset.name && p.seconds === state.editingPreset.seconds
  634. );
  635.  
  636. if (presetIndex >= 0) {
  637. state.presets[presetIndex] = { name, seconds };
  638.  
  639. // If this was the active preset, update it
  640. if (state.activePreset &&
  641. state.activePreset.name === state.editingPreset.name &&
  642. state.activePreset.seconds === state.editingPreset.seconds) {
  643. state.activePreset = { name, seconds };
  644. if (state.enabled && state.onServiceOrdersPage) {
  645. startAutoRefresh(state.activePreset);
  646. }
  647. }
  648.  
  649. // Update UI
  650. createOrUpdateUI();
  651.  
  652. // Save state
  653. saveState();
  654. }
  655.  
  656. hideEditPanel();
  657. } catch (error) {
  658. console.error('Error saving edited preset:', error);
  659. }
  660. }
  661. // Hide preset edit panel
  662. function hideEditPanel() {
  663. try {
  664. const overlay = document.getElementById('auto-refresh-edit-overlay');
  665. if (overlay) {
  666. overlay.remove();
  667. }
  668. state.editingPreset = null;
  669. } catch (error) {
  670. console.error('Error hiding edit panel:', error);
  671. }
  672. }
  673.  
  674. // Make element draggable - Enhanced for both mouse and touch devices
  675. function makeDraggable(element, handleElement) {
  676. try {
  677. let startX, startY, initialX, initialY;
  678. let isDragging = false;
  679.  
  680. const onStart = (e) => {
  681. // Don't initiate drag if it's a button or control
  682. if (e.target.id === 'auto-refresh-collapse' ||
  683. e.target.id === 'auto-refresh-toggle' ||
  684. e.target.closest('button') ||
  685. e.target.closest('.switch')) {
  686. return;
  687. }
  688.  
  689. isDragging = true;
  690. state.isDragging = true;
  691.  
  692. // Get starting positions
  693. if (e.type === 'mousedown') {
  694. startX = e.clientX;
  695. startY = e.clientY;
  696. } else if (e.type === 'touchstart') {
  697. startX = e.touches[0].clientX;
  698. startY = e.touches[0].clientY;
  699. }
  700.  
  701. initialX = element.offsetLeft;
  702. initialY = element.offsetTop;
  703.  
  704. // Add move and end event listeners
  705. if (e.type === 'mousedown') {
  706. document.addEventListener('mousemove', onMove);
  707. document.addEventListener('mouseup', onEnd);
  708. } else if (e.type === 'touchstart') {
  709. document.addEventListener('touchmove', onMove, { passive: false });
  710. document.addEventListener('touchend', onEnd);
  711. }
  712.  
  713. // Prevent default behavior only for the handle
  714. if (e.target === handleElement || handleElement.contains(e.target)) {
  715. if (e.preventDefault) e.preventDefault();
  716. }
  717. };
  718.  
  719. const onMove = (e) => {
  720. if (!isDragging) return;
  721.  
  722. // Calculate new position
  723. let clientX, clientY;
  724. if (e.type === 'mousemove') {
  725. clientX = e.clientX;
  726. clientY = e.clientY;
  727. } else if (e.type === 'touchmove') {
  728. clientX = e.touches[0].clientX;
  729. clientY = e.touches[0].clientY;
  730. e.preventDefault(); // Prevent scrolling when dragging
  731. }
  732.  
  733. // Calculate new position
  734. const deltaX = clientX - startX;
  735. const deltaY = clientY - startY;
  736.  
  737. const newLeft = initialX + deltaX;
  738. const newTop = initialY + deltaY;
  739.  
  740. // Ensure the element stays within viewport bounds
  741. const maxTop = window.innerHeight - element.offsetHeight;
  742. const maxLeft = window.innerWidth - element.offsetWidth;
  743.  
  744. element.style.top = `${Math.min(Math.max(0, newTop), maxTop)}px`;
  745. element.style.left = `${Math.min(Math.max(0, newLeft), maxLeft)}px`;
  746. };
  747.  
  748. const onEnd = (e) => {
  749. isDragging = false;
  750.  
  751. // Wait a short moment before setting isDragging to false to prevent accidental clicks
  752. setTimeout(() => {
  753. state.isDragging = false;
  754. }, 50);
  755.  
  756. // Remove move and end event listeners
  757. document.removeEventListener('mousemove', onMove);
  758. document.removeEventListener('mouseup', onEnd);
  759. document.removeEventListener('touchmove', onMove);
  760. document.removeEventListener('touchend', onEnd);
  761.  
  762. // Save the new position
  763. state.position = {
  764. x: parseInt(element.style.left, 10) || 20,
  765. y: parseInt(element.style.top, 10) || 20
  766. };
  767. saveState();
  768. };
  769.  
  770. handleElement.addEventListener('mousedown', onStart);
  771. handleElement.addEventListener('touchstart', onStart, { passive: true });
  772. } catch (error) {
  773. console.error('Error making element draggable:', error);
  774. }
  775. }
  776. // Create or update UI
  777. function createOrUpdateUI() {
  778. try {
  779. // Check if UI already exists
  780. let container = document.getElementById('auto-refresh-container');
  781.  
  782. if (!container) {
  783. // Create container
  784. container = document.createElement('div');
  785. container.id = 'auto-refresh-container';
  786. document.body.appendChild(container);
  787.  
  788. // Set initial position
  789. container.style.left = `${state.position.x}px`;
  790. container.style.top = `${state.position.y}px`;
  791.  
  792. // Create UI structure - Updated for more compact layout with better spacing
  793. container.innerHTML = `
  794. <div id="auto-refresh-header">
  795. <span id="auto-refresh-title">Auto Refresh</span>
  796. <div id="auto-refresh-controls">
  797. <label class="switch">
  798. <input type="checkbox" id="auto-refresh-toggle" ${state.enabled ? 'checked' : ''}>
  799. <span class="slider"></span>
  800. </label>
  801. <span id="auto-refresh-collapse">${state.collapsed ? '+' : '-'}</span>
  802. </div>
  803. </div>
  804. <div id="auto-refresh-body" style="max-height: ${state.collapsed ? '0' : '500px'};">
  805. <div id="auto-refresh-presets"></div>
  806. <div id="auto-refresh-status" class="info">Initializing...</div>
  807. </div>
  808. `;
  809.  
  810. // Add event listeners
  811. document.getElementById('auto-refresh-toggle').addEventListener('change', toggleEnabled);
  812.  
  813. // IMPORTANT FIX: Add event listener directly without cloning
  814. const collapseBtn = document.getElementById('auto-refresh-collapse');
  815. if (collapseBtn) {
  816. // Remove previous listeners if any (just in case)
  817. const newCollapseBtn = collapseBtn.cloneNode(true);
  818. if (collapseBtn.parentNode) {
  819. collapseBtn.parentNode.replaceChild(newCollapseBtn, collapseBtn);
  820. }
  821.  
  822. // Get fresh reference to the button that's now in the DOM
  823. const domCollapseBtn = document.getElementById('auto-refresh-collapse');
  824. if (domCollapseBtn) {
  825. // Use normal click handler - simpler is better
  826. domCollapseBtn.onclick = function(e) {
  827. // Stop propagation to prevent draggable from interfering
  828. if (e) {
  829. e.stopPropagation();
  830. e.preventDefault();
  831. }
  832. toggleCollapsed();
  833. return false;
  834. };
  835. }
  836. }
  837.  
  838. // Make draggable - with special handling for the collapse button
  839. makeDraggable(container, document.getElementById('auto-refresh-header'));
  840.  
  841. // If disabled, add class
  842. if (!state.enabled) {
  843. container.classList.add('auto-refresh-disabled');
  844. }
  845. }
  846.  
  847. // Update presets
  848. const presetsContainer = document.getElementById('auto-refresh-presets');
  849. if (presetsContainer) {
  850. presetsContainer.innerHTML = '';
  851.  
  852. state.presets.forEach(preset => {
  853. const presetBtn = document.createElement('button');
  854. presetBtn.className = 'auto-refresh-preset';
  855. presetBtn.textContent = preset.name;
  856. presetBtn.dataset.seconds = preset.seconds;
  857.  
  858. // If this is the active preset, add active class
  859. if (state.activePreset &&
  860. state.activePreset.name === preset.name &&
  861. state.activePreset.seconds === preset.seconds) {
  862. presetBtn.classList.add('active');
  863. }
  864.  
  865. // Add click event
  866. presetBtn.addEventListener('click', (e) => {
  867. // Only process click if we're not in a drag operation
  868. if (!state.isDragging && state.enabled) {
  869. startAutoRefresh(preset);
  870. }
  871. });
  872.  
  873. // Add context menu / long press
  874. presetBtn.addEventListener('contextmenu', (e) => {
  875. e.preventDefault();
  876. showEditPanel(preset);
  877. });
  878.  
  879. // Long press detection for touch devices
  880. let longPressTimer;
  881. let longPressStarted = false;
  882.  
  883. presetBtn.addEventListener('touchstart', (e) => {
  884. longPressStarted = true;
  885. longPressTimer = setTimeout(() => {
  886. if (longPressStarted) {
  887. showEditPanel(preset);
  888. }
  889. }, 800);
  890. });
  891.  
  892. presetBtn.addEventListener('touchmove', () => {
  893. // Cancel long press if user moves finger
  894. longPressStarted = false;
  895. clearTimeout(longPressTimer);
  896. });
  897.  
  898. presetBtn.addEventListener('touchend', () => {
  899. // Only process click if we're not in a drag operation and not a long press
  900. if (!state.isDragging && longPressStarted && state.enabled) {
  901. startAutoRefresh(preset);
  902. }
  903. longPressStarted = false;
  904. clearTimeout(longPressTimer);
  905. });
  906.  
  907. presetsContainer.appendChild(presetBtn);
  908. });
  909. }
  910.  
  911. // Update status based on current state - Simplified status messages
  912. updatePageStatus();
  913. } catch (error) {
  914. console.error('Error creating or updating UI:', error);
  915. }
  916. }
  917.  
  918. // Update page status based on current state
  919. function updatePageStatus() {
  920. if (!state.onServiceOrdersPage) {
  921. updateStatus('warning', 'Please navigate to Service Orders page');
  922. } else if (!state.enabled) {
  923. updateStatus('info', 'Auto refresh is off');
  924. } else if (state.lastRefreshTime) {
  925. updateStatus('success', `Last checked: ${formatTime(state.lastRefreshTime)}`);
  926. } else {
  927. updateStatus('info', 'Select a refresh interval');
  928. }
  929. }
  930.  
  931. // Check page and update UI accordingly
  932. function checkPageAndUpdateUI() {
  933. try {
  934. const wasOnServiceOrdersPage = state.onServiceOrdersPage;
  935. state.onServiceOrdersPage = checkIfOnServiceOrdersPage();
  936.  
  937. // Reset refresh button observed state if we're not on the service orders page
  938. if (!state.onServiceOrdersPage) {
  939. state.refreshButtonObserved = false;
  940. }
  941.  
  942. // Update status based on current state
  943. updatePageStatus();
  944.  
  945. // If we just arrived at the service orders page
  946. if (!wasOnServiceOrdersPage && state.onServiceOrdersPage) {
  947. // Trigger an immediate refresh
  948. if (state.enabled) {
  949. triggerRefresh();
  950.  
  951. // If we have an active preset, start auto refresh
  952. if (state.activePreset) {
  953. startAutoRefresh(state.activePreset);
  954. }
  955. }
  956. } else if (wasOnServiceOrdersPage && !state.onServiceOrdersPage) {
  957. // If we've left the service orders page
  958. clearAutoRefresh();
  959. }
  960. } catch (error) {
  961. console.error('Error checking page and updating UI:', error);
  962. }
  963. }
  964.  
  965. // Setup observer
  966. function setupObserver() {
  967. try {
  968. const observer = new MutationObserver(mutations => {
  969. // Only check once every second at most to avoid performance issues
  970. if (!state.observerDebounce) {
  971. state.observerDebounce = true;
  972. setTimeout(() => {
  973. checkPageAndUpdateUI();
  974. state.observerDebounce = false;
  975. }, 1000);
  976. }
  977. });
  978.  
  979. // Start observing
  980. observer.observe(document.body, { childList: true, subtree: true });
  981. } catch (error) {
  982. console.error('Error setting up observer:', error);
  983. }
  984. }
  985.  
  986. // Initialize the script
  987. function init() {
  988. try {
  989. // Load saved state
  990. loadState();
  991.  
  992. // Create initial UI
  993. createOrUpdateUI();
  994.  
  995. // Setup observer to detect page changes
  996. setupObserver();
  997.  
  998. // Check if we're on service orders page initially
  999. checkPageAndUpdateUI();
  1000.  
  1001. // If we have an active preset and are on service orders page, start auto refresh
  1002. if (state.activePreset && state.onServiceOrdersPage && state.enabled) {
  1003. startAutoRefresh(state.activePreset);
  1004. }
  1005.  
  1006. state.initialized = true;
  1007. } catch (error) {
  1008. console.error('Error initializing Auto Refresh Tool:', error);
  1009. }
  1010. }
  1011.  
  1012. // Handle errors globally to prevent leakage to user
  1013. window.addEventListener('error', function(event) {
  1014. // Check if error is from our script
  1015. if (event.filename && event.filename.includes('Auto Refresh Tool')) {
  1016. console.error('Auto Refresh Tool error:', event.error);
  1017. event.preventDefault();
  1018. return true;
  1019. }
  1020. return false;
  1021. }, true);
  1022.  
  1023. // Try multiple initialization attempts
  1024. let initAttempts = 0;
  1025. const maxInitAttempts = 3;
  1026.  
  1027. function attemptInit() {
  1028. if (initAttempts >= maxInitAttempts) {
  1029. console.error('Failed to initialize Auto Refresh Tool after multiple attempts');
  1030. return;
  1031. }
  1032.  
  1033. if (!state.initialized) {
  1034. initAttempts++;
  1035. init();
  1036.  
  1037. // Schedule another attempt if not successful
  1038. if (!state.initialized) {
  1039. setTimeout(attemptInit, 2000);
  1040. }
  1041. }
  1042. }
  1043.  
  1044. // Start the initialization process with a slight delay
  1045. setTimeout(attemptInit, 1000);
  1046.  
  1047. // Backup initialization - forcefully create UI after page has likely loaded
  1048. setTimeout(() => {
  1049. if (!document.getElementById('auto-refresh-container')) {
  1050. createOrUpdateUI();
  1051. checkPageAndUpdateUI();
  1052. }
  1053. }, 5000);
  1054. })();