New Ozonetel Timer Script

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

// ==UserScript==
// @name     New Ozonetel Timer Script
// @namespace  http://tampermonkey.net/
// @version   1.1.1
// @description Timer, break tracking, and disposition alerts for Ozonetel with statistics display
// @match    https://agent.cloudagent.ozonetel.com/*
// @grant    GM_xmlhttpRequest
// @license  MIT
// @author   Melvin Benedict
// ==/UserScript==

(function() {
  'use strict';
  // Constants
  const STORAGE_KEY = 'ozonetelTimerState';
  const UPDATE_INTERVAL = 5000;
  const SAVE_INTERVAL = 300000;
  const DISPOSITION_WARNING_TIME = 30000;
  const DISPOSITION_ALERT_TIME = 80000;

  // CSS Constants
const COLORS = {
  primary: '#3D8BF8',   // Bright blue from the button
  primaryHover: '#357ADC', // Slightly darker shade for hover effect
  secondary: '#2E6CC0', // Even darker shade for accents
  text: '#FFFFFF',      // White text
  shadow: 'rgba(0, 0, 0, 0.2)' // Subtle shadow
};



  // UI Config
  const UI_CONFIG = {
    timerDisplay: {
      styles: {
        position: 'fixed',
        bottom: '20px',
        right: '40px',
        padding: '12px 20px',
        backgroundColor: COLORS.primary,
        color: COLORS.text,
        borderRadius: '12px',
        zIndex: '1000',
        fontSize: '18px',
        fontWeight: '500',
        boxShadow: `0 4px 15px ${COLORS.shadow}`,
        cursor: 'pointer',
        transition: 'all 0.7s ease',
        userSelect: 'none',
        display: 'flex',
        alignItems: 'center',
        gap: '8px'
      }
    },
    collapseButton: {
      styles: {
        position: 'fixed',
        bottom: '20px',
        right: '8px',
        padding: '8px',
        backgroundColor: COLORS.secondary,
        color: COLORS.text,
        borderRadius: '30%',
        zIndex: '1001',
        fontSize: '24px',
        fontWeight: 'bold',
        boxShadow: `0 4px 15px ${COLORS.shadow}`,
        cursor: 'pointer',
        transition: 'all 0.3s ease',
        userSelect: 'none'
      }
    },
    statsPanel: {
      styles: {
        position: 'absolute',
        bottom: '90px',
        right: '20px',
        padding: '20px',
        backgroundColor: COLORS.secondary,
        color: COLORS.text,
        borderRadius: '12px',
        zIndex: '999',
        fontSize: '14px',
        display: 'none',
        minWidth: '320px',
        maxHeight: '70vh',
        overflowY: 'auto',
        boxShadow: `0 4px 20px ${COLORS.shadow}`,
        transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
        transform: 'translateY(10px)',
        backdropFilter: 'blur(8px)',
        border: '1px solid rgba(255, 255, 255, 0.1)'
      }
    }
  };

  // State management
  const state = {
    timer: {
      display: null,
      statsPanel: null,
      interval: null,
      loginTime: 0,
      startTime: null,
      currentBreakStart: null,
      breaks: [],
      showingStats: false,
      totalBreakTime: 0,
      isOnBreak: false // New flag to track break status
    },
    disposition: {
      buttonFound: false,
      notificationSent: false,
      audioPlayed: false,
      startTime: null,
      audio: null
    }
  };

  function initialize() {
    console.log('Initializing Ozonetel Timer Script...');
    createUIElements();
    loadSavedState();
    setupEventListeners();
    initializeAudio();
    console.log('Initialization complete');
  }

  function createUIElements() {
    // Timer Display with icon
    state.timer.display = createElement('div', UI_CONFIG.timerDisplay.styles);
    state.timer.display.innerHTML = `
      <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <circle cx="12" cy="12" r="10"></circle>
        <polyline points="12 6 12 12 16 14"></polyline>
      </svg>
      <span>Login Time: 00:00:00</span>
    `;

    // Collapse Button
    const collapseButton = createElement('div', UI_CONFIG.collapseButton.styles);
    collapseButton.innerHTML = '⏳';
    collapseButton.addEventListener('click', toggleTimerDisplay);

    // Stats Panel with modern layout
    state.timer.statsPanel = createElement('div', UI_CONFIG.statsPanel.styles);
    state.timer.statsPanel.innerHTML = `
      <div style="margin-bottom: 20px; font-size: 18px; font-weight: 600;">Activity Statistics</div>
      <div class="stat-item" style="margin-bottom: 15px;">
        <div style="color: rgba(255,255,255,0.8);">Login Duration</div>
        <div id="loginTimeDisplay" style="font-size: 16px; font-weight: 500;">00:00:00</div>
      </div>
      <div class="stat-item" style="margin-bottom: 15px;">
        <div style="color: rgba(255,255,255,0.8);">Total Break Time</div>
        <div id="totalBreakTime" style="font-size: 16px; font-weight: 500;">00:00:00</div>
      </div>
      <div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.1);">
        <div style="color: rgba(255,255,255,0.8); margin-bottom: 10px;">Break History</div>
        <div id="breakHistoryDisplay" style="font-size: 14px;">No breaks taken yet.</div>
      </div>
    `;

    // Add hover effects
    addHoverEffect(state.timer.display);
    // Append elements to body
    document.body.appendChild(state.timer.display);
    document.body.appendChild(collapseButton);
    document.body.appendChild(state.timer.statsPanel);
    // Add click handler for stats toggle
    state.timer.display.addEventListener('click', toggleStats);
  }

  function toggleTimerDisplay() {
    if (state.timer.display.style.display === 'none' || !state.timer.display.style.display) {
      state.timer.display.style.display = 'flex';
    } else {
      state.timer.display.style.display = 'none';
    }
  }

  function createElement(tag, styles) {
    const element = document.createElement(tag);
    Object.assign(element.style, styles);
    return element;
  }

  function addHoverEffect(element) {
    element.addEventListener('mouseover', () => {
      element.style.backgroundColor = COLORS.primaryHover;
      element.style.transform = 'translateY(-2px)';
    });
    element.addEventListener('mouseout', () => {
      element.style.backgroundColor = COLORS.primary;
      element.style.transform = 'translateY(0)';
    });
  }

  function toggleStats() {
    state.timer.showingStats = !state.timer.showingStats;
    state.timer.statsPanel.style.display = state.timer.showingStats ? 'block' : 'none';
    if (state.timer.showingStats) {
      setTimeout(() => {
        state.timer.statsPanel.style.transform = 'translateY(0)';
        state.timer.statsPanel.style.opacity = '1';
      }, 50);
      updateStatsDisplay();
    } else {
      state.timer.statsPanel.style.transform = 'translateY(10px)';
      state.timer.statsPanel.style.opacity = '0';
    }
  }

  function initializeAudio() {
    state.disposition.audio = new Audio('https://dl.dropboxusercontent.com/scl/fi/ilepbnobrix5g36zd9qiu/mixkit-facility-alarm-908.wav?rlkey=o9dak8j28dqcpir765od9tb6k&st=6zdjcfpf');
    state.disposition.audio.preload = 'none';
  }

  function loadSavedState() {
    const savedState = JSON.parse(localStorage.getItem(STORAGE_KEY));
    if (savedState?.loginTime) {
      const restore = confirm("Do you want to restore your previous logged-in time?");
      state.timer.loginTime = restore ? savedState.loginTime : 0;
      if (savedState.breaks && restore) {
        state.timer.breaks = savedState.breaks;
        state.timer.totalBreakTime = savedState.breaks.reduce((a, b) => a + b, 0);
      }
      updateDisplay();
    }
  }

  function setupEventListeners() {
    window.addEventListener('beforeunload', saveState);
    setInterval(detectLoginState, UPDATE_INTERVAL);
    setInterval(detectDisposition, UPDATE_INTERVAL);
    setInterval(saveState, SAVE_INTERVAL);
  }

  function saveState() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({
      loginTime: state.timer.loginTime,
      breaks: state.timer.breaks,
      totalBreakTime: state.timer.totalBreakTime
    }));
  }

  function startTimer() {
    if (!state.timer.interval) {
      state.timer.startTime = Date.now() - (state.timer.loginTime * 1000);
      state.timer.interval = setInterval(updateTimer, 1000);
    }
  }

  function stopTimer() {
    if (state.timer.interval) {
      clearInterval(state.timer.interval);
      state.timer.interval = null;
    }
  }

  function updateTimer() {
    state.timer.loginTime = Math.floor((Date.now() - state.timer.startTime) / 1000);
    updateDisplay();
  }

  function formatTime(seconds) {
    const hours = Math.floor(seconds / 3600) % 24;
    const minutes = Math.floor(seconds / 60) % 60;
    const secs = seconds % 60;
    return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
  }

  function updateDisplay() {
    const timeStr = formatTime(state.timer.loginTime);
    state.timer.display.querySelector('span').textContent = `Login Time: ${timeStr}`;
    if (state.timer.showingStats) {
      document.getElementById('loginTimeDisplay').textContent = timeStr;
    }
  }

 function detectLoginState() {
  const pauseButton = document.querySelector('.oz-tb-agentState-pause-icon');
  const playButton = document.querySelector('.oz-tb-agentState-play-icon');
  let foundPlayIcon = false;
  let foundPauseIcon = false;

  // Detect if the pause button is visible (active)
  if (pauseButton && pauseButton.style.display !== 'none') {
    foundPauseIcon = true;
  }

  // Detect if the play button is visible (active)
  if (playButton && playButton.style.display !== 'none') {
    foundPlayIcon = true;
  }

  // Break detection logic
  if (foundPlayIcon && !state.timer.isOnBreak) {
    // Agent went on break
    startBreak();
    state.timer.isOnBreak = true;
    stopTimer();
  } else if (foundPauseIcon && state.timer.isOnBreak) {
    // Agent returned from break
    endBreak();
    state.timer.isOnBreak = false;
    startTimer();
  } else if (foundPauseIcon) {
    // Normal working state
    startTimer();
  }
}

