Greasy Fork is available in English.

Ozonetel Timer Script

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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