// ==UserScript==
// @name Auto Refresh Interface for Hotsauce
// @namespace http://tampermonkey.net/
// @version 1.0.5
// @description Auto refresh for HotSOS
// @author PC
// @match https://na4.m-tech.com/*
// @run-at document-idle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Simplified CSS
GM_addStyle(`
#auto-refresh-container {
position: fixed;
top: 20px;
left: 20px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 6px;
padding: 8px 10px;
z-index: 9999;
font-family: Arial, sans-serif;
min-width: 150px;
max-width: 200px;
user-select: none;
font-size: 12px;
}
#auto-refresh-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
cursor: move;
padding-bottom: 6px;
border-bottom: 1px solid #eee;
}
#auto-refresh-title {
font-weight: bold;
color: #444;
}
#auto-refresh-controls {
display: flex;
align-items: center;
gap: 8px;
}
#auto-refresh-collapse {
cursor: pointer;
font-size: 14px;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
#auto-refresh-body {
overflow: hidden;
transition: max-height 0.3s ease;
}
#auto-refresh-presets {
padding-bottom: 8px;
margin-bottom: 8px;
border-bottom: 1px solid #eee;
}
.auto-refresh-preset {
display: inline-block;
background-color: #e9e9e9;
border: none;
border-radius: 4px;
padding: 4px 8px;
margin: 3px;
cursor: pointer;
font-size: 11px;
}
.auto-refresh-preset:hover {
background-color: #d9d9d9;
}
.auto-refresh-preset.active {
background-color: #4a89dc;
color: white;
}
.auto-refresh-disabled .auto-refresh-preset {
opacity: 0.5;
cursor: default;
}
#auto-refresh-status {
margin-top: 6px;
font-size: 11px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-top: 2px;
}
#auto-refresh-status.warning { color: #f44336; }
#auto-refresh-status.info { color: #9e9e9e; }
#auto-refresh-status.success { color: #4caf50; }
#auto-refresh-edit-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
#auto-refresh-edit-panel {
background-color: white;
border-radius: 6px;
padding: 16px;
width: 250px;
}
.auto-refresh-form-label {
display: block;
margin-bottom: 4px;
font-weight: bold;
font-size: 12px;
}
.auto-refresh-form-input {
width: 100%;
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
}
.auto-refresh-form-buttons {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.auto-refresh-form-button {
padding: 6px 12px;
border: none;
border-radius: 4px;
margin-left: 8px;
cursor: pointer;
font-size: 12px;
}
.auto-refresh-form-button.cancel { background-color: #f5f5f5; }
.auto-refresh-form-button.save {
background-color: #4a89dc;
color: white;
}
.switch {
position: relative;
display: inline-block;
width: 32px;
height: 16px;
}
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #ccc;
transition: .3s;
border-radius: 16px;
}
.slider:before {
position: absolute;
content: "";
height: 12px;
width: 12px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4a89dc;
}
input:checked + .slider:before {
transform: translateX(16px);
}
/* Feature toggle style */
.feature-toggle {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.feature-label {
font-size: 11px;
color: #444;
}
/* User Tip styles */
#auto-refresh-tip {
font-size: 10px;
color: #666;
margin-top: 4px;
margin-bottom: 8px;
line-height: 1.2;
border-radius: 3px;
overflow: hidden;
}
#auto-refresh-tip-header {
display: flex;
background-color: #e0e0e0;
padding: 4px 6px;
font-weight: bold;
border-left: 2px solid #4a89dc;
cursor: pointer;
}
#auto-refresh-tip-content {
padding: 0;
background-color: #f0f0f0;
font-style: italic;
border-left: 2px solid #4a89dc;
max-height: 0;
transition: all 0.3s ease;
overflow: hidden;
opacity: 0;
}
#auto-refresh-tip-content.expanded {
padding: 6px;
max-height: 50px;
opacity: 1;
}
`);
// Default presets
const DEFAULT_PRESETS = [
{ name: '15s', seconds: 15 },
{ name: '30s', seconds: 30 },
{ name: '1min', seconds: 60 },
{ name: '5min', seconds: 300 }
];
// Time-based schedule with direct second values
const TIME_SCHEDULE = [
{ start: [8, 0], end: [11, 0], seconds: 15, name: '15s' },
{ start: [11, 0], end: [13, 0], seconds: 30, name: '30s' },
{ start: [13, 0], end: [17, 0], seconds: 60, name: '1min' },
{ start: [17, 0], end: [20, 0], seconds: 30, name: '30s' },
{ start: [20, 0], end: [1, 0], seconds: 60, name: '1min' },
{ start: [1, 0], end: [8, 0], seconds: 300, name: '5min' }
];
// Predefined time options for dropdown
const TIME_OPTIONS = [15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 120, 180, 240, 300, 360, 420, 480, 540];
// Simplified state management
let state = {
enabled: true,
collapsed: false,
activePreset: null,
userSelectedPreset: null, // Track user's manually selected preset
presets: GM_getValue('autoRefreshPresets', DEFAULT_PRESETS),
position: GM_getValue('autoRefreshPosition', { x: 20, y: 20 }),
refreshTimer: null,
lastRefreshTime: null,
editingPreset: null,
onServiceOrdersPage: false,
initialized: false,
observerDebounce: false,
refreshButtonObserved: false,
isDragging: false,
tipExpanded: false,
pendingRefresh: false, // Flag to prevent duplicate refreshes
// Feature flags
useTimeBasedPresets: GM_getValue('autoRefreshUseTimeBasedPresets', true),
jitterEnabled: true, // Always enabled but hidden from user
// Time-based preset tracking
currentTimePreset: null,
timeCheckInterval: null,
lastPresetChangeTime: null // Track when preset was last changed
};
// Check if we're on the service orders page
function checkIfOnServiceOrdersPage() {
try {
// Check if URL matches the service orders page pattern
const isServiceOrdersURL = window.location.href.includes('/service-optimization/operations/service-orders');
const refreshButton = findRefreshButton();
// Only consider it a service orders page if both conditions are met
state.onServiceOrdersPage = isServiceOrdersURL && !!refreshButton;
// Update UI visibility based on page type
const container = document.getElementById('auto-refresh-container');
if (container) {
container.style.display = state.onServiceOrdersPage ? 'block' : 'none';
}
if (state.onServiceOrdersPage && !state.refreshButtonObserved && refreshButton) {
observeRefreshButton(refreshButton);
}
return state.onServiceOrdersPage;
} catch (error) {
console.error('Error checking page type:', error);
return false;
}
}
// Observe the refresh button for manual clicks
function observeRefreshButton(button) {
if (!button || state.refreshButtonObserved) return;
button.addEventListener('click', function() {
if (state.enabled) {
// Update last refresh time
state.lastRefreshTime = new Date();
// Clear any pending refresh
clearAutoRefresh();
// Don't immediately refresh again, just schedule next refresh
scheduleNextRefresh(state.activePreset);
// Update status with improved format
updateStatusWithRefreshInfo();
}
});
state.refreshButtonObserved = true;
}
// Find refresh button - optimized selector search
function findRefreshButton() {
try {
// Try specific selectors first for better performance
const selectors = [
'button[soe-data-cy="refresh"]',
'button[mat-icon-button] soe-icon[icon="refresh-dot"]',
'button[mat-icon-button] soe-icon[icon="refresh"]',
'button[aria-label="refresh"]'
];
for (const selector of selectors) {
const button = document.querySelector(selector);
if (button) return button;
}
// Fallback to broader search
const buttons = document.querySelectorAll('button');
for (const button of buttons) {
if (button.innerHTML.toLowerCase().includes('refresh') ||
button.innerHTML.toLowerCase().includes('refresh-dot')) {
return button;
}
}
return null;
} catch (error) {
console.error('Error finding refresh button:', error);
return null;
}
}
// Format time as HH:MM:SS
function formatTime(date) {
if (!date) return 'Never';
return date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'});
}
// Update status message with improved format
function updateStatusWithRefreshInfo() {
if (!state.enabled) {
updateStatus('info', 'Auto refresh is off');
return;
}
if (!state.activePreset) {
updateStatus('info', 'Select a refresh interval');
return;
}
const lastTime = state.lastRefreshTime ? `${formatTime(state.lastRefreshTime)}` : '';
updateStatus('success', `Last checked : ${lastTime}`);
}
// Update status message
function updateStatus(type, message) {
const statusEl = document.getElementById('auto-refresh-status');
if (statusEl) {
statusEl.className = type || '';
statusEl.textContent = message || '';
}
}
// Get jittered interval based on base seconds
function getJitteredInterval(baseSeconds) {
// Calculate jitter based on the base time
if (baseSeconds <= 15) {
// For minimum time (15s), only add positive jitter (up to +25%)
return baseSeconds + (baseSeconds * 0.25 * Math.random());
} else if (baseSeconds >= 540) { // 9 minutes in seconds
// For maximum time (9min), only subtract jitter (up to -25%)
return baseSeconds - (baseSeconds * 0.25 * Math.random());
} else {
// For all other values, add random jitter between -25% and +25%
return baseSeconds * (1 + (Math.random() * 0.5 - 0.25));
}
}
// Get the appropriate time-based interval in seconds
function getTimeBasedInterval() {
const now = new Date();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
// Convert current time to decimal hours for easier comparison
const currentTimeDecimal = currentHour + (currentMinute / 60);
// Find the matching time range
for (const timeSlot of TIME_SCHEDULE) {
const [startHour, startMinute] = timeSlot.start;
const [endHour, endMinute] = timeSlot.end;
// Convert to decimal hours
let startTimeDecimal = startHour + (startMinute / 60);
let endTimeDecimal = endHour + (endMinute / 60);
// Handle overnight ranges (e.g., 20:00 - 1:00)
if (endTimeDecimal < startTimeDecimal) {
if (currentTimeDecimal >= startTimeDecimal || currentTimeDecimal < endTimeDecimal) {
return timeSlot;
}
} else {
if (currentTimeDecimal >= startTimeDecimal && currentTimeDecimal < endTimeDecimal) {
return timeSlot;
}
}
}
// Default to 30s if no match found (shouldn't happen with a complete schedule)
return { seconds: 30, name: '30s' };
}
// Get time-based preset
function getTimeBasedPreset() {
const timeSlot = getTimeBasedInterval();
// Look for a matching preset first
const matchingPreset = state.presets.find(p => p.seconds === timeSlot.seconds);
if (matchingPreset) {
return matchingPreset;
} else {
// Return a virtual preset if no matching preset exists
return {
name: timeSlot.name,
seconds: timeSlot.seconds,
isVirtual: true // Mark as virtual preset
};
}
}
// Start time check interval
function startTimeCheck() {
if (state.timeCheckInterval) {
clearInterval(state.timeCheckInterval);
}
// Immediately update current time preset
updateCurrentTimePreset();
// Check periodically for time-based interval changes
state.timeCheckInterval = setInterval(() => {
// Only check and apply if time-based presets are enabled
if (state.useTimeBasedPresets) {
const previousTimePreset = state.currentTimePreset;
updateCurrentTimePreset();
// Apply the new preset if we've changed time slots
if (previousTimePreset && state.currentTimePreset &&
previousTimePreset.seconds !== state.currentTimePreset.seconds) {
applyCurrentTimePreset();
}
}
}, 300000); // Check every 5 minutes
}
// Update the current time-based preset
function updateCurrentTimePreset() {
state.currentTimePreset = getTimeBasedPreset();
}
// Apply the current time-based preset
function applyCurrentTimePreset() {
if (!state.currentTimePreset || !state.onServiceOrdersPage) {
return;
}
// Record when preset was changed
state.lastPresetChangeTime = new Date();
// Completely clear any existing refresh
clearAutoRefresh();
// Set the active preset to the current time preset
state.activePreset = state.currentTimePreset;
// Start auto refresh with this preset if enabled
if (state.enabled) {
// Don't trigger immediate refresh if we recently refreshed
const shouldTriggerNow = !state.lastRefreshTime ||
(new Date() - state.lastRefreshTime > 10000); // 10 seconds threshold
startAutoRefresh(state.activePreset, !shouldTriggerNow);
} else {
// Just update the UI to highlight the correct preset
updateActivePreset();
}
}
// Trigger refresh click
function triggerRefresh() {
// Prevent double refresh by checking the pendingRefresh flag
if (state.pendingRefresh) {
return false;
}
state.pendingRefresh = true;
const refreshButton = findRefreshButton();
if (refreshButton) {
refreshButton.click();
state.lastRefreshTime = new Date();
// Update status with improved format
updateStatusWithRefreshInfo();
// Reset pending flag after a short delay
setTimeout(() => {
state.pendingRefresh = false;
}, 1000);
return true;
} else {
updateStatus('warning', 'Refresh button not found');
state.pendingRefresh = false;
return false;
}
}
// Schedule next refresh - separated from startAutoRefresh for cleaner code
function scheduleNextRefresh(preset) {
if (!preset || !state.enabled || !state.onServiceOrdersPage) {
return;
}
// Clear any existing timer first
clearAutoRefresh();
// Calculate jittered interval
const jitteredSeconds = getJitteredInterval(preset.seconds);
const intervalMs = Math.round(jitteredSeconds * 1000);
// Set timer for next refresh
state.refreshTimer = setTimeout(() => {
if (state.enabled && state.onServiceOrdersPage) {
triggerRefresh();
// Schedule the next refresh
scheduleNextRefresh(preset);
}
}, intervalMs);
}
// Start auto refresh with given preset
function startAutoRefresh(preset, skipInitialRefresh = false) {
// Don't start if disabled
if (!state.enabled) return;
// Clear existing timer first to prevent multiple refreshes
clearAutoRefresh();
// Set active preset
state.activePreset = preset;
// Update UI to reflect the current active preset
updateActivePreset();
// Only start timer if enabled and on service orders page
if (state.enabled && state.onServiceOrdersPage) {
// Trigger a refresh immediately when setting a new preset, unless skipInitialRefresh is true
if (!skipInitialRefresh) {
triggerRefresh();
}
// Schedule the next refresh
scheduleNextRefresh(preset);
}
// Save state
saveState();
}
// Clear auto refresh timer
function clearAutoRefresh() {
if (state.refreshTimer) {
clearTimeout(state.refreshTimer);
state.refreshTimer = null;
}
}
// Update active preset highlighting
function updateActivePreset() {
// Remove active class from all presets
document.querySelectorAll('.auto-refresh-preset').forEach(btn => {
btn.classList.remove('active');
});
// Add active class to current preset if it's one of the displayed presets
if (state.activePreset) {
const activeBtn = document.querySelector(`.auto-refresh-preset[data-seconds="${state.activePreset.seconds}"]`);
if (activeBtn) {
activeBtn.classList.add('active');
}
}
}
// Toggle auto refresh enabled state
function toggleEnabled() {
state.enabled = !state.enabled;
// Update UI
const container = document.getElementById('auto-refresh-container');
if (container) {
if (state.enabled) {
container.classList.remove('auto-refresh-disabled');
if (state.activePreset && state.onServiceOrdersPage) {
startAutoRefresh(state.activePreset);
}
} else {
container.classList.add('auto-refresh-disabled');
clearAutoRefresh();
// When disabled, immediately update status to "off" message
updateStatus('info', 'Auto refresh is off');
}
}
// Update page status
updatePageStatus();
// Save state
saveState();
}
// Toggle time-based presets feature
function toggleTimeBasedPresets() {
state.useTimeBasedPresets = !state.useTimeBasedPresets;
// Update UI
updateTimeBasedToggle();
// Always trigger refresh immediately when toggle is changed
// This will update the "Last checked" time
if (state.onServiceOrdersPage && state.enabled) {
triggerRefresh();
}
if (state.useTimeBasedPresets) {
// When turning ON time-based presets, switch to time-based preset immediately
updateCurrentTimePreset();
// Remember current user preset before switching
if (state.activePreset) {
state.userSelectedPreset = {...state.activePreset};
}
// Apply the time-based preset (without triggering another refresh)
clearAutoRefresh();
state.activePreset = state.currentTimePreset;
// Start auto refresh with this preset if enabled
if (state.enabled && state.onServiceOrdersPage) {
scheduleNextRefresh(state.activePreset);
}
// Make sure the time-based preset button is visually active
document.querySelectorAll('.auto-refresh-preset').forEach(btn => {
btn.classList.remove('active');
// Add active class to the current time preset
if (state.activePreset && btn.dataset.seconds == state.activePreset.seconds) {
btn.classList.add('active');
}
});
} else {
// When turning OFF time-based presets, switch back to user's manually selected preset
if (state.userSelectedPreset) {
clearAutoRefresh();
state.activePreset = state.userSelectedPreset;
// If enabled, schedule next refresh with the user's preset
if (state.enabled && state.onServiceOrdersPage) {
scheduleNextRefresh(state.activePreset);
} else {
// Just update UI if not enabled
updateActivePreset();
}
}
}
// Update status after changes
updateStatusWithRefreshInfo();
// Save state
saveState();
}
// Update time-based toggle state in UI
function updateTimeBasedToggle() {
const timeToggle = document.getElementById('time-based-toggle');
if (timeToggle) {
timeToggle.checked = state.useTimeBasedPresets;
}
}
// Toggle tip expanded/collapsed state
function toggleTip() {
state.tipExpanded = !state.tipExpanded;
const tipContent = document.getElementById('auto-refresh-tip-content');
if (!tipContent) return;
if (state.tipExpanded) {
tipContent.classList.add('expanded');
} else {
tipContent.classList.remove('expanded');
}
// Save state
saveState();
}
// Toggle collapsed state
function toggleCollapsed() {
state.collapsed = !state.collapsed;
// Update UI
const body = document.getElementById('auto-refresh-body');
const collapseBtn = document.getElementById('auto-refresh-collapse');
if (body && collapseBtn) {
if (state.collapsed) {
body.style.maxHeight = '0';
collapseBtn.textContent = '+';
} else {
body.style.maxHeight = '500px';
collapseBtn.textContent = '-';
}
}
// Save state
saveState();
}
// Save state to GM storage
function saveState() {
try {
GM_setValue('autoRefreshPresets', state.presets);
GM_setValue('autoRefreshPosition', state.position);
GM_setValue('autoRefreshEnabled', state.enabled);
GM_setValue('autoRefreshCollapsed', state.collapsed);
GM_setValue('autoRefreshActivePreset', state.activePreset);
GM_setValue('autoRefreshUseTimeBasedPresets', state.useTimeBasedPresets);
GM_setValue('autoRefreshTipExpanded', state.tipExpanded);
} catch (error) {
console.error('Error saving state:', error);
}
}
// Load state from GM storage
function loadState() {
try {
state.presets = GM_getValue('autoRefreshPresets', DEFAULT_PRESETS);
state.position = GM_getValue('autoRefreshPosition', { x: 20, y: 20 });
state.enabled = GM_getValue('autoRefreshEnabled', true);
state.collapsed = GM_getValue('autoRefreshCollapsed', false);
state.tipExpanded = GM_getValue('autoRefreshTipExpanded', false);
// Always default to time-based presets on page reload/relogin
state.useTimeBasedPresets = true;
// Store this value to preserve user's manual toggle for time-based presets
// during the current session, but not across reloads
const savedTimeBasedSetting = GM_getValue('autoRefreshUseTimeBasedPresets', true);
GM_setValue('autoRefreshUseTimeBasedPresets', true);
// Get the current time-based preset
updateCurrentTimePreset();
// Always use time-based preset on reload, regardless of previous setting
state.activePreset = state.currentTimePreset;
// If we've loaded an active preset and we're enabled, set the lastRefreshTime
// to avoid the "Select a refresh interval" message flash on load
if (state.activePreset && state.enabled) {
state.lastRefreshTime = new Date();
}
} catch (error) {
console.error('Error loading state:', error);
// Fallback to defaults
state.presets = DEFAULT_PRESETS;
state.position = { x: 20, y: 20 };
state.enabled = true;
state.collapsed = false;
state.useTimeBasedPresets = true;
state.tipExpanded = false;
updateCurrentTimePreset();
state.activePreset = state.currentTimePreset;
// Prevent "Select a refresh interval" message
state.lastRefreshTime = new Date();
}
}
// Format preset name based on seconds
function formatPresetName(seconds) {
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}min`;
}
// Show preset edit panel
function showEditPanel(preset) {
state.editingPreset = preset;
// Create overlay and panel
const overlay = document.createElement('div');
overlay.id = 'auto-refresh-edit-overlay';
const panel = document.createElement('div');
panel.id = 'auto-refresh-edit-panel';
// Create options HTML - with predefined array
const optionsHTML = TIME_OPTIONS.map(value => {
const selected = value === preset.seconds ? 'selected' : '';
const label = value < 60 ? `${value}s` : `${Math.floor(value/60)}min`;
return `<option value="${value}" ${selected}>${label}</option>`;
}).join('');
panel.innerHTML = `
<h3 style="font-size: 14px; margin-top: 0;">Edit Preset</h3>
<div class="auto-refresh-form-group">
<label class="auto-refresh-form-label">Select Interval:</label>
<select id="edit-preset-seconds" class="auto-refresh-form-input" style="appearance: auto; background-color: white;">
${optionsHTML}
</select>
<div id="edit-preset-error" style="color: #f44336; font-size: 11px; margin: 8px 0; display: none;"></div>
</div>
<div class="auto-refresh-form-buttons">
<button class="auto-refresh-form-button cancel">Cancel</button>
<button class="auto-refresh-form-button save">Save</button>
</div>
`;
overlay.appendChild(panel);
document.body.appendChild(overlay);
// Add event listeners
overlay.querySelector('.cancel').addEventListener('click', hideEditPanel);
overlay.querySelector('.save').addEventListener('click', saveEditedPreset);
// Handle pressing Enter key
const selectInput = document.getElementById('edit-preset-seconds');
selectInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
saveEditedPreset();
}
});
// Focus the select field
selectInput.focus();
}
// Save edited preset with duplicate check
function saveEditedPreset() {
const secondsInput = document.getElementById('edit-preset-seconds');
const errorElement = document.getElementById('edit-preset-error');
if (!secondsInput) return;
const seconds = parseInt(secondsInput.value, 10);
if (isNaN(seconds)) return;
// Check for duplicate
const duplicatePreset = state.presets.find(p =>
p.seconds === seconds &&
!(p.name === state.editingPreset.name && p.seconds === state.editingPreset.seconds)
);
if (duplicatePreset) {
if (errorElement) {
errorElement.textContent = `Preset "${duplicatePreset.name}" already uses this interval.`;
errorElement.style.display = 'block';
}
return;
}
// Format name and update preset
const name = formatPresetName(seconds);
const presetIndex = state.presets.findIndex(p =>
p.name === state.editingPreset.name && p.seconds === state.editingPreset.seconds
);
if (presetIndex >= 0) {
state.presets[presetIndex] = { name, seconds };
// If editing active preset, update it
if (state.activePreset &&
state.activePreset.name === state.editingPreset.name &&
state.activePreset.seconds === state.editingPreset.seconds) {
state.activePreset = { name, seconds };
if (state.enabled && state.onServiceOrdersPage) {
startAutoRefresh(state.activePreset);
}
}
// Update UI and save
createOrUpdateUI();
saveState();
}
hideEditPanel();
}
// Hide preset edit panel
function hideEditPanel() {
const overlay = document.getElementById('auto-refresh-edit-overlay');
if (overlay) overlay.remove();
state.editingPreset = null;
}
// Make element draggable - simplified for both mouse and touch
function makeDraggable(element, handleElement) {
let startX, startY, initialX, initialY;
let isDragging = false;
const onStart = (e) => {
// Don't initiate drag if it's a button or control
if (e.target.id === 'auto-refresh-collapse' ||
e.target.id === 'auto-refresh-toggle' ||
e.target.id === 'time-based-toggle' ||
e.target.closest('button') ||
e.target.closest('.switch')) {
return;
}
isDragging = true;
state.isDragging = true;
// Get starting positions
if (e.type === 'mousedown') {
startX = e.clientX;
startY = e.clientY;
} else if (e.type === 'touchstart') {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}
initialX = element.offsetLeft;
initialY = element.offsetTop;
// Add event listeners
if (e.type === 'mousedown') {
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onEnd);
} else if (e.type === 'touchstart') {
document.addEventListener('touchmove', onMove, { passive: false });
document.addEventListener('touchend', onEnd);
}
// Prevent default for handle
if (e.target === handleElement || handleElement.contains(e.target)) {
if (e.preventDefault) e.preventDefault();
}
};
const onMove = (e) => {
if (!isDragging) return;
// Calculate new position
let clientX, clientY;
if (e.type === 'mousemove') {
clientX = e.clientX;
clientY = e.clientY;
} else if (e.type === 'touchmove') {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
e.preventDefault(); // Prevent scrolling when dragging
}
const deltaX = clientX - startX;
const deltaY = clientY - startY;
const newLeft = initialX + deltaX;
const newTop = initialY + deltaY;
// Keep within viewport
const maxTop = window.innerHeight - element.offsetHeight;
const maxLeft = window.innerWidth - element.offsetWidth;
element.style.top = `${Math.min(Math.max(0, newTop), maxTop)}px`;
element.style.left = `${Math.min(Math.max(0, newLeft), maxLeft)}px`;
};
const onEnd = () => {
isDragging = false;
// Small delay to prevent accidental clicks
setTimeout(() => { state.isDragging = false; }, 50);
// Remove event listeners
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onEnd);
document.removeEventListener('touchmove', onMove);
document.removeEventListener('touchend', onEnd);
// Save position
state.position = {
x: parseInt(element.style.left, 10) || 20,
y: parseInt(element.style.top, 10) || 20
};
saveState();
};
handleElement.addEventListener('mousedown', onStart);
handleElement.addEventListener('touchstart', onStart, { passive: true });
}
// Update page status based on current state
function updatePageStatus() {
if (!state.onServiceOrdersPage) {
updateStatus('warning', 'Please navigate to Service Orders');
} else if (!state.enabled) {
// When disabled, always show "off" message regardless of last refresh time
updateStatus('info', 'Auto refresh is off');
} else if (state.activePreset) {
// Show improved status message with interval and last time
updateStatusWithRefreshInfo();
} else {
updateStatus('info', 'Select a refresh interval');
}
}
// Create or update UI
function createOrUpdateUI() {
// Check if UI already exists
let container = document.getElementById('auto-refresh-container');
if (!container) {
// Create new container
container = document.createElement('div');
container.id = 'auto-refresh-container';
document.body.appendChild(container);
// Set position
container.style.left = `${state.position.x}px`;
container.style.top = `${state.position.y}px`;
// Create UI structure
container.innerHTML = `
<div id="auto-refresh-header">
<span id="auto-refresh-title">Auto Refresh</span>
<div id="auto-refresh-controls">
<label class="switch">
<input type="checkbox" id="auto-refresh-toggle" ${state.enabled ? 'checked' : ''}>
<span class="slider"></span>
</label>
<span id="auto-refresh-collapse">${state.collapsed ? '+' : '-'}</span>
</div>
</div>
<div id="auto-refresh-body" style="max-height: ${state.collapsed ? '0' : '500px'};">
<div class="feature-toggle">
<span class="feature-label">Use time-based presets</span>
<label class="switch">
<input type="checkbox" id="time-based-toggle" ${state.useTimeBasedPresets ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>
<div id="auto-refresh-tip">
<div id="auto-refresh-tip-header">
<span>User Tip</span>
</div>
<div id="auto-refresh-tip-content" class="${state.tipExpanded ? 'expanded' : ''}">
Busy? Go fast. Slow? Take it easy
</div>
</div>
<div id="auto-refresh-presets"></div>
<div id="auto-refresh-status" class="info">Initializing...</div>
</div>
`;
// Add toggle event listeners
document.getElementById('auto-refresh-toggle').addEventListener('change', toggleEnabled);
document.getElementById('time-based-toggle').addEventListener('change', toggleTimeBasedPresets);
// Add collapse event listener
const domCollapseBtn = document.getElementById('auto-refresh-collapse');
if (domCollapseBtn) {
domCollapseBtn.onclick = function(e) {
if (e) {
e.stopPropagation();
e.preventDefault();
}
toggleCollapsed();
return false;
};
}
// Add tip toggle functionality
const tipHeader = document.getElementById('auto-refresh-tip-header');
if (tipHeader) {
tipHeader.addEventListener('click', function(e) {
toggleTip();
e.stopPropagation();
});
}
// Make draggable
makeDraggable(container, document.getElementById('auto-refresh-header'));
// Set disabled class if needed
if (!state.enabled) {
container.classList.add('auto-refresh-disabled');
}
}
// Update presets
const presetsContainer = document.getElementById('auto-refresh-presets');
if (presetsContainer) {
presetsContainer.innerHTML = '';
state.presets.forEach(preset => {
const presetBtn = document.createElement('button');
presetBtn.className = 'auto-refresh-preset';
presetBtn.textContent = preset.name;
presetBtn.dataset.seconds = preset.seconds;
// Set active class if needed
if (state.activePreset &&
state.activePreset.seconds === preset.seconds) {
presetBtn.classList.add('active');
}
// Normal click event
presetBtn.addEventListener('click', () => {
if (!state.isDragging && state.enabled) {
// When user selects a preset manually, disable time-based presets
state.useTimeBasedPresets = false;
updateTimeBasedToggle();
// Save this as the user's manual preset
state.userSelectedPreset = {...preset};
// Set this preset as active
startAutoRefresh(preset);
// Ensure this preset gets highlighted (not just in startAutoRefresh)
document.querySelectorAll('.auto-refresh-preset').forEach(btn => {
btn.classList.remove('active');
});
presetBtn.classList.add('active');
}
});
// Right-click for edit
presetBtn.addEventListener('contextmenu', (e) => {
e.preventDefault();
showEditPanel(preset);
});
// Long press for mobile
let longPressTimer;
let longPressStarted = false;
let longPressFired = false; // New flag to track when long press action has fired
presetBtn.addEventListener('touchstart', () => {
longPressStarted = true;
longPressFired = false;
longPressTimer = setTimeout(() => {
if (longPressStarted) {
longPressFired = true; // Set flag when edit panel is shown
showEditPanel(preset);
}
}, 800);
});
presetBtn.addEventListener('touchmove', () => {
longPressStarted = false;
clearTimeout(longPressTimer);
});
presetBtn.addEventListener('touchend', () => {
// Only activate preset if it wasn't a long press
if (!state.isDragging && longPressStarted && !longPressFired && state.enabled) {
// Normal tap behavior - activate the preset
state.useTimeBasedPresets = false;
updateTimeBasedToggle();
// Save this as the user's manual preset
state.userSelectedPreset = {...preset};
startAutoRefresh(preset);
// Ensure this preset gets highlighted
document.querySelectorAll('.auto-refresh-preset').forEach(btn => {
btn.classList.remove('active');
});
presetBtn.classList.add('active');
}
longPressStarted = false;
clearTimeout(longPressTimer);
});
presetsContainer.appendChild(presetBtn);
});
}
// Update status
updatePageStatus();
}
// Check page and update UI accordingly
function checkPageAndUpdateUI() {
const wasOnServiceOrdersPage = state.onServiceOrdersPage;
state.onServiceOrdersPage = checkIfOnServiceOrdersPage();
// Reset button observed state if needed
if (!state.onServiceOrdersPage) {
state.refreshButtonObserved = false;
}
// Update status
updatePageStatus();
// Handle page transitions
if (!wasOnServiceOrdersPage && state.onServiceOrdersPage) {
// Just arrived at service orders page
if (state.enabled) {
triggerRefresh();
if (state.activePreset) {
startAutoRefresh(state.activePreset);
}
}
} else if (wasOnServiceOrdersPage && !state.onServiceOrdersPage) {
// Just left service orders page
clearAutoRefresh();
}
}
// Setup observer for page changes
function setupObserver() {
const observer = new MutationObserver(() => {
// Debounce to prevent excessive checks
if (!state.observerDebounce) {
state.observerDebounce = true;
setTimeout(() => {
checkPageAndUpdateUI();
state.observerDebounce = false;
}, 1000);
}
});
// Observe body changes
observer.observe(document.body, { childList: true, subtree: true });
}
// Initialize the script
function init() {
// Load saved state
loadState();
// Create UI
createOrUpdateUI();
// Make sure the correct toggle state is shown
updateTimeBasedToggle();
// Setup observer
setupObserver();
// Start time check for time-based presets
startTimeCheck();
// Check current page
checkPageAndUpdateUI();
// Start auto refresh if applicable
if (state.activePreset && state.onServiceOrdersPage && state.enabled) {
startAutoRefresh(state.activePreset);
}
// Ensure the correct preset is highlighted
updateActivePreset();
state.initialized = true;
}
// Handle global errors
window.addEventListener('error', function(event) {
if (event.filename && event.filename.includes('Auto Refresh Tool')) {
//console.log('Auto Refresh Tool error:', error);
event.preventDefault();
return true;
}
return false;
}, true);
// Initialize with retry mechanism
let initAttempts = 0;
const maxInitAttempts = 3;
function attemptInit() {
if (initAttempts >= maxInitAttempts) {
console.error('Failed to initialize Auto Refresh Tool after multiple attempts');
return;
}
if (!state.initialized) {
initAttempts++;
init();
// Schedule another attempt if needed
if (!state.initialized) {
setTimeout(attemptInit, 2000);
}
}
}
// Start initialization with delay
setTimeout(attemptInit, 1000);
// Backup initialization
setTimeout(() => {
if (!document.getElementById('auto-refresh-container')) {
createOrUpdateUI();
checkPageAndUpdateUI();
}
}, 5000);
})();