function startBreak() {
    console.log('Break started');
    state.timer.currentBreakStart = Date.now();
    state.timer.display.style.backgroundColor = '#DC2626'; // Red color during break

    // Log "on break" status to Google Sheets (Break Status sheet)
    logToGoogleSheet("on break");

    // Set a timeout to log break time if it crosses one minute
    state.timer.breakTimeout = setTimeout(() => {
        const breakDuration = (Date.now() - state.timer.currentBreakStart) / 1000; // Current break duration in seconds
        if (breakDuration >= 600) {
            logBreakTime(breakDuration); // Log break time to Slack Trigger Sheet
        }
    }, 600000); // Trigger after 1 minute
}

function endBreak() {
    if (state.timer.currentBreakStart) {
        const breakDuration = (Date.now() - state.timer.currentBreakStart) / 1000;
        console.log('Break ended, duration:', breakDuration);
        state.timer.breaks.push(breakDuration);
        state.timer.totalBreakTime += breakDuration;
        state.timer.currentBreakStart = null;

        // Reset timer display color
        state.timer.display.style.backgroundColor = COLORS.primary;

        // Clear the 1-minute timeout if the break ends before 1 minute
        if (state.timer.breakTimeout) {
            clearTimeout(state.timer.breakTimeout);
            state.timer.breakTimeout = null;
        }

        // Log "active" status to Google Sheets (Break Status sheet)
        logToGoogleSheet("active");
    }
}

