New Ozonetel Timer Script

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
})();