New Ozonetel Timer Script

Timer, break tracking, and disposition alerts for Ozonetel with statistics display

  1. // ==UserScript==
  2. // @name New Ozonetel Timer Script
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1.1
  5. // @description Timer, break tracking, and disposition alerts for Ozonetel with statistics display
  6. // @match https://agent.cloudagent.ozonetel.com/*
  7. // @grant GM_xmlhttpRequest
  8. // @license MIT
  9. // @author Melvin Benedict
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14. // Constants
  15. const STORAGE_KEY = 'ozonetelTimerState';
  16. const UPDATE_INTERVAL = 5000;
  17. const SAVE_INTERVAL = 300000;
  18. const DISPOSITION_WARNING_TIME = 30000;
  19. const DISPOSITION_ALERT_TIME = 80000;
  20.  
  21. // CSS Constants
  22. const COLORS = {
  23. primary: '#3D8BF8', // Bright blue from the button
  24. primaryHover: '#357ADC', // Slightly darker shade for hover effect
  25. secondary: '#2E6CC0', // Even darker shade for accents
  26. text: '#FFFFFF', // White text
  27. shadow: 'rgba(0, 0, 0, 0.2)' // Subtle shadow
  28. };
  29.  
  30.  
  31.  
  32. // UI Config
  33. const UI_CONFIG = {
  34. timerDisplay: {
  35. styles: {
  36. position: 'fixed',
  37. bottom: '20px',
  38. right: '40px',
  39. padding: '12px 20px',
  40. backgroundColor: COLORS.primary,
  41. color: COLORS.text,
  42. borderRadius: '12px',
  43. zIndex: '1000',
  44. fontSize: '18px',
  45. fontWeight: '500',
  46. boxShadow: `0 4px 15px ${COLORS.shadow}`,
  47. cursor: 'pointer',
  48. transition: 'all 0.7s ease',
  49. userSelect: 'none',
  50. display: 'flex',
  51. alignItems: 'center',
  52. gap: '8px'
  53. }
  54. },
  55. collapseButton: {
  56. styles: {
  57. position: 'fixed',
  58. bottom: '20px',
  59. right: '8px',
  60. padding: '8px',
  61. backgroundColor: COLORS.secondary,
  62. color: COLORS.text,
  63. borderRadius: '30%',
  64. zIndex: '1001',
  65. fontSize: '24px',
  66. fontWeight: 'bold',
  67. boxShadow: `0 4px 15px ${COLORS.shadow}`,
  68. cursor: 'pointer',
  69. transition: 'all 0.3s ease',
  70. userSelect: 'none'
  71. }
  72. },
  73. statsPanel: {
  74. styles: {
  75. position: 'absolute',
  76. bottom: '90px',
  77. right: '20px',
  78. padding: '20px',
  79. backgroundColor: COLORS.secondary,
  80. color: COLORS.text,
  81. borderRadius: '12px',
  82. zIndex: '999',
  83. fontSize: '14px',
  84. display: 'none',
  85. minWidth: '320px',
  86. maxHeight: '70vh',
  87. overflowY: 'auto',
  88. boxShadow: `0 4px 20px ${COLORS.shadow}`,
  89. transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
  90. transform: 'translateY(10px)',
  91. backdropFilter: 'blur(8px)',
  92. border: '1px solid rgba(255, 255, 255, 0.1)'
  93. }
  94. }
  95. };
  96.  
  97. // State management
  98. const state = {
  99. timer: {
  100. display: null,
  101. statsPanel: null,
  102. interval: null,
  103. loginTime: 0,
  104. startTime: null,
  105. currentBreakStart: null,
  106. breaks: [],
  107. showingStats: false,
  108. totalBreakTime: 0,
  109. isOnBreak: false // New flag to track break status
  110. },
  111. disposition: {
  112. buttonFound: false,
  113. notificationSent: false,
  114. audioPlayed: false,
  115. startTime: null,
  116. audio: null
  117. }
  118. };
  119.  
  120. function initialize() {
  121. console.log('Initializing Ozonetel Timer Script...');
  122. createUIElements();
  123. loadSavedState();
  124. setupEventListeners();
  125. initializeAudio();
  126. console.log('Initialization complete');
  127. }
  128.  
  129. function createUIElements() {
  130. // Timer Display with icon
  131. state.timer.display = createElement('div', UI_CONFIG.timerDisplay.styles);
  132. state.timer.display.innerHTML = `
  133. <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  134. <circle cx="12" cy="12" r="10"></circle>
  135. <polyline points="12 6 12 12 16 14"></polyline>
  136. </svg>
  137. <span>Login Time: 00:00:00</span>
  138. `;
  139.  
  140. // Collapse Button
  141. const collapseButton = createElement('div', UI_CONFIG.collapseButton.styles);
  142. collapseButton.innerHTML = '⏳';
  143. collapseButton.addEventListener('click', toggleTimerDisplay);
  144.  
  145. // Stats Panel with modern layout
  146. state.timer.statsPanel = createElement('div', UI_CONFIG.statsPanel.styles);
  147. state.timer.statsPanel.innerHTML = `
  148. <div style="margin-bottom: 20px; font-size: 18px; font-weight: 600;">Activity Statistics</div>
  149. <div class="stat-item" style="margin-bottom: 15px;">
  150. <div style="color: rgba(255,255,255,0.8);">Login Duration</div>
  151. <div id="loginTimeDisplay" style="font-size: 16px; font-weight: 500;">00:00:00</div>
  152. </div>
  153. <div class="stat-item" style="margin-bottom: 15px;">
  154. <div style="color: rgba(255,255,255,0.8);">Total Break Time</div>
  155. <div id="totalBreakTime" style="font-size: 16px; font-weight: 500;">00:00:00</div>
  156. </div>
  157. <div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.1);">
  158. <div style="color: rgba(255,255,255,0.8); margin-bottom: 10px;">Break History</div>
  159. <div id="breakHistoryDisplay" style="font-size: 14px;">No breaks taken yet.</div>
  160. </div>
  161. `;
  162.  
  163. // Add hover effects
  164. addHoverEffect(state.timer.display);
  165. // Append elements to body
  166. document.body.appendChild(state.timer.display);
  167. document.body.appendChild(collapseButton);
  168. document.body.appendChild(state.timer.statsPanel);
  169. // Add click handler for stats toggle
  170. state.timer.display.addEventListener('click', toggleStats);
  171. }
  172.  
  173. function toggleTimerDisplay() {
  174. if (state.timer.display.style.display === 'none' || !state.timer.display.style.display) {
  175. state.timer.display.style.display = 'flex';
  176. } else {
  177. state.timer.display.style.display = 'none';
  178. }
  179. }
  180.  
  181. function createElement(tag, styles) {
  182. const element = document.createElement(tag);
  183. Object.assign(element.style, styles);
  184. return element;
  185. }
  186.  
  187. function addHoverEffect(element) {
  188. element.addEventListener('mouseover', () => {
  189. element.style.backgroundColor = COLORS.primaryHover;
  190. element.style.transform = 'translateY(-2px)';
  191. });
  192. element.addEventListener('mouseout', () => {
  193. element.style.backgroundColor = COLORS.primary;
  194. element.style.transform = 'translateY(0)';
  195. });
  196. }
  197.  
  198. function toggleStats() {
  199. state.timer.showingStats = !state.timer.showingStats;
  200. state.timer.statsPanel.style.display = state.timer.showingStats ? 'block' : 'none';
  201. if (state.timer.showingStats) {
  202. setTimeout(() => {
  203. state.timer.statsPanel.style.transform = 'translateY(0)';
  204. state.timer.statsPanel.style.opacity = '1';
  205. }, 50);
  206. updateStatsDisplay();
  207. } else {
  208. state.timer.statsPanel.style.transform = 'translateY(10px)';
  209. state.timer.statsPanel.style.opacity = '0';
  210. }
  211. }
  212.  
  213. function initializeAudio() {
  214. state.disposition.audio = new Audio('https://dl.dropboxusercontent.com/scl/fi/ilepbnobrix5g36zd9qiu/mixkit-facility-alarm-908.wav?rlkey=o9dak8j28dqcpir765od9tb6k&st=6zdjcfpf');
  215. state.disposition.audio.preload = 'none';
  216. }
  217.  
  218. function loadSavedState() {
  219. const savedState = JSON.parse(localStorage.getItem(STORAGE_KEY));
  220. if (savedState?.loginTime) {
  221. const restore = confirm("Do you want to restore your previous logged-in time?");
  222. state.timer.loginTime = restore ? savedState.loginTime : 0;
  223. if (savedState.breaks && restore) {
  224. state.timer.breaks = savedState.breaks;
  225. state.timer.totalBreakTime = savedState.breaks.reduce((a, b) => a + b, 0);
  226. }
  227. updateDisplay();
  228. }
  229. }
  230.  
  231. function setupEventListeners() {
  232. window.addEventListener('beforeunload', saveState);
  233. setInterval(detectLoginState, UPDATE_INTERVAL);
  234. setInterval(detectDisposition, UPDATE_INTERVAL);
  235. setInterval(saveState, SAVE_INTERVAL);
  236. }
  237.  
  238. function saveState() {
  239. localStorage.setItem(STORAGE_KEY, JSON.stringify({
  240. loginTime: state.timer.loginTime,
  241. breaks: state.timer.breaks,
  242. totalBreakTime: state.timer.totalBreakTime
  243. }));
  244. }
  245.  
  246. function startTimer() {
  247. if (!state.timer.interval) {
  248. state.timer.startTime = Date.now() - (state.timer.loginTime * 1000);
  249. state.timer.interval = setInterval(updateTimer, 1000);
  250. }
  251. }
  252.  
  253. function stopTimer() {
  254. if (state.timer.interval) {
  255. clearInterval(state.timer.interval);
  256. state.timer.interval = null;
  257. }
  258. }
  259.  
  260. function updateTimer() {
  261. state.timer.loginTime = Math.floor((Date.now() - state.timer.startTime) / 1000);
  262. updateDisplay();
  263. }
  264.  
  265. function formatTime(seconds) {
  266. const hours = Math.floor(seconds / 3600) % 24;
  267. const minutes = Math.floor(seconds / 60) % 60;
  268. const secs = seconds % 60;
  269. return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
  270. }
  271.  
  272. function updateDisplay() {
  273. const timeStr = formatTime(state.timer.loginTime);
  274. state.timer.display.querySelector('span').textContent = `Login Time: ${timeStr}`;
  275. if (state.timer.showingStats) {
  276. document.getElementById('loginTimeDisplay').textContent = timeStr;
  277. }
  278. }
  279.  
  280. function detectLoginState() {
  281. const pauseButton = document.querySelector('.oz-tb-agentState-pause-icon');
  282. const playButton = document.querySelector('.oz-tb-agentState-play-icon');
  283. let foundPlayIcon = false;
  284. let foundPauseIcon = false;
  285.  
  286. // Detect if the pause button is visible (active)
  287. if (pauseButton && pauseButton.style.display !== 'none') {
  288. foundPauseIcon = true;
  289. }
  290.  
  291. // Detect if the play button is visible (active)
  292. if (playButton && playButton.style.display !== 'none') {
  293. foundPlayIcon = true;
  294. }
  295.  
  296. // Break detection logic
  297. if (foundPlayIcon && !state.timer.isOnBreak) {
  298. // Agent went on break
  299. startBreak();
  300. state.timer.isOnBreak = true;
  301. stopTimer();
  302. } else if (foundPauseIcon && state.timer.isOnBreak) {
  303. // Agent returned from break
  304. endBreak();
  305. state.timer.isOnBreak = false;
  306. startTimer();
  307. } else if (foundPauseIcon) {
  308. // Normal working state
  309. startTimer();
  310. }
  311. }
  312.  
  313. function startBreak() {
  314. console.log('Break started');
  315. state.timer.currentBreakStart = Date.now();
  316. state.timer.display.style.backgroundColor = '#DC2626'; // Red color during break
  317.  
  318. // Log "on break" status to Google Sheets (Break Status sheet)
  319. logToGoogleSheet("on break");
  320.  
  321. // Set a timeout to log break time if it crosses one minute
  322. state.timer.breakTimeout = setTimeout(() => {
  323. const breakDuration = (Date.now() - state.timer.currentBreakStart) / 1000; // Current break duration in seconds
  324. if (breakDuration >= 600) {
  325. logBreakTime(breakDuration); // Log break time to Slack Trigger Sheet
  326. }
  327. }, 600000); // Trigger after 1 minute
  328. }
  329.  
  330. function endBreak() {
  331. if (state.timer.currentBreakStart) {
  332. const breakDuration = (Date.now() - state.timer.currentBreakStart) / 1000;
  333. console.log('Break ended, duration:', breakDuration);
  334. state.timer.breaks.push(breakDuration);
  335. state.timer.totalBreakTime += breakDuration;
  336. state.timer.currentBreakStart = null;
  337.  
  338. // Reset timer display color
  339. state.timer.display.style.backgroundColor = COLORS.primary;
  340.  
  341. // Clear the 1-minute timeout if the break ends before 1 minute
  342. if (state.timer.breakTimeout) {
  343. clearTimeout(state.timer.breakTimeout);
  344. state.timer.breakTimeout = null;
  345. }
  346.  
  347. // Log "active" status to Google Sheets (Break Status sheet)
  348. logToGoogleSheet("active");
  349. }
  350. }
  351.  
  352. function logBreakTime(breakDuration) {
  353. // Get the current timestamp in "YYYY-MM-DD HH:MM:SS" format
  354. const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
  355.  
  356. // Extract the agent's initials from the avatar element
  357. const avatarElement = document.querySelector('.MuiAvatar-root.MuiAvatar-circular.MuiAvatar-colorDefault');
  358.  
  359. if (avatarElement) {
  360. // Access the text content of the avatar to get the initials
  361. const agentInitials = avatarElement.textContent.trim();
  362. console.log('Agent Initials:', agentInitials); // Logs the initials (e.g., "MB")
  363. } else {
  364. console.error('Avatar element not found!');
  365. return; // Exit early if avatar element is not found
  366. }
  367.  
  368. // Replace with your Web App URL
  369. const sheetEndpoint = "https://script.google.com/macros/s/AKfycbzqzymoOVct0gRPYr_RYiZmAYM4VPfUSd0FZWrI6PElOV5O4fIiz7nQ8_t_zBchDFlk/exec";
  370.  
  371. // Create the POST payload as JSON, including agent initials, break duration, and timestamp
  372. const postData = {
  373. agentName: agentInitials, // Use initials instead of full name
  374. status: "on break", // Status should be on break for Slack Trigger sheet
  375. break_duration: Math.floor(breakDuration), // Log whole seconds
  376. timestamp: timestamp // Timestamp when break time was logged
  377. };
  378.  
  379. // Send the request using GM_xmlhttpRequest
  380. GM_xmlhttpRequest({
  381. method: "POST",
  382. url: sheetEndpoint,
  383. headers: {
  384. "Content-Type": "application/json" // Specify JSON format
  385. },
  386. data: JSON.stringify(postData), // Convert payload to JSON string
  387. onload: function(response) {
  388. if (response.status === 200) {
  389. console.log(`Logged break time successfully for ${agentInitials} at ${timestamp}. Duration: ${breakDuration} seconds.`);
  390. } else {
  391. console.error(`Failed to log break time. HTTP status: ${response.status}, Message: ${response.statusText}`);
  392. }
  393. },
  394. onerror: function(error) {
  395. console.error("Error logging break time:", error);
  396. }
  397. });
  398. }
  399.  
  400. function logToGoogleSheet(status) {
  401. // Get the current timestamp in "YYYY-MM-DD HH:MM:SS" format
  402. const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
  403.  
  404. // Extract the agent's initials from the avatar element (for status logging)
  405. const avatarElement = document.querySelector('.MuiAvatar-root.MuiAvatar-circular.MuiAvatar-colorDefault');
  406. const agentInitials = avatarElement ? avatarElement.textContent.trim() : 'Unknown Agent';
  407. console.log('Agent Initials:', agentInitials); // Logs the initials (e.g., "MB")
  408.  
  409. // Replace with your Web App URL
  410. const sheetEndpoint = "https://script.google.com/macros/s/AKfycbzqzymoOVct0gRPYr_RYiZmAYM4VPfUSd0FZWrI6PElOV5O4fIiz7nQ8_t_zBchDFlk/exec";
  411.  
  412. // Create the POST payload as JSON, including agent initials, status, and timestamp
  413. const postData = {
  414. agentName: agentInitials, // Use initials instead of full name
  415. status: status, // "on break" or "active"
  416. timestamp: timestamp, // Timestamp when status was logged
  417. };
  418.  
  419. // Send the request using GM_xmlhttpRequest
  420. GM_xmlhttpRequest({
  421. method: "POST",
  422. url: sheetEndpoint,
  423. headers: {
  424. "Content-Type": "application/json" // Specify JSON format
  425. },
  426. data: JSON.stringify(postData), // Convert payload to JSON string
  427. onload: function(response) {
  428. if (response.status === 200) {
  429. console.log(`Logged status '${status}' successfully for ${agentInitials} at ${timestamp}.`);
  430. } else {
  431. console.error(`Failed to log status. HTTP status: ${response.status}, Message: ${response.statusText}`);
  432. }
  433. },
  434. onerror: function(error) {
  435. console.error("Error logging status:", error);
  436. }
  437. });
  438. }
  439.  
  440.  
  441. function detectDisposition() {
  442. const button = document.querySelector('button.oz-tb-callback-dialog-save-button.oz-tb-call-end-diposition-button');
  443. if (button) {
  444. if (!state.disposition.buttonFound) {
  445. state.disposition.buttonFound = true;
  446. state.disposition.startTime = Date.now();
  447. return;
  448. }
  449. const elapsedTime = Date.now() - state.disposition.startTime;
  450. if (elapsedTime >= DISPOSITION_WARNING_TIME && !state.disposition.notificationSent) {
  451. showNotification();
  452. state.disposition.notificationSent = true;
  453. }
  454. if (elapsedTime >= DISPOSITION_ALERT_TIME && !state.disposition.audioPlayed) {
  455. playDispositionAlert();
  456. }
  457. } else {
  458. resetDispositionState();
  459. }
  460. }
  461.  
  462. function playDispositionAlert() {
  463. state.disposition.audio.load();
  464. state.disposition.audio.play().catch(error => console.error('Audio playback failed:', error));
  465. state.disposition.audioPlayed = true;
  466. }
  467.  
  468. function resetDispositionState() {
  469. Object.assign(state.disposition, {
  470. buttonFound: false,
  471. notificationSent: false,
  472. audioPlayed: false,
  473. startTime: null
  474. });
  475. }
  476.  
  477. function showNotification() {
  478. if (!("Notification" in window)) return;
  479. Notification.requestPermission()
  480. .then(permission => {
  481. if (permission === "granted") {
  482. new Notification("Disposition Alert", {
  483. body: "Disposition Pending!",
  484. icon: 'https://example.com/icon.png'
  485. }).onclick = function() {
  486. window.focus();
  487. this.close();
  488. };
  489. }
  490. })
  491. .catch(error => console.error('Notification error:', error));
  492. }
  493.  
  494. function updateStatsDisplay() {
  495. if (!state.timer.showingStats) return;
  496. const breakHistory = state.timer.breaks.map((breakTime, index) => `
  497. <div style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1)">
  498. <span style="color: rgba(255,255,255,0.7)">Break ${index + 1}:</span>
  499. <span style="float: right">${formatTime(Math.floor(breakTime))}</span>
  500. </div>
  501. `).join('');
  502. document.getElementById('breakHistoryDisplay').innerHTML = breakHistory ||
  503. '<div style="color: rgba(255,255,255,0.6)">No breaks taken yet.</div>';
  504. document.getElementById('totalBreakTime').textContent = formatTime(Math.floor(state.timer.totalBreakTime));
  505. }
  506.  
  507. // Initialize the script
  508. initialize();
  509. })();