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