Auto Refresh Interface for Hotsauce

Auto refresh for HotSOS

目前为 2025-05-01 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Auto Refresh Interface for Hotsauce
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.3
  5. // @description Auto refresh for HotSOS
  6. // @author PC
  7. // @match https://na4.m-tech.com/*
  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. // Simplified CSS
  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. padding: 8px 10px;
  28. z-index: 9999;
  29. font-family: Arial, sans-serif;
  30. min-width: 150px;
  31. max-width: 200px;
  32. user-select: none;
  33. font-size: 12px;
  34. }
  35. #auto-refresh-header {
  36. display: flex;
  37. justify-content: space-between;
  38. align-items: center;
  39. margin-bottom: 10px;
  40. cursor: move;
  41. padding-bottom: 6px;
  42. border-bottom: 1px solid #eee;
  43. }
  44. #auto-refresh-title {
  45. font-weight: bold;
  46. color: #444;
  47. }
  48. #auto-refresh-controls {
  49. display: flex;
  50. align-items: center;
  51. gap: 8px;
  52. }
  53. #auto-refresh-collapse {
  54. cursor: pointer;
  55. font-size: 14px;
  56. width: 16px;
  57. height: 16px;
  58. display: flex;
  59. align-items: center;
  60. justify-content: center;
  61. border-radius: 50%;
  62. }
  63. #auto-refresh-body {
  64. overflow: hidden;
  65. transition: max-height 0.3s ease;
  66. }
  67. #auto-refresh-presets {
  68. padding-bottom: 8px;
  69. margin-bottom: 8px;
  70. border-bottom: 1px solid #eee;
  71. }
  72. .auto-refresh-preset {
  73. display: inline-block;
  74. background-color: #e9e9e9;
  75. border: none;
  76. border-radius: 4px;
  77. padding: 4px 8px;
  78. margin: 3px;
  79. cursor: pointer;
  80. font-size: 11px;
  81. }
  82. .auto-refresh-preset:hover {
  83. background-color: #d9d9d9;
  84. }
  85. .auto-refresh-preset.active {
  86. background-color: #4a89dc;
  87. color: white;
  88. }
  89. .auto-refresh-disabled .auto-refresh-preset {
  90. opacity: 0.5;
  91. cursor: default;
  92. }
  93. #auto-refresh-status {
  94. margin-top: 6px;
  95. font-size: 11px;
  96. color: #666;
  97. white-space: nowrap;
  98. overflow: hidden;
  99. text-overflow: ellipsis;
  100. padding-top: 2px;
  101. }
  102. #auto-refresh-status.warning { color: #f44336; }
  103. #auto-refresh-status.info { color: #9e9e9e; }
  104. #auto-refresh-status.success { color: #4caf50; }
  105. #auto-refresh-edit-overlay {
  106. position: fixed;
  107. top: 0; left: 0; right: 0; bottom: 0;
  108. background-color: rgba(0,0,0,0.5);
  109. z-index: 10000;
  110. display: flex;
  111. align-items: center;
  112. justify-content: center;
  113. }
  114. #auto-refresh-edit-panel {
  115. background-color: white;
  116. border-radius: 6px;
  117. padding: 16px;
  118. width: 250px;
  119. }
  120. .auto-refresh-form-label {
  121. display: block;
  122. margin-bottom: 4px;
  123. font-weight: bold;
  124. font-size: 12px;
  125. }
  126. .auto-refresh-form-input {
  127. width: 100%;
  128. padding: 6px;
  129. border: 1px solid #ddd;
  130. border-radius: 4px;
  131. font-size: 12px;
  132. }
  133. .auto-refresh-form-buttons {
  134. display: flex;
  135. justify-content: flex-end;
  136. margin-top: 10px;
  137. }
  138. .auto-refresh-form-button {
  139. padding: 6px 12px;
  140. border: none;
  141. border-radius: 4px;
  142. margin-left: 8px;
  143. cursor: pointer;
  144. font-size: 12px;
  145. }
  146. .auto-refresh-form-button.cancel { background-color: #f5f5f5; }
  147. .auto-refresh-form-button.save {
  148. background-color: #4a89dc;
  149. color: white;
  150. }
  151. .switch {
  152. position: relative;
  153. display: inline-block;
  154. width: 32px;
  155. height: 16px;
  156. }
  157. .switch input { opacity: 0; width: 0; height: 0; }
  158. .slider {
  159. position: absolute;
  160. cursor: pointer;
  161. top: 0; left: 0; right: 0; bottom: 0;
  162. background-color: #ccc;
  163. transition: .3s;
  164. border-radius: 16px;
  165. }
  166. .slider:before {
  167. position: absolute;
  168. content: "";
  169. height: 12px;
  170. width: 12px;
  171. left: 2px;
  172. bottom: 2px;
  173. background-color: white;
  174. transition: .3s;
  175. border-radius: 50%;
  176. }
  177. input:checked + .slider {
  178. background-color: #4a89dc;
  179. }
  180. input:checked + .slider:before {
  181. transform: translateX(16px);
  182. }
  183. /* Feature toggle style */
  184. .feature-toggle {
  185. display: flex;
  186. justify-content: space-between;
  187. align-items: center;
  188. margin-bottom: 8px;
  189. padding-bottom: 8px;
  190. border-bottom: 1px solid #eee;
  191. }
  192. .feature-label {
  193. font-size: 11px;
  194. color: #444;
  195. }
  196. /* User Tip styles */
  197. #auto-refresh-tip {
  198. font-size: 10px;
  199. color: #666;
  200. margin-top: 4px;
  201. margin-bottom: 8px;
  202. line-height: 1.2;
  203. border-radius: 3px;
  204. overflow: hidden;
  205. }
  206. #auto-refresh-tip-header {
  207. display: flex;
  208. background-color: #e0e0e0;
  209. padding: 4px 6px;
  210. font-weight: bold;
  211. border-left: 2px solid #4a89dc;
  212. cursor: pointer;
  213. }
  214. #auto-refresh-tip-content {
  215. padding: 0;
  216. background-color: #f0f0f0;
  217. font-style: italic;
  218. border-left: 2px solid #4a89dc;
  219. max-height: 0;
  220. transition: all 0.3s ease;
  221. overflow: hidden;
  222. opacity: 0;
  223. }
  224. #auto-refresh-tip-content.expanded {
  225. padding: 6px;
  226. max-height: 50px;
  227. opacity: 1;
  228. }
  229. `);
  230.  
  231. // Default presets
  232. const DEFAULT_PRESETS = [
  233. { name: '15s', seconds: 15 },
  234. { name: '30s', seconds: 30 },
  235. { name: '1min', seconds: 60 },
  236. { name: '5min', seconds: 300 }
  237. ];
  238.  
  239. // Time-based schedule with direct second values
  240. const TIME_SCHEDULE = [
  241. { start: [8, 0], end: [11, 0], seconds: 15, name: '15s' },
  242. { start: [11, 0], end: [13, 0], seconds: 30, name: '30s' },
  243. { start: [13, 0], end: [17, 0], seconds: 60, name: '1min' },
  244. { start: [17, 0], end: [20, 0], seconds: 30, name: '30s' },
  245. { start: [20, 0], end: [1, 0], seconds: 60, name: '1min' },
  246. { start: [1, 0], end: [8, 0], seconds: 300, name: '5min' }
  247. ];
  248.  
  249. // Predefined time options for dropdown
  250. const TIME_OPTIONS = [15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 360, 420, 480, 540];
  251.  
  252. // Simplified state management
  253. let state = {
  254. enabled: true,
  255. collapsed: false,
  256. activePreset: null,
  257. userSelectedPreset: null, // Track user's manually selected preset
  258. presets: GM_getValue('autoRefreshPresets', DEFAULT_PRESETS),
  259. position: GM_getValue('autoRefreshPosition', { x: 20, y: 20 }),
  260. refreshTimer: null,
  261. lastRefreshTime: null,
  262. editingPreset: null,
  263. onServiceOrdersPage: false,
  264. initialized: false,
  265. observerDebounce: false,
  266. refreshButtonObserved: false,
  267. isDragging: false,
  268. tipExpanded: false,
  269. pendingRefresh: false, // Flag to prevent duplicate refreshes
  270.  
  271. // Feature flags
  272. useTimeBasedPresets: GM_getValue('autoRefreshUseTimeBasedPresets', true),
  273. jitterEnabled: true, // Always enabled but hidden from user
  274.  
  275. // Time-based preset tracking
  276. currentTimePreset: null,
  277. timeCheckInterval: null,
  278. lastPresetChangeTime: null // Track when preset was last changed
  279. };
  280.  
  281. // Check if we're on the service orders page
  282. function checkIfOnServiceOrdersPage() {
  283. try {
  284. // Check if URL matches the service orders page pattern
  285. const isServiceOrdersURL = window.location.href.includes('/service-optimization/operations/service-orders');
  286. const refreshButton = findRefreshButton();
  287.  
  288. // Only consider it a service orders page if both conditions are met
  289. state.onServiceOrdersPage = isServiceOrdersURL && !!refreshButton;
  290.  
  291. // Update UI visibility based on page type
  292. const container = document.getElementById('auto-refresh-container');
  293. if (container) {
  294. container.style.display = state.onServiceOrdersPage ? 'block' : 'none';
  295. }
  296.  
  297. if (state.onServiceOrdersPage && !state.refreshButtonObserved && refreshButton) {
  298. observeRefreshButton(refreshButton);
  299. }
  300.  
  301. return state.onServiceOrdersPage;
  302. } catch (error) {
  303. console.error('Error checking page type:', error);
  304. return false;
  305. }
  306. }
  307.  
  308. // Observe the refresh button for manual clicks
  309. function observeRefreshButton(button) {
  310. if (!button || state.refreshButtonObserved) return;
  311.  
  312. button.addEventListener('click', function() {
  313. if (state.enabled) {
  314. // Update last refresh time
  315. state.lastRefreshTime = new Date();
  316.  
  317. // Clear any pending refresh
  318. clearAutoRefresh();
  319.  
  320. // Don't immediately refresh again, just schedule next refresh
  321. scheduleNextRefresh(state.activePreset);
  322.  
  323. // Update status with improved format
  324. updateStatusWithRefreshInfo();
  325. }
  326. });
  327.  
  328. state.refreshButtonObserved = true;
  329. }
  330.  
  331. // Find refresh button - optimized selector search
  332. function findRefreshButton() {
  333. try {
  334. // Try specific selectors first for better performance
  335. const selectors = [
  336. 'button[soe-data-cy="refresh"]',
  337. 'button[mat-icon-button] soe-icon[icon="refresh-dot"]',
  338. 'button[mat-icon-button] soe-icon[icon="refresh"]',
  339. 'button[aria-label="refresh"]'
  340. ];
  341.  
  342. for (const selector of selectors) {
  343. const button = document.querySelector(selector);
  344. if (button) return button;
  345. }
  346.  
  347. // Fallback to broader search
  348. const buttons = document.querySelectorAll('button');
  349. for (const button of buttons) {
  350. if (button.innerHTML.toLowerCase().includes('refresh') ||
  351. button.innerHTML.toLowerCase().includes('refresh-dot')) {
  352. return button;
  353. }
  354. }
  355. return null;
  356. } catch (error) {
  357. console.error('Error finding refresh button:', error);
  358. return null;
  359. }
  360. }
  361.  
  362. // Format time as HH:MM:SS
  363. function formatTime(date) {
  364. if (!date) return 'Never';
  365. return date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'});
  366. }
  367.  
  368. // Update status message with improved format
  369. function updateStatusWithRefreshInfo() {
  370. if (!state.enabled) {
  371. updateStatus('info', 'Auto refresh is off');
  372. return;
  373. }
  374.  
  375. if (!state.activePreset) {
  376. updateStatus('info', 'Select a refresh interval');
  377. return;
  378. }
  379.  
  380. const lastTime = state.lastRefreshTime ? `${formatTime(state.lastRefreshTime)}` : '';
  381. updateStatus('success', `Last checked : ${lastTime}`);
  382. }
  383.  
  384. // Update status message
  385. function updateStatus(type, message) {
  386. const statusEl = document.getElementById('auto-refresh-status');
  387. if (statusEl) {
  388. statusEl.className = type || '';
  389. statusEl.textContent = message || '';
  390. }
  391. }
  392.  
  393. // Get jittered interval based on base seconds
  394. function getJitteredInterval(baseSeconds) {
  395. // Calculate jitter based on the base time
  396. if (baseSeconds <= 15) {
  397. // For minimum time (15s), only add positive jitter (up to +25%)
  398. return baseSeconds + (baseSeconds * 0.25 * Math.random());
  399. } else if (baseSeconds >= 540) { // 9 minutes in seconds
  400. // For maximum time (9min), only subtract jitter (up to -25%)
  401. return baseSeconds - (baseSeconds * 0.25 * Math.random());
  402. } else {
  403. // For all other values, add random jitter between -25% and +25%
  404. return baseSeconds * (1 + (Math.random() * 0.5 - 0.25));
  405. }
  406. }
  407.  
  408. // Get the appropriate time-based interval in seconds
  409. function getTimeBasedInterval() {
  410. const now = new Date();
  411. const currentHour = now.getHours();
  412. const currentMinute = now.getMinutes();
  413.  
  414. // Convert current time to decimal hours for easier comparison
  415. const currentTimeDecimal = currentHour + (currentMinute / 60);
  416.  
  417. // Find the matching time range
  418. for (const timeSlot of TIME_SCHEDULE) {
  419. const [startHour, startMinute] = timeSlot.start;
  420. const [endHour, endMinute] = timeSlot.end;
  421.  
  422. // Convert to decimal hours
  423. let startTimeDecimal = startHour + (startMinute / 60);
  424. let endTimeDecimal = endHour + (endMinute / 60);
  425.  
  426. // Handle overnight ranges (e.g., 20:00 - 1:00)
  427. if (endTimeDecimal < startTimeDecimal) {
  428. if (currentTimeDecimal >= startTimeDecimal || currentTimeDecimal < endTimeDecimal) {
  429. return timeSlot;
  430. }
  431. } else {
  432. if (currentTimeDecimal >= startTimeDecimal && currentTimeDecimal < endTimeDecimal) {
  433. return timeSlot;
  434. }
  435. }
  436. }
  437.  
  438. // Default to 30s if no match found (shouldn't happen with a complete schedule)
  439. return { seconds: 30, name: '30s' };
  440. }
  441.  
  442. // Get time-based preset
  443. function getTimeBasedPreset() {
  444. const timeSlot = getTimeBasedInterval();
  445.  
  446. // Look for a matching preset first
  447. const matchingPreset = state.presets.find(p => p.seconds === timeSlot.seconds);
  448.  
  449. if (matchingPreset) {
  450. return matchingPreset;
  451. } else {
  452. // Return a virtual preset if no matching preset exists
  453. return {
  454. name: timeSlot.name,
  455. seconds: timeSlot.seconds,
  456. isVirtual: true // Mark as virtual preset
  457. };
  458. }
  459. }
  460.  
  461. // Start time check interval
  462. function startTimeCheck() {
  463. if (state.timeCheckInterval) {
  464. clearInterval(state.timeCheckInterval);
  465. }
  466.  
  467. // Immediately update current time preset
  468. updateCurrentTimePreset();
  469.  
  470. // Check periodically for time-based interval changes
  471. state.timeCheckInterval = setInterval(() => {
  472. // Only check and apply if time-based presets are enabled
  473. if (state.useTimeBasedPresets) {
  474. const previousTimePreset = state.currentTimePreset;
  475. updateCurrentTimePreset();
  476.  
  477. // Apply the new preset if we've changed time slots
  478. if (previousTimePreset && state.currentTimePreset &&
  479. previousTimePreset.seconds !== state.currentTimePreset.seconds) {
  480. applyCurrentTimePreset();
  481. }
  482. }
  483. }, 300000); // Check every 5 minutes
  484. }
  485.  
  486. // Update the current time-based preset
  487. function updateCurrentTimePreset() {
  488. state.currentTimePreset = getTimeBasedPreset();
  489. }
  490.  
  491. // Apply the current time-based preset
  492. function applyCurrentTimePreset() {
  493. if (!state.currentTimePreset || !state.onServiceOrdersPage) {
  494. return;
  495. }
  496.  
  497. // Record when preset was changed
  498. state.lastPresetChangeTime = new Date();
  499.  
  500. // Completely clear any existing refresh
  501. clearAutoRefresh();
  502.  
  503. // Set the active preset to the current time preset
  504. state.activePreset = state.currentTimePreset;
  505.  
  506. // Start auto refresh with this preset if enabled
  507. if (state.enabled) {
  508. // Don't trigger immediate refresh if we recently refreshed
  509. const shouldTriggerNow = !state.lastRefreshTime ||
  510. (new Date() - state.lastRefreshTime > 10000); // 10 seconds threshold
  511.  
  512. startAutoRefresh(state.activePreset, !shouldTriggerNow);
  513. } else {
  514. // Just update the UI to highlight the correct preset
  515. updateActivePreset();
  516. }
  517. }
  518.  
  519. // Trigger refresh click
  520. function triggerRefresh() {
  521. // Prevent double refresh by checking the pendingRefresh flag
  522. if (state.pendingRefresh) {
  523. return false;
  524. }
  525.  
  526. state.pendingRefresh = true;
  527.  
  528. const refreshButton = findRefreshButton();
  529. if (refreshButton) {
  530. refreshButton.click();
  531. state.lastRefreshTime = new Date();
  532.  
  533. // Update status with improved format
  534. updateStatusWithRefreshInfo();
  535.  
  536. // Reset pending flag after a short delay
  537. setTimeout(() => {
  538. state.pendingRefresh = false;
  539. }, 1000);
  540.  
  541. return true;
  542. } else {
  543. updateStatus('warning', 'Refresh button not found');
  544. state.pendingRefresh = false;
  545. return false;
  546. }
  547. }
  548.  
  549. // Schedule next refresh - separated from startAutoRefresh for cleaner code
  550. function scheduleNextRefresh(preset) {
  551. if (!preset || !state.enabled || !state.onServiceOrdersPage) {
  552. return;
  553. }
  554.  
  555. // Clear any existing timer first
  556. clearAutoRefresh();
  557.  
  558. // Calculate jittered interval
  559. const jitteredSeconds = getJitteredInterval(preset.seconds);
  560. const intervalMs = Math.round(jitteredSeconds * 1000);
  561.  
  562. // Set timer for next refresh
  563. state.refreshTimer = setTimeout(() => {
  564. if (state.enabled && state.onServiceOrdersPage) {
  565. triggerRefresh();
  566. // Schedule the next refresh
  567. scheduleNextRefresh(preset);
  568. }
  569. }, intervalMs);
  570. }
  571.  
  572. // Start auto refresh with given preset
  573. function startAutoRefresh(preset, skipInitialRefresh = false) {
  574. // Don't start if disabled
  575. if (!state.enabled) return;
  576.  
  577. // Clear existing timer first to prevent multiple refreshes
  578. clearAutoRefresh();
  579.  
  580. // Set active preset
  581. state.activePreset = preset;
  582.  
  583. // Update UI to reflect the current active preset
  584. updateActivePreset();
  585.  
  586. // Only start timer if enabled and on service orders page
  587. if (state.enabled && state.onServiceOrdersPage) {
  588. // Trigger a refresh immediately when setting a new preset, unless skipInitialRefresh is true
  589. if (!skipInitialRefresh) {
  590. triggerRefresh();
  591. }
  592.  
  593. // Schedule the next refresh
  594. scheduleNextRefresh(preset);
  595. }
  596.  
  597. // Save state
  598. saveState();
  599. }
  600.  
  601. // Clear auto refresh timer
  602. function clearAutoRefresh() {
  603. if (state.refreshTimer) {
  604. clearTimeout(state.refreshTimer);
  605. state.refreshTimer = null;
  606. }
  607. }
  608.  
  609. // Update active preset highlighting
  610. function updateActivePreset() {
  611. // Remove active class from all presets
  612. document.querySelectorAll('.auto-refresh-preset').forEach(btn => {
  613. btn.classList.remove('active');
  614. });
  615.  
  616. // Add active class to current preset if it's one of the displayed presets
  617. if (state.activePreset) {
  618. const activeBtn = document.querySelector(`.auto-refresh-preset[data-seconds="${state.activePreset.seconds}"]`);
  619. if (activeBtn) {
  620. activeBtn.classList.add('active');
  621. }
  622. }
  623. }
  624.  
  625. // Toggle auto refresh enabled state
  626. function toggleEnabled() {
  627. state.enabled = !state.enabled;
  628.  
  629. // Update UI
  630. const container = document.getElementById('auto-refresh-container');
  631. if (container) {
  632. if (state.enabled) {
  633. container.classList.remove('auto-refresh-disabled');
  634. if (state.activePreset && state.onServiceOrdersPage) {
  635. startAutoRefresh(state.activePreset);
  636. }
  637. } else {
  638. container.classList.add('auto-refresh-disabled');
  639. clearAutoRefresh();
  640. // When disabled, immediately update status to "off" message
  641. updateStatus('info', 'Auto refresh is off');
  642. }
  643. }
  644.  
  645. // Update page status
  646. updatePageStatus();
  647.  
  648. // Save state
  649. saveState();
  650. }
  651.  
  652. // Toggle time-based presets feature
  653. function toggleTimeBasedPresets() {
  654. state.useTimeBasedPresets = !state.useTimeBasedPresets;
  655.  
  656. // Update UI
  657. updateTimeBasedToggle();
  658.  
  659. // Always trigger refresh immediately when toggle is changed
  660. // This will update the "Last checked" time
  661. if (state.onServiceOrdersPage && state.enabled) {
  662. triggerRefresh();
  663. }
  664.  
  665. if (state.useTimeBasedPresets) {
  666. // When turning ON time-based presets, switch to time-based preset immediately
  667. updateCurrentTimePreset();
  668.  
  669. // Remember current user preset before switching
  670. if (state.activePreset) {
  671. state.userSelectedPreset = {...state.activePreset};
  672. }
  673.  
  674. // Apply the time-based preset (without triggering another refresh)
  675. clearAutoRefresh();
  676. state.activePreset = state.currentTimePreset;
  677.  
  678. // Start auto refresh with this preset if enabled
  679. if (state.enabled && state.onServiceOrdersPage) {
  680. scheduleNextRefresh(state.activePreset);
  681. }
  682.  
  683. // Make sure the time-based preset button is visually active
  684. document.querySelectorAll('.auto-refresh-preset').forEach(btn => {
  685. btn.classList.remove('active');
  686.  
  687. // Add active class to the current time preset
  688. if (state.activePreset && btn.dataset.seconds == state.activePreset.seconds) {
  689. btn.classList.add('active');
  690. }
  691. });
  692. } else {
  693. // When turning OFF time-based presets, switch back to user's manually selected preset
  694. if (state.userSelectedPreset) {
  695. clearAutoRefresh();
  696. state.activePreset = state.userSelectedPreset;
  697.  
  698. // If enabled, schedule next refresh with the user's preset
  699. if (state.enabled && state.onServiceOrdersPage) {
  700. scheduleNextRefresh(state.activePreset);
  701. } else {
  702. // Just update UI if not enabled
  703. updateActivePreset();
  704. }
  705. }
  706. }
  707.  
  708. // Update status after changes
  709. updateStatusWithRefreshInfo();
  710.  
  711. // Save state
  712. saveState();
  713. }
  714.  
  715. // Update time-based toggle state in UI
  716. function updateTimeBasedToggle() {
  717. const timeToggle = document.getElementById('time-based-toggle');
  718. if (timeToggle) {
  719. timeToggle.checked = state.useTimeBasedPresets;
  720. }
  721. }
  722.  
  723. // Toggle tip expanded/collapsed state
  724. function toggleTip() {
  725. state.tipExpanded = !state.tipExpanded;
  726.  
  727. const tipContent = document.getElementById('auto-refresh-tip-content');
  728. if (!tipContent) return;
  729.  
  730. if (state.tipExpanded) {
  731. tipContent.classList.add('expanded');
  732. } else {
  733. tipContent.classList.remove('expanded');
  734. }
  735.  
  736. // Save state
  737. saveState();
  738. }
  739.  
  740. // Toggle collapsed state
  741. function toggleCollapsed() {
  742. state.collapsed = !state.collapsed;
  743.  
  744. // Update UI
  745. const body = document.getElementById('auto-refresh-body');
  746. const collapseBtn = document.getElementById('auto-refresh-collapse');
  747.  
  748. if (body && collapseBtn) {
  749. if (state.collapsed) {
  750. body.style.maxHeight = '0';
  751. collapseBtn.textContent = '+';
  752. } else {
  753. body.style.maxHeight = '500px';
  754. collapseBtn.textContent = '-';
  755. }
  756. }
  757.  
  758. // Save state
  759. saveState();
  760. }
  761.  
  762. // Save state to GM storage
  763. function saveState() {
  764. try {
  765. GM_setValue('autoRefreshPresets', state.presets);
  766. GM_setValue('autoRefreshPosition', state.position);
  767. GM_setValue('autoRefreshEnabled', state.enabled);
  768. GM_setValue('autoRefreshCollapsed', state.collapsed);
  769. GM_setValue('autoRefreshActivePreset', state.activePreset);
  770. GM_setValue('autoRefreshUseTimeBasedPresets', state.useTimeBasedPresets);
  771. GM_setValue('autoRefreshTipExpanded', state.tipExpanded);
  772. } catch (error) {
  773. console.error('Error saving state:', error);
  774. }
  775. }
  776.  
  777. // Load state from GM storage
  778. function loadState() {
  779. try {
  780. state.presets = GM_getValue('autoRefreshPresets', DEFAULT_PRESETS);
  781. state.position = GM_getValue('autoRefreshPosition', { x: 20, y: 20 });
  782. state.enabled = GM_getValue('autoRefreshEnabled', true);
  783. state.collapsed = GM_getValue('autoRefreshCollapsed', false);
  784. state.tipExpanded = GM_getValue('autoRefreshTipExpanded', false);
  785.  
  786. // Always default to time-based presets on page reload/relogin
  787. state.useTimeBasedPresets = true;
  788.  
  789. // Store this value to preserve user's manual toggle for time-based presets
  790. // during the current session, but not across reloads
  791. const savedTimeBasedSetting = GM_getValue('autoRefreshUseTimeBasedPresets', true);
  792. GM_setValue('autoRefreshUseTimeBasedPresets', true);
  793.  
  794. // Get the current time-based preset
  795. updateCurrentTimePreset();
  796.  
  797. // Always use time-based preset on reload, regardless of previous setting
  798. state.activePreset = state.currentTimePreset;
  799.  
  800. // If we've loaded an active preset and we're enabled, set the lastRefreshTime
  801. // to avoid the "Select a refresh interval" message flash on load
  802. if (state.activePreset && state.enabled) {
  803. state.lastRefreshTime = new Date();
  804. }
  805. } catch (error) {
  806. console.error('Error loading state:', error);
  807. // Fallback to defaults
  808. state.presets = DEFAULT_PRESETS;
  809. state.position = { x: 20, y: 20 };
  810. state.enabled = true;
  811. state.collapsed = false;
  812. state.useTimeBasedPresets = true;
  813. state.tipExpanded = false;
  814. updateCurrentTimePreset();
  815. state.activePreset = state.currentTimePreset;
  816.  
  817. // Prevent "Select a refresh interval" message
  818. state.lastRefreshTime = new Date();
  819. }
  820. }
  821.  
  822. // Format preset name based on seconds
  823. function formatPresetName(seconds) {
  824. return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}min`;
  825. }
  826.  
  827. // Show preset edit panel
  828. function showEditPanel(preset) {
  829. state.editingPreset = preset;
  830.  
  831. // Create overlay and panel
  832. const overlay = document.createElement('div');
  833. overlay.id = 'auto-refresh-edit-overlay';
  834.  
  835. const panel = document.createElement('div');
  836. panel.id = 'auto-refresh-edit-panel';
  837.  
  838. // Create options HTML - with predefined array
  839. const optionsHTML = TIME_OPTIONS.map(value => {
  840. const selected = value === preset.seconds ? 'selected' : '';
  841. const label = value < 60 ? `${value}s` : `${Math.floor(value/60)}min`;
  842. return `<option value="${value}" ${selected}>${label}</option>`;
  843. }).join('');
  844.  
  845. panel.innerHTML = `
  846. <h3 style="font-size: 14px; margin-top: 0;">Edit Preset</h3>
  847. <div class="auto-refresh-form-group">
  848. <label class="auto-refresh-form-label">Select Interval:</label>
  849. <select id="edit-preset-seconds" class="auto-refresh-form-input" style="appearance: auto; background-color: white;">
  850. ${optionsHTML}
  851. </select>
  852. <div id="edit-preset-error" style="color: #f44336; font-size: 11px; margin: 8px 0; display: none;"></div>
  853. </div>
  854. <div class="auto-refresh-form-buttons">
  855. <button class="auto-refresh-form-button cancel">Cancel</button>
  856. <button class="auto-refresh-form-button save">Save</button>
  857. </div>
  858. `;
  859.  
  860. overlay.appendChild(panel);
  861. document.body.appendChild(overlay);
  862.  
  863. // Add event listeners
  864. overlay.querySelector('.cancel').addEventListener('click', hideEditPanel);
  865. overlay.querySelector('.save').addEventListener('click', saveEditedPreset);
  866.  
  867. // Handle pressing Enter key
  868. const selectInput = document.getElementById('edit-preset-seconds');
  869. selectInput.addEventListener('keydown', (e) => {
  870. if (e.key === 'Enter') {
  871. e.preventDefault();
  872. saveEditedPreset();
  873. }
  874. });
  875.  
  876. // Focus the select field
  877. selectInput.focus();
  878. }
  879.  
  880. // Save edited preset with duplicate check
  881. function saveEditedPreset() {
  882. const secondsInput = document.getElementById('edit-preset-seconds');
  883. const errorElement = document.getElementById('edit-preset-error');
  884.  
  885. if (!secondsInput) return;
  886.  
  887. const seconds = parseInt(secondsInput.value, 10);
  888. if (isNaN(seconds)) return;
  889.  
  890. // Check for duplicate
  891. const duplicatePreset = state.presets.find(p =>
  892. p.seconds === seconds &&
  893. !(p.name === state.editingPreset.name && p.seconds === state.editingPreset.seconds)
  894. );
  895.  
  896. if (duplicatePreset) {
  897. if (errorElement) {
  898. errorElement.textContent = `Preset "${duplicatePreset.name}" already uses this interval.`;
  899. errorElement.style.display = 'block';
  900. }
  901. return;
  902. }
  903.  
  904. // Format name and update preset
  905. const name = formatPresetName(seconds);
  906. const presetIndex = state.presets.findIndex(p =>
  907. p.name === state.editingPreset.name && p.seconds === state.editingPreset.seconds
  908. );
  909.  
  910. if (presetIndex >= 0) {
  911. state.presets[presetIndex] = { name, seconds };
  912.  
  913. // If editing active preset, update it
  914. if (state.activePreset &&
  915. state.activePreset.name === state.editingPreset.name &&
  916. state.activePreset.seconds === state.editingPreset.seconds) {
  917. state.activePreset = { name, seconds };
  918. if (state.enabled && state.onServiceOrdersPage) {
  919. startAutoRefresh(state.activePreset);
  920. }
  921. }
  922.  
  923. // Update UI and save
  924. createOrUpdateUI();
  925. saveState();
  926. }
  927.  
  928. hideEditPanel();
  929. }
  930.  
  931. // Hide preset edit panel
  932. function hideEditPanel() {
  933. const overlay = document.getElementById('auto-refresh-edit-overlay');
  934. if (overlay) overlay.remove();
  935. state.editingPreset = null;
  936. }
  937.  
  938. // Make element draggable - simplified for both mouse and touch
  939. function makeDraggable(element, handleElement) {
  940. let startX, startY, initialX, initialY;
  941. let isDragging = false;
  942.  
  943. const onStart = (e) => {
  944. // Don't initiate drag if it's a button or control
  945. if (e.target.id === 'auto-refresh-collapse' ||
  946. e.target.id === 'auto-refresh-toggle' ||
  947. e.target.id === 'time-based-toggle' ||
  948. e.target.closest('button') ||
  949. e.target.closest('.switch')) {
  950. return;
  951. }
  952.  
  953. isDragging = true;
  954. state.isDragging = true;
  955.  
  956. // Get starting positions
  957. if (e.type === 'mousedown') {
  958. startX = e.clientX;
  959. startY = e.clientY;
  960. } else if (e.type === 'touchstart') {
  961. startX = e.touches[0].clientX;
  962. startY = e.touches[0].clientY;
  963. }
  964.  
  965. initialX = element.offsetLeft;
  966. initialY = element.offsetTop;
  967.  
  968. // Add event listeners
  969. if (e.type === 'mousedown') {
  970. document.addEventListener('mousemove', onMove);
  971. document.addEventListener('mouseup', onEnd);
  972. } else if (e.type === 'touchstart') {
  973. document.addEventListener('touchmove', onMove, { passive: false });
  974. document.addEventListener('touchend', onEnd);
  975. }
  976.  
  977. // Prevent default for handle
  978. if (e.target === handleElement || handleElement.contains(e.target)) {
  979. if (e.preventDefault) e.preventDefault();
  980. }
  981. };
  982.  
  983. const onMove = (e) => {
  984. if (!isDragging) return;
  985.  
  986. // Calculate new position
  987. let clientX, clientY;
  988. if (e.type === 'mousemove') {
  989. clientX = e.clientX;
  990. clientY = e.clientY;
  991. } else if (e.type === 'touchmove') {
  992. clientX = e.touches[0].clientX;
  993. clientY = e.touches[0].clientY;
  994. e.preventDefault(); // Prevent scrolling when dragging
  995. }
  996.  
  997. const deltaX = clientX - startX;
  998. const deltaY = clientY - startY;
  999. const newLeft = initialX + deltaX;
  1000. const newTop = initialY + deltaY;
  1001.  
  1002. // Keep within viewport
  1003. const maxTop = window.innerHeight - element.offsetHeight;
  1004. const maxLeft = window.innerWidth - element.offsetWidth;
  1005. element.style.top = `${Math.min(Math.max(0, newTop), maxTop)}px`;
  1006. element.style.left = `${Math.min(Math.max(0, newLeft), maxLeft)}px`;
  1007. };
  1008.  
  1009. const onEnd = () => {
  1010. isDragging = false;
  1011.  
  1012. // Small delay to prevent accidental clicks
  1013. setTimeout(() => { state.isDragging = false; }, 50);
  1014.  
  1015. // Remove event listeners
  1016. document.removeEventListener('mousemove', onMove);
  1017. document.removeEventListener('mouseup', onEnd);
  1018. document.removeEventListener('touchmove', onMove);
  1019. document.removeEventListener('touchend', onEnd);
  1020.  
  1021. // Save position
  1022. state.position = {
  1023. x: parseInt(element.style.left, 10) || 20,
  1024. y: parseInt(element.style.top, 10) || 20
  1025. };
  1026. saveState();
  1027. };
  1028.  
  1029. handleElement.addEventListener('mousedown', onStart);
  1030. handleElement.addEventListener('touchstart', onStart, { passive: true });
  1031. }
  1032.  
  1033. // Update page status based on current state
  1034. function updatePageStatus() {
  1035. if (!state.onServiceOrdersPage) {
  1036. updateStatus('warning', 'Please navigate to Service Orders');
  1037. } else if (!state.enabled) {
  1038. // When disabled, always show "off" message regardless of last refresh time
  1039. updateStatus('info', 'Auto refresh is off');
  1040. } else if (state.activePreset) {
  1041. // Show improved status message with interval and last time
  1042. updateStatusWithRefreshInfo();
  1043. } else {
  1044. updateStatus('info', 'Select a refresh interval');
  1045. }
  1046. }
  1047.  
  1048. // Create or update UI
  1049. function createOrUpdateUI() {
  1050. // Check if UI already exists
  1051. let container = document.getElementById('auto-refresh-container');
  1052.  
  1053. if (!container) {
  1054. // Create new container
  1055. container = document.createElement('div');
  1056. container.id = 'auto-refresh-container';
  1057. document.body.appendChild(container);
  1058.  
  1059. // Set position
  1060. container.style.left = `${state.position.x}px`;
  1061. container.style.top = `${state.position.y}px`;
  1062.  
  1063. // Create UI structure
  1064. container.innerHTML = `
  1065. <div id="auto-refresh-header">
  1066. <span id="auto-refresh-title">Auto Refresh</span>
  1067. <div id="auto-refresh-controls">
  1068. <label class="switch">
  1069. <input type="checkbox" id="auto-refresh-toggle" ${state.enabled ? 'checked' : ''}>
  1070. <span class="slider"></span>
  1071. </label>
  1072. <span id="auto-refresh-collapse">${state.collapsed ? '+' : '-'}</span>
  1073. </div>
  1074. </div>
  1075. <div id="auto-refresh-body" style="max-height: ${state.collapsed ? '0' : '500px'};">
  1076. <div class="feature-toggle">
  1077. <span class="feature-label">Use time-based presets</span>
  1078. <label class="switch">
  1079. <input type="checkbox" id="time-based-toggle" ${state.useTimeBasedPresets ? 'checked' : ''}>
  1080. <span class="slider"></span>
  1081. </label>
  1082. </div>
  1083. <div id="auto-refresh-tip">
  1084. <div id="auto-refresh-tip-header">
  1085. <span>User Tip</span>
  1086. </div>
  1087. <div id="auto-refresh-tip-content" class="${state.tipExpanded ? 'expanded' : ''}">
  1088. Adjust refresh for busy periods.
  1089. </div>
  1090. </div>
  1091. <div id="auto-refresh-presets"></div>
  1092. <div id="auto-refresh-status" class="info">Initializing...</div>
  1093. </div>
  1094. `;
  1095.  
  1096. // Add toggle event listeners
  1097. document.getElementById('auto-refresh-toggle').addEventListener('change', toggleEnabled);
  1098. document.getElementById('time-based-toggle').addEventListener('change', toggleTimeBasedPresets);
  1099.  
  1100. // Add collapse event listener
  1101. const domCollapseBtn = document.getElementById('auto-refresh-collapse');
  1102. if (domCollapseBtn) {
  1103. domCollapseBtn.onclick = function(e) {
  1104. if (e) {
  1105. e.stopPropagation();
  1106. e.preventDefault();
  1107. }
  1108. toggleCollapsed();
  1109. return false;
  1110. };
  1111. }
  1112.  
  1113. // Add tip toggle functionality
  1114. const tipHeader = document.getElementById('auto-refresh-tip-header');
  1115. if (tipHeader) {
  1116. tipHeader.addEventListener('click', function(e) {
  1117. toggleTip();
  1118. e.stopPropagation();
  1119. });
  1120. }
  1121.  
  1122. // Make draggable
  1123. makeDraggable(container, document.getElementById('auto-refresh-header'));
  1124.  
  1125. // Set disabled class if needed
  1126. if (!state.enabled) {
  1127. container.classList.add('auto-refresh-disabled');
  1128. }
  1129. }
  1130.  
  1131. // Update presets
  1132. const presetsContainer = document.getElementById('auto-refresh-presets');
  1133. if (presetsContainer) {
  1134. presetsContainer.innerHTML = '';
  1135.  
  1136. state.presets.forEach(preset => {
  1137. const presetBtn = document.createElement('button');
  1138. presetBtn.className = 'auto-refresh-preset';
  1139. presetBtn.textContent = preset.name;
  1140. presetBtn.dataset.seconds = preset.seconds;
  1141.  
  1142. // Set active class if needed
  1143. if (state.activePreset &&
  1144. state.activePreset.seconds === preset.seconds) {
  1145. presetBtn.classList.add('active');
  1146. }
  1147.  
  1148. // Normal click event
  1149. presetBtn.addEventListener('click', () => {
  1150. if (!state.isDragging && state.enabled) {
  1151. // When user selects a preset manually, disable time-based presets
  1152. state.useTimeBasedPresets = false;
  1153. updateTimeBasedToggle();
  1154.  
  1155. // Save this as the user's manual preset
  1156. state.userSelectedPreset = {...preset};
  1157.  
  1158. // Set this preset as active
  1159. startAutoRefresh(preset);
  1160.  
  1161. // Ensure this preset gets highlighted (not just in startAutoRefresh)
  1162. document.querySelectorAll('.auto-refresh-preset').forEach(btn => {
  1163. btn.classList.remove('active');
  1164. });
  1165. presetBtn.classList.add('active');
  1166. }
  1167. });
  1168.  
  1169. // Right-click for edit
  1170. presetBtn.addEventListener('contextmenu', (e) => {
  1171. e.preventDefault();
  1172. showEditPanel(preset);
  1173. });
  1174.  
  1175. // Long press for mobile
  1176. let longPressTimer;
  1177. let longPressStarted = false;
  1178. let longPressFired = false; // New flag to track when long press action has fired
  1179.  
  1180. presetBtn.addEventListener('touchstart', () => {
  1181. longPressStarted = true;
  1182. longPressFired = false;
  1183. longPressTimer = setTimeout(() => {
  1184. if (longPressStarted) {
  1185. longPressFired = true; // Set flag when edit panel is shown
  1186. showEditPanel(preset);
  1187. }
  1188. }, 800);
  1189. });
  1190.  
  1191. presetBtn.addEventListener('touchmove', () => {
  1192. longPressStarted = false;
  1193. clearTimeout(longPressTimer);
  1194. });
  1195.  
  1196. presetBtn.addEventListener('touchend', () => {
  1197. // Only activate preset if it wasn't a long press
  1198. if (!state.isDragging && longPressStarted && !longPressFired && state.enabled) {
  1199. // Normal tap behavior - activate the preset
  1200. state.useTimeBasedPresets = false;
  1201. updateTimeBasedToggle();
  1202.  
  1203. // Save this as the user's manual preset
  1204. state.userSelectedPreset = {...preset};
  1205.  
  1206. startAutoRefresh(preset);
  1207.  
  1208. // Ensure this preset gets highlighted
  1209. document.querySelectorAll('.auto-refresh-preset').forEach(btn => {
  1210. btn.classList.remove('active');
  1211. });
  1212. presetBtn.classList.add('active');
  1213. }
  1214. longPressStarted = false;
  1215. clearTimeout(longPressTimer);
  1216. });
  1217.  
  1218. presetsContainer.appendChild(presetBtn);
  1219. });
  1220. }
  1221.  
  1222. // Update status
  1223. updatePageStatus();
  1224. }
  1225.  
  1226. // Check page and update UI accordingly
  1227. function checkPageAndUpdateUI() {
  1228. const wasOnServiceOrdersPage = state.onServiceOrdersPage;
  1229. state.onServiceOrdersPage = checkIfOnServiceOrdersPage();
  1230.  
  1231. // Reset button observed state if needed
  1232. if (!state.onServiceOrdersPage) {
  1233. state.refreshButtonObserved = false;
  1234. }
  1235.  
  1236. // Update status
  1237. updatePageStatus();
  1238.  
  1239. // Handle page transitions
  1240. if (!wasOnServiceOrdersPage && state.onServiceOrdersPage) {
  1241. // Just arrived at service orders page
  1242. if (state.enabled) {
  1243. triggerRefresh();
  1244. if (state.activePreset) {
  1245. startAutoRefresh(state.activePreset);
  1246. }
  1247. }
  1248. } else if (wasOnServiceOrdersPage && !state.onServiceOrdersPage) {
  1249. // Just left service orders page
  1250. clearAutoRefresh();
  1251. }
  1252. }
  1253.  
  1254. // Setup observer for page changes
  1255. function setupObserver() {
  1256. const observer = new MutationObserver(() => {
  1257. // Debounce to prevent excessive checks
  1258. if (!state.observerDebounce) {
  1259. state.observerDebounce = true;
  1260. setTimeout(() => {
  1261. checkPageAndUpdateUI();
  1262. state.observerDebounce = false;
  1263. }, 1000);
  1264. }
  1265. });
  1266.  
  1267. // Observe body changes
  1268. observer.observe(document.body, { childList: true, subtree: true });
  1269. }
  1270.  
  1271. // Initialize the script
  1272. function init() {
  1273. // Load saved state
  1274. loadState();
  1275.  
  1276. // Create UI
  1277. createOrUpdateUI();
  1278.  
  1279. // Make sure the correct toggle state is shown
  1280. updateTimeBasedToggle();
  1281.  
  1282. // Setup observer
  1283. setupObserver();
  1284.  
  1285. // Start time check for time-based presets
  1286. startTimeCheck();
  1287.  
  1288. // Check current page
  1289. checkPageAndUpdateUI();
  1290.  
  1291. // Start auto refresh if applicable
  1292. if (state.activePreset && state.onServiceOrdersPage && state.enabled) {
  1293. startAutoRefresh(state.activePreset);
  1294. }
  1295.  
  1296. // Ensure the correct preset is highlighted
  1297. updateActivePreset();
  1298.  
  1299. state.initialized = true;
  1300. }
  1301.  
  1302. // Handle global errors
  1303. window.addEventListener('error', function(event) {
  1304. if (event.filename && event.filename.includes('Auto Refresh Tool')) {
  1305. //console.log('Auto Refresh Tool error:', error);
  1306. event.preventDefault();
  1307. return true;
  1308. }
  1309. return false;
  1310. }, true);
  1311.  
  1312. // Initialize with retry mechanism
  1313. let initAttempts = 0;
  1314. const maxInitAttempts = 3;
  1315.  
  1316. function attemptInit() {
  1317. if (initAttempts >= maxInitAttempts) {
  1318. console.error('Failed to initialize Auto Refresh Tool after multiple attempts');
  1319. return;
  1320. }
  1321.  
  1322. if (!state.initialized) {
  1323. initAttempts++;
  1324. init();
  1325.  
  1326. // Schedule another attempt if needed
  1327. if (!state.initialized) {
  1328. setTimeout(attemptInit, 2000);
  1329. }
  1330. }
  1331. }
  1332.  
  1333. // Start initialization with delay
  1334. setTimeout(attemptInit, 1000);
  1335.  
  1336. // Backup initialization
  1337. setTimeout(() => {
  1338. if (!document.getElementById('auto-refresh-container')) {
  1339. createOrUpdateUI();
  1340. checkPageAndUpdateUI();
  1341. }
  1342. }, 5000);
  1343. })();