function logBreakTime(breakDuration) {
    // Get the current timestamp in "YYYY-MM-DD HH:MM:SS" format
    const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);

    // Extract the agent's initials from the avatar element
    const avatarElement = document.querySelector('.MuiAvatar-root.MuiAvatar-circular.MuiAvatar-colorDefault');

    if (avatarElement) {
        // Access the text content of the avatar to get the initials
        const agentInitials = avatarElement.textContent.trim();
        console.log('Agent Initials:', agentInitials); // Logs the initials (e.g., "MB")
    } else {
        console.error('Avatar element not found!');
        return; // Exit early if avatar element is not found
    }

    // Replace with your Web App URL
    const sheetEndpoint = "https://script.google.com/macros/s/AKfycbzqzymoOVct0gRPYr_RYiZmAYM4VPfUSd0FZWrI6PElOV5O4fIiz7nQ8_t_zBchDFlk/exec";

    // Create the POST payload as JSON, including agent initials, break duration, and timestamp
    const postData = {
        agentName: agentInitials,  // Use initials instead of full name
        status: "on break",        // Status should be on break for Slack Trigger sheet
        break_duration: Math.floor(breakDuration), // Log whole seconds
        timestamp: timestamp      // Timestamp when break time was logged
    };

    // Send the request using GM_xmlhttpRequest
    GM_xmlhttpRequest({
        method: "POST",
        url: sheetEndpoint,
        headers: {
            "Content-Type": "application/json" // Specify JSON format
        },
        data: JSON.stringify(postData), // Convert payload to JSON string
        onload: function(response) {
            if (response.status === 200) {
                console.log(`Logged break time successfully for ${agentInitials} at ${timestamp}. Duration: ${breakDuration} seconds.`);
            } else {
                console.error(`Failed to log break time. HTTP status: ${response.status}, Message: ${response.statusText}`);
            }
        },
        onerror: function(error) {
            console.error("Error logging break time:", error);
        }
    });
}

