Ozonetel Timer Script

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

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name    Ozonetel Timer Script
// @namespace  http://tampermonkey.net/
// @version   1.1.3
// @description Timer, break tracking, and disposition alerts for Ozonetel with statistics display
// @match    https://in-ccaas.ozonetel.com/*
// @grant    none
// @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: '#2563EB',   // Modern blue
    primaryHover: '#1D4ED8',
    secondary: '#1E40AF',
    text: '#FFFFFF',
    shadow: 'rgba(0, 0, 0, 0.2)'
  };

  // UI Config
  const UI_CONFIG = {
    timerDisplay: {
      styles: {
        position: 'fixed',
        bottom: '20px',
        left: '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',
        left: '10px',
        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',
        left: '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 icons = document.querySelectorAll('i.material-icons');
    let foundPlayIcon = false;
    let foundPauseIcon = false;
    icons.forEach(icon => {
      const iconText = icon.textContent?.trim();
      if (iconText?.includes("pause_circle_outline")) {
        foundPauseIcon = true;
      } else if (icon.classList.contains("ready") && iconText?.includes("play_circle_outline")) {
        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();
    // Visual indication of break status
    state.timer.display.style.backgroundColor = '#DC2626'; // Red color during break
  }

  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;
      updateStatsDisplay();
      // Reset timer display color
      state.timer.display.style.backgroundColor = COLORS.primary;
    }
  }

  function detectDisposition() {
    const button = document.querySelector('button.btn.btn-lg.btn-secondary.rounded-0');
    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();
})();