function logToGoogleSheet(status) {
    // Get the current timestamp in "YYYY-MM-DD HH:MM:SS" format
    const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);

    // Extract the agent's initials from the avatar element (for status logging)
    const avatarElement = document.querySelector('.MuiAvatar-root.MuiAvatar-circular.MuiAvatar-colorDefault');
    const agentInitials = avatarElement ? avatarElement.textContent.trim() : 'Unknown Agent';
    console.log('Agent Initials:', agentInitials); // Logs the initials (e.g., "MB")

    // Replace with your Web App URL
    const sheetEndpoint = "https://script.google.com/macros/s/AKfycbzqzymoOVct0gRPYr_RYiZmAYM4VPfUSd0FZWrI6PElOV5O4fIiz7nQ8_t_zBchDFlk/exec";

    // Create the POST payload as JSON, including agent initials, status, and timestamp
    const postData = {
        agentName: agentInitials,  // Use initials instead of full name
        status: status,           // "on break" or "active"
        timestamp: timestamp,     // Timestamp when status was logged
    };

    // Send the request using GM_xmlhttpRequest
    GM_xmlhttpRequest({
        method: "POST",
        url: sheetEndpoint,
        headers: {
            "Content-Type": "application/json" // Specify JSON format
        },
        data: JSON.stringify(postData), // Convert payload to JSON string
        onload: function(response) {
            if (response.status === 200) {
                console.log(`Logged status '${status}' successfully for ${agentInitials} at ${timestamp}.`);
            } else {
                console.error(`Failed to log status. HTTP status: ${response.status}, Message: ${response.statusText}`);
            }
        },
        onerror: function(error) {
            console.error("Error logging status:", error);
        }
    });
}


  function detectDisposition() {
    const button = document.querySelector('button.oz-tb-callback-dialog-save-button.oz-tb-call-end-diposition-button');
    if (button) {
      if (!state.disposition.buttonFound) {
        state.disposition.buttonFound = true;
        state.disposition.startTime = Date.now();
        return;
      }
      const elapsedTime = Date.now() - state.disposition.startTime;
      if (elapsedTime >= DISPOSITION_WARNING_TIME && !state.disposition.notificationSent) {
        showNotification();
        state.disposition.notificationSent = true;
      }
      if (elapsedTime >= DISPOSITION_ALERT_TIME && !state.disposition.audioPlayed) {
        playDispositionAlert();
      }
    } else {
      resetDispositionState();
    }
  }

  function playDispositionAlert() {
    state.disposition.audio.load();
    state.disposition.audio.play().catch(error => console.error('Audio playback failed:', error));
    state.disposition.audioPlayed = true;
  }

  function resetDispositionState() {
    Object.assign(state.disposition, {
      buttonFound: false,
      notificationSent: false,
      audioPlayed: false,
      startTime: null
    });
  }

  function showNotification() {
    if (!("Notification" in window)) return;
    Notification.requestPermission()
      .then(permission => {
        if (permission === "granted") {
          new Notification("Disposition Alert", {
            body: "Disposition Pending!",
            icon: 'https://example.com/icon.png'
          }).onclick = function() {
            window.focus();
            this.close();
          };
        }
      })
      .catch(error => console.error('Notification error:', error));
  }

  function updateStatsDisplay() {
    if (!state.timer.showingStats) return;
    const breakHistory = state.timer.breaks.map((breakTime, index) => `
      <div style="padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.1)">
        <span style="color: rgba(255,255,255,0.7)">Break ${index + 1}:</span>
        <span style="float: right">${formatTime(Math.floor(breakTime))}</span>
      </div>
    `).join('');
    document.getElementById('breakHistoryDisplay').innerHTML = breakHistory ||
      '<div style="color: rgba(255,255,255,0.6)">No breaks taken yet.</div>';
    document.getElementById('totalBreakTime').textContent = formatTime(Math.floor(state.timer.totalBreakTime));
  }

  // Initialize the script
  initialize();
})();