// ==UserScript==
// @name AtoZ OnCall Allowance Sheet Generator & Work Statistics
// @namespace http://tampermonkey.net/
// @version 2.6
// @description Combined tool for OnCall allowance management and work statistics from AtoZ
// @author wmehedis
// @match https://atoz.amazon.work/time/balance-ledger/TimeOff_ATPLUS_DE_CF_PaidAbsenceFlexDismantlingHour
// @grant GM_xmlhttpRequest
// @connect oncall-api.corp.amazon.com
// @connect atoz-apps.amazon.work
// ==/UserScript==
(function () {
'use strict';
const styles = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
.punch-column-header {
font-weight: 600;
text-align: center;
padding: 12px;
font-family: 'Inter', sans-serif;
}
.combined-panel {
position: fixed !important;
right: 0 !important;
top: 63px !important;
z-index: 9999 !important;
height: 100vh;
width: 320px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
backdrop-filter: blur(10px);
box-shadow: -10px 0 30px rgba(0,0,0,0.15);
border-left: 1px solid rgba(255,255,255,0.2);
display: flex;
flex-direction: column;
overflow: hidden;
font-family: 'Inter', sans-serif;
}
.punch-cell {
font-size: 12px;
text-align: center;
padding: 8px;
font-family: 'Inter', sans-serif;
margin: 2px;
}
.punch-card {
display: inline-block;
background: rgba(255,255,255,0.9);
border: 1px solid rgba(255,255,255,0.3);
border-radius: 12px;
padding: 6px 12px;
margin: 2px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
backdrop-filter: blur(5px);
transition: all 0.3s ease;
}
.punch-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.punch-out {
color: #e74c3c;
font-weight: 600;
}
.punch-in {
color: #27ae60;
font-weight: 600;
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 18px;
scrollbar-width: thin;
scrollbar-color: rgba(0,0,0,0.2) transparent;
}
.panel-content::-webkit-scrollbar {
width: 6px;
}
.panel-content::-webkit-scrollbar-track {
background: transparent;
}
.panel-content::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.2);
border-radius: 3px;
}
.section {
background: rgba(255,255,255,0.8);
border-radius: 16px;
padding: 12px;
margin-bottom: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.2);
transition: all 0.3s ease;
}
.section:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
}
.oncall-input {
width: calc(50% - 8px) !important;
margin: 8px 4px;
padding: 12px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 12px;
background: rgba(255,255,255,0.9);
font-family: 'Inter', sans-serif;
font-size: 14px;
transition: all 0.3s ease;
}
.oncall-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
transform: translateY(-1px);
}
.action-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 14px 20px;
border-radius: 12px;
cursor: pointer;
margin-top: 12px;
width: 100%;
font-family: 'Inter', sans-serif;
font-weight: 600;
font-size: 14px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.action-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
}
.action-button:active {
transform: translateY(0);
}
.date-range {
background: rgba(255,255,255,0.6);
padding: 16px;
border-radius: 12px;
margin-bottom: 12px;
border: 1px solid rgba(255,255,255,0.3);
backdrop-filter: blur(5px);
}
.shift-type-container {
margin-bottom: 12px;
font-size: 13px;
}
.shift-type-container label {
display: inline-flex;
align-items: center;
cursor: pointer;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.2s ease;
font-weight: 500;
}
.shift-type-container label:hover {
background: rgba(102, 126, 234, 0.1);
}
.shift-type-container input[type="radio"] {
margin-right: 8px;
accent-color: #667eea;
}
.main-content {
margin-right: 320px;
}
.nav-header {
margin-bottom: 18px;
padding-bottom: 15px;
border-bottom: 2px solid rgba(255,255,255,0.3);
text-align: center;
}
.nav-header h3 {
margin: 0;
color: #2d3748;
font-family: 'Inter', sans-serif;
font-weight: 700;
font-size: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.content-wrapper {
margin-bottom: 20px;
}
.footer {
padding: 15px;
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(255,255,255,0.2);
text-align: center;
font-size: 11px;
color: #666;
flex-shrink: 0;
width: 100%;
}
.creator-text {
color: #666;
display: inline-block;
font-weight: 500;
}
.creator-text a {
color: #667eea;
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
}
.creator-text a:hover {
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
padding: 4px 8px;
margin: -4px -8px;
transform: translateY(-1px);
}
.feature-request {
color: #667eea;
text-decoration: none;
font-size: 12px;
display: block;
text-align: center;
margin-top: 8px;
font-weight: 500;
transition: all 0.3s ease;
}
.feature-request:hover {
color: #5a67d8;
transform: translateY(-1px);
}
h4, h5 {
font-family: 'Inter', sans-serif;
font-weight: 700;
color: #2d3748;
margin-bottom: 10px;
font-size: 14px;
}
@keyframes flash {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.3; }
}
input, select {
font-family: 'Inter', sans-serif;
}
.stats-card {
background: rgba(255,255,255,0.7);
border-radius: 12px;
padding: 8px;
margin-bottom: 6px;
border: 1px solid rgba(255,255,255,0.3);
backdrop-filter: blur(5px);
transition: all 0.3s ease;
}
.stats-card:hover {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
z-index: 99998;
opacity: 0;
transition: all 0.3s ease;
}
.modal-backdrop.show {
opacity: 1;
}
`;
function formatTime(dateTime) {
let time = new Date(dateTime).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: 'Europe/Berlin'
});
return time === '00:00' ? '24:00' : time;
}
function getPunchesForDate(punchData, date) {
if (!punchData || !punchData.punchesTimeSegments) return [];
return punchData.punchesTimeSegments.filter(p =>
new Date(p.startDateTime).toDateString() === date.toDateString()
);
}
function injectStyles() {
const styleSheet = document.createElement("style");
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
}
function fetchAPI(url) {
// For internal Amazon URLs, use regular fetch
if (url.includes('atoz-apps.amazon.work')) {
return fetch(url, {
headers: {
'accept': '/',
'x-atoz-client-id': 'ATOZ_TIMEOFF_SERVICE'
},
credentials: 'include'
}).then(response => response.json());
}
// For OnCall API, use GM_xmlhttpRequest
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
credentials: 'include',
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
resolve(data);
} catch (e) {
reject(new Error('Failed to parse response'));
}
} else {
reject(new Error(`API call failed: ${response.status} ${response.statusText}`));
}
},
onerror: function(error) {
reject(new Error('Network error occurred'));
}
});
});
}
async function getUserTeams(loginId) {
try {
const teams = await fetchAPI(`https://oncall-api.corp.amazon.com/teams?q=members:'${loginId}' OR owners:'${loginId}'`);
return teams;
} catch (error) {
console.error('Error fetching teams:', error);
return [];
}
}
async function fetchPunchData() {
try {
console.log('Attempting to fetch punch data...');
let employeeId = null;
// Method 1: Try to get from URL
const urlParams = new URLSearchParams(window.location.search);
employeeId = urlParams.get('employeeId');
// Method 2: Try to get from page content
if (!employeeId) {
const pageContent = document.documentElement.innerHTML;
const matches = pageContent.match(/employeeId["']?\s*:\s*["']([A-Z0-9]+)["']/i) ||
pageContent.match(/employee_id["']?\s*:\s*["']([A-Z0-9]+)["']/i) ||
pageContent.match(/userId["']?\s*:\s*["']([A-Z0-9]+)["']/i);
if (matches && matches[1]) {
employeeId = matches[1];
console.log('Found employee ID from page content:', employeeId);
}
}
// Method 3: Try to get from meta tags
if (!employeeId) {
const metaTags = document.getElementsByTagName('meta');
for (let tag of metaTags) {
if (tag.getAttribute('name') === 'employee-id' ||
tag.getAttribute('name') === 'user-id') {
employeeId = tag.getAttribute('content');
break;
}
}
}
// Method 4: Try to get from localStorage
if (!employeeId) {
try {
const stored = localStorage.getItem('AtoZContext');
if (stored) {
const parsed = JSON.parse(stored);
employeeId = parsed?.employee?.employeeId;
}
} catch (e) {
console.log('Error reading from localStorage:', e);
}
}
// If still no employee ID, try one last method
if (!employeeId) {
const scripts = document.getElementsByTagName('script');
for (let script of scripts) {
const content = script.textContent;
const match = content.match(/employeeId["']?\s*:\s*["']([A-Z0-9]+)["']/i);
if (match) {
employeeId = match[1];
break;
}
}
}
if (!employeeId) {
console.error('Failed to find employee ID using all methods');
return null;
}
console.log('Employee ID found:', employeeId);
const endDate = new Date();
const startDate = new Date();
startDate.setMonth(startDate.getMonth() - 2);
const url = `https://atoz-apps.amazon.work/apis/AtoZTimeoffService/punches?employeeId=${employeeId}&startDate=${startDate.toISOString().split('T')[0]}&endDate=${endDate.toISOString().split('T')[0]}`;
console.log('Fetching punch data from URL:', url);
const response = await fetch(url, {
headers: {
'accept': '*/*',
'x-atoz-client-id': 'ATOZ_TIMEOFF_SERVICE'
},
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Punch data received successfully');
return data;
} catch (error) {
console.error('Error in fetchPunchData:', error);
return null;
}
}
function parseDate(text) {
const match = text.match(/(\d+)\.\s+(\w+\.?)\s+(\d{4})/) || text.match(/(\w+)\s+(\d+),\s+(\d{4})/);
if (!match) return null;
const [_, part1, part2, year] = match;
const day = part1.length <= 2 ? part1 : part2;
const monthName = (part1.length <= 2 ? part2 : part1).replace('.', '');
const months = {
Jan: '01', January: '01',
Feb: '02', February: '02',
Mar: '03', March: '03',
Apr: '04', April: '04',
May: '05',
Jun: '06', June: '06',
Jul: '07', July: '07',
Aug: '08', August: '08',
Sep: '09', September: '09',
Oct: '10', October: '10',
Nov: '11', November: '11',
Dec: '12', December: '12'
};
const month = months[monthName];
if (!month) return null;
return new Date(`${year}-${month}-${day.padStart(2, '0')}`);
}
function addPunchColumnHeader(table) {
const headerRow = table.querySelector('thead tr');
const th = document.createElement('th');
th.className = 'punch-column-header';
th.textContent = 'Punch Times';
headerRow.appendChild(th);
}
function calculateWorkHours(inTime, outTime) {
const start = new Date(`2000-01-01 ${inTime}`);
const end = new Date(`2000-01-01 ${outTime}`);
let totalHours = (end - start) / (1000 * 60 * 60);
// Apply break time deductions
if (totalHours < 6) {
// No break deduction for less than 6 hours
} else if (totalHours >= 9) {
// 45 min break for 9+ hours
totalHours -= 0.75;
} else if (totalHours >= 6) {
// 30 min break for 6+ hours
totalHours -= 0.5;
}
return Math.round(totalHours * 100) / 100;
}
function exportToExcel(punchData) {
const rows = [
['Date', 'Punch In', 'Punch Out', 'Hours Worked']
];
const punchesByDate = {};
punchData.punchesTimeSegments.forEach(punch => {
const date = new Date(punch.startDateTime).toLocaleDateString();
if (!punchesByDate[date]) {
punchesByDate[date] = [];
}
punchesByDate[date].push(punch);
});
Object.entries(punchesByDate).forEach(([date, punches]) => {
if (punches.length > 0) {
const inTime = formatTime(punches[0].startDateTime);
const outTime = formatTime(punches[punches.length - 1].endDateTime || punches[punches.length - 1].startDateTime);
const hours = calculateWorkHours(inTime, outTime);
rows.push([date, inTime, outTime, hours.toFixed(2)]);
}
});
const csvContent = rows.map(row => row.join(',')).join('\n');
downloadCSV(csvContent, 'AtoZ_punch_history.csv');
}
function downloadCSV(csvContent, filename) {
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function createCombinedPanel(punchData) {
// Adjust the main content area
const mainContent = document.querySelector('.container');
if (mainContent) {
mainContent.classList.add('main-content');
}
const panel = document.createElement('div');
panel.className = 'combined-panel';
panel.innerHTML = `
<div class="panel-content">
<div class="nav-header">
<h3>⚡ AtoZ Assistant</h3>
</div>
<div class="content-wrapper">
<div class="section">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
<h4 style="margin: 0;">🕰️ Today's Time</h4>
<button id="today-info-btn" style="background: #667eea; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-weight: bold; transition: all 0.3s ease;">i</button>
</div>
<div id="today-punch-content">
<div class="stats-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 400; color: #4a5568; font-size: 14px;">🟢 In</span>
<span id="punch-in-time" style="font-weight: 600; color: #27ae60; font-size: 14px;">--:--:--</span>
</div>
</div>
<div class="stats-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 400; color: #4a5568; font-size: 14px;">⏳ To 8h</span>
<span id="time-to-eight" style="font-weight: 600; color: #f39c12; font-size: 14px;">--h --m <span id="to-eight-seconds" style="font-size: 13px; opacity: 0.7;">0s</span></span>
</div>
</div>
<div class="stats-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 400; color: #4a5568; font-size: 14px;">⏱️ Worked</span>
<span id="worked-time" style="font-weight: 600; color: #f39c12; font-size: 14px;">0h 0m <span id="worked-seconds" style="font-size: 13px; opacity: 0.7;">0s</span></span>
</div>
</div>
</div>
</div>
<div class="section">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
<h4 style="margin: 0;">📈 My Work Stats</h4>
<button id="break-info-btn" style="background: #667eea; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; font-size: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-weight: bold; transition: all 0.3s ease;">i</button>
</div>
<div id="stats-content">
<div class="stats-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 500; color: #4a5568;">🗓️ This Week</span>
<span id="weekly-hours" style="font-weight: 700; color: #667eea; font-size: 18px;">0.00</span>
</div>
</div>
<div class="stats-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 500; color: #4a5568;">📊 This Month</span>
<span id="monthly-hours" style="font-weight: 700; color: #667eea; font-size: 18px;">0.00</span>
</div>
</div>
<button id="export-punches" class="action-button">📊 Export Work Report</button>
</div>
</div>
<div class="section">
<h4>🏖️ Time Off Balance</h4>
<div id="balance-content">
<div class="stats-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 500; color: #4a5568;">🌴 Vacation Days</span>
<span id="vacation-balance" style="font-weight: 700; color: #27ae60; font-size: 18px;">--</span>
</div>
</div>
<div class="stats-card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 500; color: #4a5568;">⏰ Overtime Hours</span>
<span id="overtime-balance" style="font-weight: 700; color: #f39c12; font-size: 18px;">--</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
<span class="creator-text">💡 Built by <a href="https://phonetool.amazon.com/users/wmehedis" target="_blank">@wmehedis</a></span>
<a href="mailto:[email protected]?subject=Feature Request - AtoZ Work Assistant"
class="feature-request">
🚀 Suggest Feature
</a>
</div>
`;
document.body.appendChild(panel);
// Initialize dates
const now = new Date();
const startDate = new Date(now.getFullYear(), now.getMonth(), 1);
const endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0);
// Initialize dates (removed date range functionality)
// Initialize work statistics
updateWorkStatistics(punchData);
// Update today's punch display
updateTodaysPunchDisplay(punchData);
startPunchTimeUpdater();
// Fetch time off balances
fetchTimeOffBalances(punchData);
// Add event listeners
document.getElementById('export-punches').onclick = () => exportToExcel(punchData);
// Add break info button handler
document.getElementById('break-info-btn').onclick = () => {
alert('Break Time Calculation:\n\n• Less than 6 hours: No break deducted\n• 6+ hours: 30 minutes break deducted\n• 9+ hours: 45 minutes break deducted\n\nThis follows German labor law requirements.');
};
// Add today's time info button handler
document.getElementById('today-info-btn').onclick = () => {
alert('Today\'s Time Information:\n\n• In: Your punch-in time for today\n• To 8h: Time remaining to reach 8 hours or overtime if over 8h\n• Worked: Total hours worked today with break time deducted\n\nColor coding:\n• Orange: Less than 8 hours worked\n• Green: 8-9 hours worked (optimal)\n• Red flashing: Over 9 hours worked (alert)');
};
// Fetch time off balances
fetchTimeOffBalances(punchData);
// Update font styles for consistency
updateFontStyles();
}
function updateWorkStatistics(punchData) {
const now = new Date();
const weekStart = new Date(now.setDate(now.getDate() - now.getDay()));
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
let weeklyHours = 0;
let monthlyHours = 0;
punchData.punchesTimeSegments.forEach(punch => {
const punchDate = new Date(punch.startDateTime);
const inTime = formatTime(punch.startDateTime);
const outTime = formatTime(punch.endDateTime || punch.startDateTime);
const hours = calculateWorkHours(inTime, outTime);
if (punchDate >= weekStart) {
weeklyHours += hours;
}
if (punchDate >= monthStart) {
monthlyHours += hours;
}
});
document.getElementById('weekly-hours').textContent = formatHoursMinutes(weeklyHours);
document.getElementById('monthly-hours').textContent = formatHoursMinutes(monthlyHours);
}
async function fetchTimeOffBalances(punchData) {
try {
const employeeId = getEmployeeIdFromPage();
if (!employeeId) return;
const now = new Date().toISOString();
const url = `https://atoz-apps.amazon.work/apis/TAAPI/v1/time-away/balances?employeeId=${employeeId}&asOfDateTime=${now}&asOfDateTimeTimezone=UTC`;
const response = await fetch(url, {
headers: {
'accept': '*/*',
'x-atoz-client-id': 'ATOZ_TIMEOFF_SERVICE'
},
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
updateBalanceDisplay(data);
}
} catch (error) {
console.error('Balance fetch error:', error);
}
}
function updateBalanceDisplay(balanceData) {
let vacationDays = '--';
let overtimeHours = '--';
if (balanceData?.balances) {
balanceData.balances.forEach(balance => {
if (balance.balanceName.includes('VacationHours')) {
vacationDays = balance.availableBalance.toFixed(1);
} else if (balance.balanceName.includes('FlexDismantlingHour')) {
overtimeHours = balance.availableBalance.toFixed(2);
}
});
}
document.getElementById('vacation-balance').textContent = vacationDays;
document.getElementById('overtime-balance').textContent = overtimeHours === '--' ? '--' : formatHoursMinutes(parseFloat(overtimeHours));
}
function formatHoursMinutes(totalHours) {
const hours = Math.floor(totalHours);
const minutes = Math.round((totalHours - hours) * 60);
return `${hours}h ${minutes}m`;
}
function updateTodaysPunchDisplay(punchData) {
const today = new Date();
const todayPunches = getPunchesForDate(punchData, today);
if (todayPunches.length > 0) {
const punchIn = new Date(todayPunches[0].startDateTime);
const punchOut = todayPunches[todayPunches.length - 1].endDateTime ?
new Date(todayPunches[todayPunches.length - 1].endDateTime) : null;
document.getElementById('punch-in-time').textContent = formatTimeWithSeconds(punchIn);
if (punchOut) {
const hours = calculateWorkHours(formatTime(punchIn), formatTime(punchOut));
document.getElementById('worked-time').textContent = formatHoursMinutes(hours);
updateTimeToEight(hours, true);
} else {
updateTimeToEight(0, false);
}
}
}
function formatTimeWithSeconds(dateTime) {
return new Date(dateTime).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: 'Europe/Berlin'
});
}
function updateTimeToEight(currentHours, isPunchedOut) {
const timeToEightElement = document.getElementById('time-to-eight');
if (currentHours >= 8) {
const overtime = currentHours - 8;
const overtimeFormatted = formatHoursMinutes(overtime);
timeToEightElement.innerHTML = `<span style="font-size: 12px;">+${overtimeFormatted}</span>`;
timeToEightElement.style.color = '#27ae60';
} else {
const remaining = 8 - currentHours;
timeToEightElement.textContent = formatHoursMinutes(remaining);
timeToEightElement.style.color = '#f39c12';
}
}
let startTime = null;
let alertSoundPlayed = false;
function startPunchTimeUpdater() {
setInterval(() => {
const punchInElement = document.getElementById('punch-in-time');
const workedElement = document.getElementById('worked-time');
if (punchInElement && punchInElement.textContent !== '--:--:--') {
const today = new Date();
const todayPunches = getPunchesForDate(window.punchData, today);
const isPunchedOut = todayPunches.length > 0 && todayPunches[todayPunches.length - 1].endDateTime;
if (!isPunchedOut) {
if (!startTime) {
const punchInTime = punchInElement.textContent;
const [hours, minutes, seconds] = punchInTime.split(':');
startTime = new Date();
startTime.setHours(parseInt(hours), parseInt(minutes), parseInt(seconds));
}
const now = new Date();
const workedMs = now - startTime;
const totalSeconds = Math.floor(workedMs / 1000);
const totalHours = totalSeconds / 3600;
// Apply break time deductions for display
let adjustedHours = totalHours;
if (totalHours >= 9) {
adjustedHours -= 0.75; // 45 min break
} else if (totalHours >= 6) {
adjustedHours -= 0.5; // 30 min break
}
// Calculate hours and minutes from adjusted time
const hours = Math.floor(adjustedHours);
const minutes = Math.floor((adjustedHours - hours) * 60);
const seconds = totalSeconds % 60;
// Color coding and alerts based on hours worked
let color = '#f39c12'; // Orange for <8h
let animation = '';
if (totalHours >= 9) {
color = '#e74c3c'; // Red for 9h+
animation = 'flash 1s infinite';
// Sound alert at 9h 45m
if (totalHours >= 9.75 && !alertSoundPlayed) {
playAlertSound();
showNotification('Work Time Alert', 'You have worked 9h 45m. Remember to punch out!');
alertSoundPlayed = true;
}
} else if (totalHours >= 8) {
color = '#27ae60'; // Green for 8-9h
}
workedElement.innerHTML = `${hours}h ${minutes}m <span style="font-size: 13px; opacity: 0.7;">${seconds}s</span>`;
workedElement.style.color = color;
workedElement.style.animation = animation;
updateTimeToEightWithSeconds(totalSeconds);
}
}
}, 1000);
}
function playAlertSound() {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1);
oscillator.frequency.setValueAtTime(800, audioContext.currentTime + 0.2);
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
}
function showNotification(title, message) {
if ('Notification' in window) {
if (Notification.permission === 'granted') {
new Notification(title, { body: message, icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><text y="18" font-size="18">⏰</text></svg>' });
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
new Notification(title, { body: message });
}
});
}
}
// Fallback: Browser alert
setTimeout(() => alert(`${title}: ${message}`), 100);
}
function updateTimeToEightWithSeconds(totalWorkedSeconds) {
const timeToEightElement = document.getElementById('time-to-eight');
const eightHoursInSeconds = 8 * 3600;
if (totalWorkedSeconds >= eightHoursInSeconds) {
const overtimeSeconds = totalWorkedSeconds - eightHoursInSeconds;
const hours = Math.floor(overtimeSeconds / 3600);
const minutes = Math.floor((overtimeSeconds % 3600) / 60);
const seconds = overtimeSeconds % 60;
timeToEightElement.innerHTML = `<span style="font-size: 12px;">+${hours}h ${minutes}m <span style="font-size: 10px; opacity: 0.7; animation: pulse 0.5s ease-in-out;">${seconds}s</span></span>`;
timeToEightElement.style.color = '#27ae60';
} else {
const remainingSeconds = eightHoursInSeconds - totalWorkedSeconds;
const hours = Math.floor(remainingSeconds / 3600);
const minutes = Math.floor((remainingSeconds % 3600) / 60);
const seconds = remainingSeconds % 60;
timeToEightElement.innerHTML = `${hours}h ${minutes}m <span style="font-size: 12px; opacity: 0.7; animation: pulse 0.5s ease-in-out;">${seconds}s</span>`;
timeToEightElement.style.color = '#f39c12';
}
}
function updateFontStyles() {
// Update Work Stats section labels and values
document.querySelectorAll('.stats-card').forEach(card => {
const label = card.querySelector('span:first-child');
const value = card.querySelector('span:last-child');
if (label) {
label.style.fontWeight = '400';
label.style.fontSize = '14px';
}
if (value) {
value.style.fontWeight = '600';
value.style.fontSize = '14px';
}
});
// Specific IDs
['weekly-hours', 'monthly-hours', 'vacation-balance', 'overtime-balance'].forEach(id => {
const element = document.getElementById(id);
if (element) {
element.style.fontWeight = '600';
element.style.fontSize = '14px';
}
});
}
function getEmployeeIdFromPage() {
const urlParams = new URLSearchParams(window.location.search);
let employeeId = urlParams.get('employeeId');
if (!employeeId) {
const pageContent = document.documentElement.innerHTML;
const matches = pageContent.match(/employeeId["']?\s*:\s*["']([A-Z0-9]+)["']/i);
if (matches && matches[1]) {
employeeId = matches[1];
}
}
return employeeId;
}
async function processTable() {
console.log('Starting processTable');
try {
// Find the table
const table = document.querySelector('table[data-test-component="StencilTable"]');
if (!table) {
console.log('Table not found');
return;
}
// Check if punch column already exists
if (!table.querySelector('.punch-column-header')) {
console.log('Found table, injecting styles');
injectStyles();
console.log('Adding punch column header');
addPunchColumnHeader(table);
}
console.log('Fetching punch data');
const punchData = await fetchPunchData();
if (!punchData) {
console.error('No punch data received');
return;
}
// Store punch data globally for use in OnCall sheet generation
window.punchData = punchData;
console.log('Punch data received and stored globally:', punchData);
console.log('Creating combined panel');
// Remove existing panel if it exists
const existingPanel = document.querySelector('.combined-panel');
if (existingPanel) {
existingPanel.remove();
}
createCombinedPanel(punchData);
console.log('Adding OnCall button');
injectOnCallButton();
console.log('Processing table rows');
const rows = table.querySelectorAll('tbody tr');
rows.forEach((row, index) => {
try {
const dateCell = row.querySelector('.css-1vslykb');
const dateText = dateCell?.querySelector('.css-1kgbsl4')?.textContent;
console.log(`Processing row ${index}, date: ${dateText}`);
const date = parseDate(dateText);
const cell = document.createElement('td');
cell.className = 'punch-cell';
if (date) {
const punches = getPunchesForDate(punchData, date);
console.log(`Found ${punches.length} punches for ${dateText}`);
if (punches.length > 0) {
const inTime = formatTime(punches[0].startDateTime);
const outTime = formatTime(
punches[punches.length - 1].endDateTime ||
punches[punches.length - 1].startDateTime
);
cell.innerHTML = `
<span class="punch-card">
In: <span class="punch-in">${inTime}</span>
</span>
<span class="punch-card">
Out: <span class="punch-out">${outTime}</span>
</span>
`;
} else {
cell.textContent = '—';
}
} else {
console.log(`Invalid date for row ${index}: ${dateText}`);
cell.textContent = 'Invalid';
}
// Check if cell already exists
const existingCell = row.querySelector('.punch-cell');
if (existingCell) {
console.log(`Replacing existing cell for row ${index}`);
row.replaceChild(cell, existingCell);
} else {
console.log(`Adding new cell for row ${index}`);
row.appendChild(cell);
}
} catch (rowError) {
console.error(`Error processing row ${index}:`, rowError);
}
});
// Update work statistics
console.log('Updating work statistics');
try {
updateWorkStatistics(punchData);
} catch (statsError) {
console.error('Error updating work statistics:', statsError);
}
// Add mutation observer to handle dynamic updates
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
const newRows = Array.from(mutation.addedNodes)
.filter(node => node.nodeName === 'TR');
if (newRows.length > 0) {
console.log('New rows detected, updating punch data');
newRows.forEach((row, index) => {
try {
const dateCell = row.querySelector('.css-1vslykb');
const dateText = dateCell?.querySelector('.css-1kgbsl4')?.textContent;
if (dateText) {
const date = parseDate(dateText);
if (date) {
const punches = getPunchesForDate(punchData, date);
const cell = document.createElement('td');
cell.className = 'punch-cell';
if (punches.length > 0) {
const inTime = formatTime(punches[0].startDateTime);
const outTime = formatTime(
punches[punches.length - 1].endDateTime ||
punches[punches.length - 1].startDateTime
);
cell.innerHTML = `
<span class="punch-card">
In: <span class="punch-in">${inTime}</span>
</span>
<span class="punch-card">
Out: <span class="punch-out">${outTime}</span>
</span>
`;
} else {
cell.textContent = '—';
}
row.appendChild(cell);
}
}
} catch (error) {
console.error(`Error processing new row ${index}:`, error);
}
});
}
}
});
});
// Start observing the table body
const tbody = table.querySelector('tbody');
if (tbody) {
observer.observe(tbody, {
childList: true,
subtree: true
});
}
// Store the observer in a global variable so it can be disconnected if needed
window.tableObserver = observer;
console.log('Table processing complete');
} catch (error) {
console.error('Error in processTable:', error);
}
}
// Add event listener for DOM changes
const observeDOM = (function() {
const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
return function(obj, callback) {
if (!obj || obj.nodeType !== 1) return;
if (MutationObserver) {
const obs = new MutationObserver((mutations, observer) => {
callback(mutations);
});
obs.observe(obj, {
childList: true,
subtree: true
});
} else if (window.addEventListener) {
obj.addEventListener('DOMNodeInserted', callback, false);
obj.addEventListener('DOMNodeRemoved', callback, false);
}
};
})();
// Initialize the script with retry mechanism
function injectOnCallDialog() {
const existingDialog = document.getElementById("oncall-dialog");
if (existingDialog) return;
const dialog = document.createElement("div");
dialog.id = "oncall-dialog";
dialog.style.cssText = `
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: white;
border: 1px solid #ccc;
border-radius: 16px;
padding: 30px;
z-index: 99999;
width: 850px;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
font-family: 'Inter', sans-serif;
backdrop-filter: blur(10px);
opacity: 0;
transform: translateX(-50%) translateY(-20px);
transition: all 0.3s ease;
`;
dialog.innerHTML = `
<div style="position: sticky; top: 0; background: white; z-index: 100; display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; padding: 15px; border-radius:15px; border-bottom: 2px solid #f1f5f9; box-shadow: 0 2px 10px rgba(0,0,0,0.1);">
<h3 style="margin: 0; font-family: 'Inter', sans-serif; font-weight: 700; color: #2d3748; font-size: 26px;">📋 My OnCall Schedule</h3>
<button id="close-oncall-dialog" style="background: linear-gradient(135deg, #ff6b6b, #ee5a52); color: white; border: none; font-size: 18px; cursor: pointer; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);">×</button>
</div>
<div style="font-family: 'Inter', sans-serif; padding: 0 5px;">
<div style="margin-bottom: 25px;">
<label style="display: block; margin-bottom: 10px; font-weight: 600; color: #4a5568; font-size: 15px;">👤 Login ID:</label>
<input type="text" id="oncall-loginId" placeholder="Enter your Amazon login" style="width: 100%; margin-bottom: 18px; padding: 14px; border: 2px solid #e2e8f0; border-radius: 12px; font-family: 'Inter', sans-serif; font-size: 14px; transition: all 0.3s ease;">
</div>
<div style="margin-bottom: 25px;">
<label style="display: block; margin-bottom: 10px; font-weight: 600; color: #4a5568; font-size: 15px;">📆 Month:</label>
<input type="month" id="oncall-monthSelect" style="width: 100%; margin-bottom: 18px; padding: 14px; border: 2px solid #e2e8f0; border-radius: 12px; font-family: 'Inter', sans-serif; font-size: 14px; transition: all 0.3s ease;">
</div>
<div style="margin: 25px 0; padding: 25px; background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1)); border-radius: 16px; border: 1px solid rgba(102, 126, 234, 0.2); transition: all 0.3s ease;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px;">
<h4 style="margin: 0; font-weight: 600; color: #2d3748; font-size: 16px;">⚙️ Manual Time Override</h4>
<label class="switch">
<input type="checkbox" id="manual-override-toggle">
<span class="slider round"></span>
</label>
</div>
<div id="manual-override-section" style="display: none;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 25px;">
<div style="background: rgba(255,255,255,0.8); padding: 18px; border-radius: 12px; transition: all 0.3s ease; border: 1px solid rgba(255,255,255,0.5);">
<label style="display: block; margin-bottom: 10px; font-weight: 600; color: #4a5568; font-size: 14px;">🌅 Early Shift:</label>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<input type="text" id="early-shift-start" placeholder="02:30" style="width: 85px; padding: 10px; border: 2px solid #e2e8f0; border-radius: 8px; font-family: 'Inter', sans-serif; transition: all 0.3s ease;" maxlength="5">
<input type="text" id="early-shift-end" placeholder="13:00" style="width: 85px; padding: 10px; border: 2px solid #e2e8f0; border-radius: 8px; font-family: 'Inter', sans-serif; transition: all 0.3s ease;" maxlength="5">
</div>
<small style="color: #718096; font-size: 12px; margin-top: 5px; display: block;">24hr format (e.g., 02:30)</small>
</div>
<div style="background: rgba(255,255,255,0.8); padding: 18px; border-radius: 12px; transition: all 0.3s ease; border: 1px solid rgba(255,255,255,0.5);">
<label style="display: block; margin-bottom: 10px; font-weight: 600; color: #4a5568; font-size: 14px;">🌙 Late Shift:</label>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<input type="text" id="late-shift-start" placeholder="13:00" style="width: 85px; padding: 10px; border: 2px solid #e2e8f0; border-radius: 8px; font-family: 'Inter', sans-serif; transition: all 0.3s ease;" maxlength="5">
<input type="text" id="late-shift-end" placeholder="22:00" style="width: 85px; padding: 10px; border: 2px solid #e2e8f0; border-radius: 8px; font-family: 'Inter', sans-serif; transition: all 0.3s ease;" maxlength="5">
</div>
<small style="color: #718096; font-size: 12px; margin-top: 5px; display: block;">24hr format (e.g., 13:00)</small>
</div>
</div>
</div>
</div>
<div style="display: flex; gap: 15px; margin-top: 30px;">
<button id="oncall-fetch" style="flex: 1; padding: 16px 24px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 12px; font-family: 'Inter', sans-serif; font-weight: 600; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); font-size: 15px;">🔍 Load My Schedule</button>
<button id="download-oncall-sheet" style="flex: 1; padding: 16px 24px; background: linear-gradient(135deg, #48bb78 0%, #38a169 100%); color: white; border: none; border-radius: 12px; font-family: 'Inter', sans-serif; font-weight: 600; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(72, 187, 120, 0.3); font-size: 15px;" disabled>💾 Export Timesheet</button>
</div>
</div>
<div id="oncall-debug" style="font-size: 13px; margin-top: 25px; background: rgba(255,255,255,0.9); padding: 18px; border-radius: 12px; border: 1px solid #e2e8f0; font-family: 'Inter', sans-serif;"></div>
<div id="oncall-results" style="margin-top: 25px;"></div>
`;
// Add CSS for the toggle switch and hover effects
const style = document.createElement('style');
style.textContent = `
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #667eea;
}
input:checked + .slider:before {
transform: translateX(26px);
}
#oncall-dialog input:hover {
border-color: #667eea;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
}
#oncall-dialog button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.2);
}
#close-oncall-dialog:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4);
}
.manual-override-section > div:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
`;
document.head.appendChild(style);
// Create backdrop
const backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop';
backdrop.onclick = () => {
backdrop.classList.remove('show');
dialog.style.opacity = '0';
dialog.style.transform = 'translateX(-50%) translateY(-20px)';
setTimeout(() => {
backdrop.remove();
dialog.remove();
}, 300);
};
document.body.appendChild(backdrop);
document.body.appendChild(dialog);
// Animate in
setTimeout(() => {
backdrop.classList.add('show');
dialog.style.opacity = '1';
dialog.style.transform = 'translateX(-50%) translateY(0)';
}, 10);
// Create state tracker
const dialogState = {
isOverrideEnabled: false
};
// Get elements
const toggleSwitch = document.getElementById("manual-override-toggle");
const overrideSection = document.getElementById("manual-override-section");
console.log("Initial elements state:", {
toggleSwitch: toggleSwitch,
overrideSection: overrideSection,
toggleChecked: toggleSwitch?.checked,
dialogState: dialogState
});
// Toggle event handler with state tracking
toggleSwitch.addEventListener('change', (event) => {
dialogState.isOverrideEnabled = event.target.checked;
console.log("Toggle changed:", {
checked: event.target.checked,
previousDisplay: overrideSection.style.display,
dialogState: dialogState
});
overrideSection.style.display = event.target.checked ? "block" : "none";
console.log("After display change:", {
newDisplay: overrideSection.style.display,
sectionVisible: overrideSection.offsetParent !== null,
dialogState: dialogState
});
});
// Add time format validation
function validateTimeInput(input) {
input.addEventListener('input', function() {
let value = this.value;
if (value.length === 2 && !value.includes(':')) {
this.value = value + ':';
}
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
if (value && !timeRegex.test(value)) {
this.style.borderColor = 'red';
} else {
this.style.borderColor = '';
}
});
}
// Apply validation to all time inputs
['early-shift-start', 'early-shift-end', 'late-shift-start', 'late-shift-end'].forEach(id => {
validateTimeInput(document.getElementById(id));
});
// Download button handler with state check
document.getElementById("download-oncall-sheet").addEventListener('click', () => {
// Read toggle state directly from element
const isOverrideEnabled = toggleSwitch.checked;
console.log("Download button clicked, override enabled:", isOverrideEnabled);
// Create config using actual toggle state
const config = {
startHour: 6,
isOverrideEnabled: isOverrideEnabled
};
// If override is enabled, collect the time values
if (isOverrideEnabled) {
config.overrideValues = {
earlyShift: {
start: document.getElementById('early-shift-start').value || '02:30',
end: document.getElementById('early-shift-end').value || '13:00'
},
lateShift: {
start: document.getElementById('late-shift-start').value || '13:00',
end: document.getElementById('late-shift-end').value || '22:00'
}
};
}
console.log("Generated config:", config);
generateOnCallSheetFromAPI(
window.oncallScheduleData,
window.punchData,
document.getElementById("oncall-monthSelect").value,
config
);
});
// Close dialog handler
document.getElementById("close-oncall-dialog").onclick = () => {
backdrop.classList.remove('show');
dialog.style.opacity = '0';
dialog.style.transform = 'translateX(-50%) translateY(-20px)';
setTimeout(() => {
backdrop.remove();
dialog.remove();
}, 300);
};
// Add hover effects to manual override cards
const overrideCards = dialog.querySelectorAll('#manual-override-section > div > div');
overrideCards.forEach(card => {
card.classList.add('manual-override-section');
});
// Set initial month value
document.getElementById("oncall-monthSelect").value = new Date().toISOString().slice(0, 7);
// Fetch button handler
document.getElementById("oncall-fetch").onclick = () => {
const loginId = document.getElementById("oncall-loginId").value.trim();
const month = document.getElementById("oncall-monthSelect").value;
const debugDiv = document.getElementById("oncall-debug");
const resultsDiv = document.getElementById("oncall-results");
// Validation with animated feedback
if (!loginId) {
debugDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 10px; padding: 10px; background: linear-gradient(135deg, rgba(255, 193, 7, 0.1), rgba(255, 152, 0, 0.1)); border-radius: 12px; animation: shake 0.5s;">
<span style="font-size: 24px; animation: wiggle 1s infinite;">⚠️</span>
<span style="font-weight: 600; color: #f39c12;">Please enter your Login ID to continue</span>
</div>
<style>
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
@keyframes wiggle { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(-10deg); } 75% { transform: rotate(10deg); } }
</style>
`;
document.getElementById("oncall-loginId").focus();
return;
}
if (!month) {
debugDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 10px; padding: 10px; background: linear-gradient(135deg, rgba(255, 193, 7, 0.1), rgba(255, 152, 0, 0.1)); border-radius: 12px; animation: shake 0.5s;">
<span style="font-size: 24px; animation: wiggle 1s infinite;">📅</span>
<span style="font-weight: 600; color: #f39c12;">Please select which month to view</span>
</div>
`;
document.getElementById("oncall-monthSelect").focus();
return;
}
fetchScheduleData(loginId, month, debugDiv, resultsDiv);
};
}
function generateOnCallSheetFromAPI(scheduleData, punchData, monthValue, config) {
// Set default configuration if not provided
const defaultConfig = {
date: new Date().toLocaleDateString(),
startHour: 6,
isOverrideEnabled: false,
overrideValues: 'disabled'
};
// Merge provided config with defaults
const finalConfig = { ...defaultConfig, ...config };
console.log("Processing with configuration:", finalConfig);
const [year, month] = monthValue.split('-').map(num => parseInt(num));
// Headers for the CSV
const rows = [
['Date', '', 'One Shift', '', 'Double Shift', '', 'Notes'],
['', '', 'From', 'To', 'From', 'To', ''],
['', '', '(00:00)', '(24:00)', '(00:00)', '(24:00)', '']
];
// Create a map of dates and their shifts
const dateShifts = new Map();
const lastDay = new Date(year, month, 0).getDate();
// Initialize all dates in the month
for (let day = 1; day <= lastDay; day++) {
const date = new Date(year, month - 1, day);
const dateStr = date.toLocaleDateString('en-US', {
month: 'numeric',
day: 'numeric',
year: 'numeric'
});
dateShifts.set(dateStr, {
oneShiftFrom: '',
oneShiftTo: '',
doubleShiftFrom: '',
doubleShiftTo: '',
notes: ''
});
}
// Process schedule data with multi-day shift handling
scheduleData.forEach(entry => {
if (entry.oncallShift) {
const startDate = new Date(entry.oncallShift.startDateTime);
const endDate = new Date(entry.oncallShift.endDateTime);
const startHour = startDate.getHours();
// Calculate all dates for this shift
const dates = [];
let currentDate = new Date(startDate);
const endHour = endDate.getHours();
let isMidnightCrossing = startHour >= 13 && (endHour <= 6 || endDate.getDate() !== startDate.getDate());
// Check for manual override midnight crossing
if (finalConfig.isOverrideEnabled && finalConfig.overrideValues && startHour >= 13) {
const lateEndTime = finalConfig.overrideValues.lateShift.end;
const [endH] = lateEndTime.split(':').map(Number);
if (endH <= 6) {
isMidnightCrossing = true;
}
}
if (isMidnightCrossing && endDate.getDate() !== startDate.getDate()) {
// For midnight crossing shifts, add both start and end dates
if (startDate.getMonth() === month - 1) dates.push(new Date(startDate));
if (endDate.getMonth() === month - 1) dates.push(new Date(endDate));
} else {
while (currentDate <= endDate) {
if (currentDate.getMonth() === month - 1) {
dates.push(new Date(currentDate));
}
currentDate.setDate(currentDate.getDate() + 1);
}
}
// Process each date in the shift
dates.forEach((date, index) => {
const dateStr = date.toLocaleDateString('en-US', {
month: 'numeric',
day: 'numeric',
year: 'numeric'
});
const shiftData = dateShifts.get(dateStr);
if (!shiftData) return;
const isStartDate = index === 0;
const isEndDate = index === dates.length - 1;
const isMultiDayShift = dates.length > 1;
// isMidnightCrossing already calculated above
if (finalConfig.isOverrideEnabled && finalConfig.overrideValues) {
const overrides = finalConfig.overrideValues;
if (isMultiDayShift) {
if (isStartDate) {
// First day: use override start time or API time
const startTime = startHour < 13 ? overrides.earlyShift.start : overrides.lateShift.start;
shiftData.oneShiftFrom = startTime;
shiftData.oneShiftTo = '24:00:00';
} else if (isEndDate) {
// Last day: 00:00 to punch in or API end time
const punches = getPunchesForDate(punchData, date);
shiftData.oneShiftFrom = '0:00';
if (punches.length > 0) {
const punchIn = new Date(punches[0].startDateTime);
const apiEnd = new Date(endDate);
const earlierTime = punchIn < apiEnd ? punchIn : apiEnd;
shiftData.oneShiftTo = formatTime(earlierTime);
} else {
shiftData.oneShiftTo = formatTime(endDate);
}
} else {
// Middle days: 00:00 to 24:00
shiftData.oneShiftFrom = '0:00';
shiftData.oneShiftTo = '24:00:00';
}
} else if (isMidnightCrossing && isStartDate) {
// Late shift crossing midnight with override - use punch out time
const punches = getPunchesForDate(punchData, startDate);
if (punches.length > 0) {
const punchOut = new Date(punches[punches.length - 1].endDateTime);
shiftData.doubleShiftFrom = formatTime(punchOut);
} else {
shiftData.doubleShiftFrom = '15:00';
shiftData.notes = 'No punch time found in AtoZ';
}
shiftData.doubleShiftTo = '24:00';
const nextDate = new Date(startDate);
nextDate.setDate(nextDate.getDate() + 1);
const nextDateStr = nextDate.toLocaleDateString('en-US', {
month: 'numeric',
day: 'numeric',
year: 'numeric'
});
const nextShiftData = dateShifts.get(nextDateStr);
if (nextShiftData) {
nextShiftData.oneShiftFrom = '00:00';
nextShiftData.oneShiftTo = overrides.lateShift.end;
}
} else {
// Single day shifts with override
const punches = getPunchesForDate(punchData, date);
if (startHour < 13) {
// Early shift with override
shiftData.oneShiftFrom = overrides.earlyShift.start;
if (punches.length > 0) {
const punchIn = new Date(punches[0].startDateTime);
shiftData.oneShiftTo = formatTime(punchIn);
} else {
shiftData.oneShiftTo = overrides.earlyShift.end;
shiftData.notes = 'No punch time found in AtoZ';
}
} else {
// Late shift with override
if (punches.length > 0) {
const punchOut = new Date(punches[punches.length - 1].endDateTime);
shiftData.oneShiftFrom = formatTime(punchOut);
} else {
shiftData.oneShiftFrom = overrides.lateShift.start;
shiftData.notes = 'No punch time found in AtoZ';
}
shiftData.oneShiftTo = overrides.lateShift.end;
}
}
} else {
if (isMultiDayShift) {
if (isStartDate) {
const punches = getPunchesForDate(punchData, startDate);
if (punches.length > 0) {
const punchIn = new Date(punches[0].startDateTime);
shiftData.oneShiftFrom = formatTime(startDate);
shiftData.oneShiftTo = '24:00:00';
} else {
shiftData.oneShiftFrom = formatTime(startDate);
shiftData.oneShiftTo = '24:00:00';
}
} else if (isEndDate) {
const punches = getPunchesForDate(punchData, endDate);
if (punches.length > 0) {
const punchIn = new Date(punches[0].startDateTime);
const oncallEnd = new Date(endDate);
const earlierTime = punchIn < oncallEnd ? punchIn : oncallEnd;
shiftData.oneShiftFrom = '0:00';
shiftData.oneShiftTo = formatTime(earlierTime) + ':00';
} else {
shiftData.oneShiftFrom = '0:00';
shiftData.oneShiftTo = formatTime(endDate) + ':00';
}
} else {
shiftData.oneShiftFrom = '0:00';
shiftData.oneShiftTo = '24:00:00';
}
} else if (isMidnightCrossing && isStartDate) {
// Late shift crossing midnight - use punch times
const punches = getPunchesForDate(punchData, startDate);
if (punches.length > 0) {
const punchOut = new Date(punches[punches.length - 1].endDateTime);
shiftData.doubleShiftFrom = formatTime(punchOut);
} else {
shiftData.doubleShiftFrom = '15:00';
shiftData.notes = 'No punch time found in AtoZ';
}
shiftData.doubleShiftTo = '24:00';
const nextDate = new Date(startDate);
nextDate.setDate(nextDate.getDate() + 1);
const nextDateStr = nextDate.toLocaleDateString('en-US', {
month: 'numeric',
day: 'numeric',
year: 'numeric'
});
const nextShiftData = dateShifts.get(nextDateStr);
if (nextShiftData) {
nextShiftData.oneShiftFrom = '00:00';
nextShiftData.oneShiftTo = formatTime(endDate);
}
} else {
const punches = getPunchesForDate(punchData, startDate);
if (startHour >= 13) {
if (punches.length > 0) {
const punchOut = new Date(punches[punches.length - 1].endDateTime);
shiftData.oneShiftFrom = formatTime(punchOut);
} else {
shiftData.oneShiftFrom = '15:00';
shiftData.notes = 'No punch time found in AtoZ';
}
shiftData.oneShiftTo = formatTime(endDate);
} else {
shiftData.oneShiftFrom = formatTime(startDate);
if (punches.length > 0) {
const punchIn = new Date(punches[0].startDateTime);
shiftData.oneShiftTo = formatTime(punchIn);
} else {
shiftData.oneShiftTo = formatTime(endDate);
shiftData.notes = 'No punch time found in AtoZ';
}
}
}
}
});
}
});
// Create rows in chronological order
const sortedDates = Array.from(dateShifts.keys()).sort((a, b) => new Date(a) - new Date(b));
sortedDates.forEach(dateStr => {
const shiftData = dateShifts.get(dateStr);
rows.push([
dateStr,
'',
shiftData.oneShiftFrom,
shiftData.oneShiftTo,
shiftData.doubleShiftFrom,
shiftData.doubleShiftTo,
shiftData.notes
]);
});
// Convert to TSV and download
const tsvContent = rows.map(row => row.join('\t')).join('\n');
const blob = new Blob([tsvContent], { type: 'text/tab-separated-values;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `Oncall_allowance_sheet_${monthValue}.xls`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
async function fetchScheduleData(loginId, monthValue, debugDiv, resultsDiv) {
const loadingMessages = [
{ emoji: '🚀', text: 'Starting fetch process...', progress: 10 },
{ emoji: '📅', text: 'Setting up date range...', progress: 20 },
{ emoji: '🔍', text: 'Searching for your teams...', progress: 40 },
{ emoji: '⚡', text: 'Found teams! Fetching schedules...', progress: 60 },
{ emoji: '📊', text: 'Processing data...', progress: 80 },
{ emoji: '✨', text: 'Almost done...', progress: 95 }
];
let messageIndex = 0;
let progressInterval;
let messageTimeout;
let isProcessing = true;
const updateProgress = (targetProgress, currentMessage) => {
if (!isProcessing) return;
let currentProgress = messageIndex > 0 ? loadingMessages[messageIndex - 1].progress : 0;
const increment = (targetProgress - currentProgress) / 20;
progressInterval = setInterval(() => {
if (!isProcessing) {
clearInterval(progressInterval);
return;
}
currentProgress += increment;
if (currentProgress >= targetProgress) {
currentProgress = targetProgress;
clearInterval(progressInterval);
}
// Update message and emoji for each step, but preserve progress elements
const existingProgressFill = document.getElementById('progress-fill');
const existingProgressText = document.getElementById('progress-text');
debugDiv.innerHTML = `
<div style="padding: 15px; background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1)); border-radius: 12px; animation: pulse 2s infinite;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px; min-height: 40px;">
<span id="debug-emoji" style="font-size: 24px; animation: debugBounce 1s infinite; display: inline-block; margin: 10px 0;">${currentMessage.emoji}</span>
<span id="debug-text" style="font-weight: 500; color: #4a5568;">${currentMessage.text}</span>
</div>
<div style="background: rgba(255,255,255,0.3); border-radius: 10px; height: 8px; overflow: hidden;">
<div id="progress-fill" style="background: linear-gradient(90deg, #667eea, #764ba2); height: 100%; width: ${currentProgress}%; transition: width 0.3s ease; border-radius: 10px;"></div>
</div>
<div id="progress-text" style="text-align: center; margin-top: 5px; font-size: 12px; color: #667eea; font-weight: 600;">${Math.round(currentProgress)}%</div>
</div>
<style>
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
@keyframes debugBounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
</style>
`;
// Update only progress during interval
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
if (progressFill && progressText) {
progressFill.style.width = `${currentProgress}%`;
progressText.textContent = `${Math.round(currentProgress)}%`;
}
// Also animate the results div icon with separate style
resultsDiv.innerHTML = `<div style="text-align: center; padding: 30px 20px; color: #667eea; font-weight: 500;">
<div style="min-height: 50px; display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px; animation: loadingBounce 1s infinite; display: inline-block;">🔄</span>
</div>
Loading your OnCall data...
<style>
@keyframes loadingBounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
</style>
</div>`;
}, 50);
};
const showNextMessage = () => {
if (messageIndex < loadingMessages.length && isProcessing) {
const msg = loadingMessages[messageIndex];
updateProgress(msg.progress, msg);
messageIndex++;
messageTimeout = setTimeout(showNextMessage, 1000);
}
};
showNextMessage();
resultsDiv.innerHTML = `<div style="text-align: center; padding: 30px 20px; color: #667eea; font-weight: 500;">
<div style="min-height: 50px; display: flex; align-items: center; justify-content: center; margin-bottom: 10px;">
<span style="font-size: 32px; animation: initialBounce 1s infinite; display: inline-block;">🔄</span>
</div>
Loading your OnCall data...
<style>
@keyframes initialBounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
</style>
</div>`;
try {
const [year, month] = monthValue.split('-').map(num => parseInt(num));
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const lastDay = new Date(year, month, 0).getDate();
const endDate = `${year}-${String(month).padStart(2, '0')}-${lastDay}`;
const teamsResponse = await getUserTeams(loginId);
if (!teamsResponse || teamsResponse.length === 0) {
isProcessing = false;
clearInterval(progressInterval);
clearTimeout(messageTimeout);
throw new Error('No teams found for this user');
}
const teamNames = teamsResponse.map(team => team.rawTeamName);
const scheduleUrl = `https://oncall-api.corp.amazon.com/teams/${teamNames.join(',')}/schedules/detailed?memberFilterList=${loginId}&from=${startDate}&to=${endDate}&timeZone=Europe/Berlin`;
const scheduleResponse = await fetchAPI(scheduleUrl);
// Store schedule data and enable download button
window.oncallScheduleData = scheduleResponse;
const downloadButton = document.getElementById('download-oncall-sheet');
downloadButton.disabled = false;
let allData = {};
scheduleResponse.forEach(entry => {
if (entry.oncallShift && entry.shiftDetails) {
const teamName = entry.shiftDetails.teamName;
const shift = entry.oncallShift;
if (shift.oncallMember && shift.oncallMember.includes(loginId)) {
if (!allData[teamName]) {
allData[teamName] = [];
}
const isDuplicate = allData[teamName].some(existingShift =>
existingShift.startDateTime === shift.startDateTime &&
existingShift.endDateTime === shift.endDateTime
);
if (!isDuplicate) {
allData[teamName].push({
startDateTime: shift.startDateTime,
endDateTime: shift.endDateTime,
oncallMember: [loginId],
shiftType: shift.shiftType || 'regular'
});
}
}
}
});
isProcessing = false;
clearInterval(progressInterval);
clearTimeout(messageTimeout);
debugDiv.innerHTML = `
<div style="padding: 15px; background: linear-gradient(135deg, rgba(39, 174, 96, 0.1), rgba(46, 125, 50, 0.1)); border-radius: 12px;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<span style="font-size: 24px; animation: celebration 2s infinite;">🎉</span>
<span style="font-weight: 600; color: #27ae60;">Success! Data loaded and calendar generated!</span>
</div>
<div style="background: rgba(255,255,255,0.3); border-radius: 10px; height: 8px; overflow: hidden;">
<div style="background: linear-gradient(90deg, #27ae60, #2ecc71); height: 100%; width: 100%; border-radius: 10px;"></div>
</div>
<div style="text-align: center; margin-top: 5px; font-size: 12px; color: #27ae60; font-weight: 600;">100% Complete! 🚀</div>
</div>
<style>
@keyframes celebration { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); } }
</style>
`;
if (Object.keys(allData).length > 0) {
let resultsHtml = `
<div style="font-family: Arial;">
<div style="display: flex; gap: 20px;">
<div style="flex: 1;">
${generateCalendarView(allData, month, year)}
</div>
</div>
</div>
`;
resultsDiv.innerHTML = resultsHtml;
} else {
resultsDiv.innerHTML = 'No shifts found for the selected user and period';
document.getElementById('download-oncall-sheet').disabled = true;
}
} catch (error) {
isProcessing = false;
clearInterval(progressInterval);
clearTimeout(messageTimeout);
// Determine error type and show appropriate message
let errorEmoji = '❌';
let errorTitle = 'Oops! Something went wrong';
let errorSuggestion = 'Please try again or check your connection';
if (error.message.includes('teams')) {
errorEmoji = '👥';
errorTitle = 'No teams found';
errorSuggestion = 'Make sure your Login ID is correct and you\'re part of an OnCall team';
} else if (error.message.includes('Network')) {
errorEmoji = '🌐';
errorTitle = 'Connection issue';
errorSuggestion = 'Check your internet connection and try again';
} else if (error.message.includes('API')) {
errorEmoji = '🔧';
errorTitle = 'Service temporarily unavailable';
errorSuggestion = 'The OnCall API might be down. Please try again later';
}
debugDiv.innerHTML = `
<div style="padding: 15px; background: linear-gradient(135deg, rgba(231, 76, 60, 0.1), rgba(192, 57, 43, 0.1)); border-radius: 12px; animation: shake 0.5s;">
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
<span style="font-size: 24px; animation: wiggle 1s infinite;">${errorEmoji}</span>
<span style="font-weight: 600; color: #e74c3c;">${errorTitle}</span>
</div>
<div style="background: rgba(255,255,255,0.3); border-radius: 10px; height: 8px; overflow: hidden;">
<div style="background: linear-gradient(90deg, #e74c3c, #c0392b); height: 100%; width: 100%; border-radius: 10px;"></div>
</div>
<div style="text-align: center; margin-top: 8px; font-size: 12px; color: #e74c3c; font-weight: 500;">${errorSuggestion}</div>
</div>
<style>
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
@keyframes wiggle { 0%, 100% { transform: rotate(0deg); } 25% { transform: rotate(-10deg); } 75% { transform: rotate(10deg); } }
</style>
`;
resultsDiv.innerHTML = `
<div style="text-align: center; padding: 20px; color: #e74c3c; font-weight: 500;">
<div style="font-size: 48px; margin-bottom: 10px;">😅</div>
<div>Don't worry, these things happen!</div>
<small style="color: #666;">Check the info above and give it another try</small>
</div>`;
document.getElementById('download-oncall-sheet').disabled = true;
}
}
function injectOnCallButton() {
const panel = document.querySelector(".combined-panel .panel-content");
if (!panel || document.getElementById("oncall-show-btn")) return;
const btn = document.createElement("button");
btn.id = "oncall-show-btn";
btn.className = "action-button";
btn.textContent = "📋 View My OnCall Schedule";
btn.onclick = injectOnCallDialog;
panel.appendChild(btn);
}
// Add this at the top of your script, right after 'use strict'
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
function generateCalendarView(allData, month, year) {
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
const daysInMonth = lastDay.getDate();
let calendarHtml = `
<div style="margin-bottom: 30px; background: linear-gradient(135deg, rgba(255,255,255,0.9), rgba(248,250,252,0.9)); border-radius: 20px; padding: 25px; box-shadow: 0 10px 40px rgba(0,0,0,0.1); backdrop-filter: blur(10px);">
<h4 style="text-align: center; margin-bottom: 25px; font-family: 'Inter', sans-serif; font-weight: 700; font-size: 24px; color: #2d3748;">📅 ${new Date(year, month - 1).toLocaleString('default', { month: 'long', year: 'numeric' })}</h4>
<div style="display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; margin-bottom: 8px;">
<div style="text-align: center; font-weight: 600; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px; font-family: 'Inter', sans-serif;">Mon</div>
<div style="text-align: center; font-weight: 600; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px; font-family: 'Inter', sans-serif;">Tue</div>
<div style="text-align: center; font-weight: 600; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px; font-family: 'Inter', sans-serif;">Wed</div>
<div style="text-align: center; font-weight: 600; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px; font-family: 'Inter', sans-serif;">Thu</div>
<div style="text-align: center; font-weight: 600; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px; font-family: 'Inter', sans-serif;">Fri</div>
<div style="text-align: center; font-weight: 600; padding: 12px; background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; border-radius: 12px; font-family: 'Inter', sans-serif;">Sat</div>
<div style="text-align: center; font-weight: 600; padding: 12px; background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; border-radius: 12px; font-family: 'Inter', sans-serif;">Sun</div>
</div>
<div style="display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px;">
`;
let firstDayOfWeek = firstDay.getDay() || 7;
for (let i = 1; i < firstDayOfWeek; i++) {
calendarHtml += `<div style="padding: 5px; background: #f5f5f5; min-height: 100px;"></div>`;
}
function formatTime(dateTimeStr) {
return new Date(dateTimeStr).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: 'Europe/Berlin'
});
}
function getDateKey(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
let shiftsByDate = {};
Object.entries(allData).forEach(([teamName, shifts]) => {
shifts.forEach(shift => {
const startDate = new Date(shift.startDateTime);
const endDate = new Date(shift.endDateTime);
// Calculate dates for the shift
const dates = [];
let currentDate = new Date(startDate);
// Add all dates between start and end (inclusive)
while (currentDate <= endDate) {
if (currentDate.getMonth() === month - 1) {
dates.push(new Date(currentDate));
}
currentDate.setDate(currentDate.getDate() + 1);
}
// Process each date
dates.forEach((date, index) => {
const dateKey = getDateKey(date);
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
const isMultiDayShift = dates.length > 1;
// Skip single-day shifts on weekends
if (isWeekend && !isMultiDayShift) return;
if (!shiftsByDate[dateKey]) {
shiftsByDate[dateKey] = [];
}
let timeDisplay;
const isStartDate = index === 0;
const isEndDate = index === dates.length - 1;
if (dates.length === 1) {
// Single day shift
timeDisplay = `${formatTime(startDate)} → ${formatTime(endDate)}`;
} else if (isStartDate) {
// First day of multi-day shift
timeDisplay = `${formatTime(startDate)} → 24:00`;
} else if (isEndDate) {
// Last day of multi-day shift
timeDisplay = `00:00 → ${formatTime(endDate)}`;
} else {
// Middle days of multi-day shift
timeDisplay = '00:00 → 24:00';
}
shiftsByDate[dateKey].push({
...shift,
teamName,
isMultiDay: isMultiDayShift,
displayTime: timeDisplay
});
});
});
});
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month - 1, day);
const dateKey = getDateKey(date);
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
const shiftsForDay = shiftsByDate[dateKey] || [];
let consolidatedShifts = {};
shiftsForDay.forEach(shift => {
const shiftKey = `${shift.displayTime}`;
if (!consolidatedShifts[shiftKey]) {
consolidatedShifts[shiftKey] = {
time: shift.displayTime,
teams: [shift.teamName],
isMultiDay: shift.isMultiDay
};
} else if (!consolidatedShifts[shiftKey].teams.includes(shift.teamName)) {
consolidatedShifts[shiftKey].teams.push(shift.teamName);
}
});
calendarHtml += `
<div style="padding: 12px; background: ${isWeekend ? 'linear-gradient(135deg, rgba(231, 76, 60, 0.05), rgba(192, 57, 43, 0.05))' : 'rgba(255,255,255,0.8)'};
min-height: 120px; border-radius: 12px; border: 1px solid ${isWeekend ? 'rgba(231, 76, 60, 0.2)' : 'rgba(226, 232, 240, 0.5)'}; overflow-y: auto; transition: all 0.3s ease;">
<div style="font-weight: 700; margin-bottom: 8px; color: ${isWeekend ? '#e74c3c' : '#2d3748'}; font-family: 'Inter', sans-serif; font-size: 16px;">
${day}
</div>
${Object.values(consolidatedShifts).map(shift => `
<div style="margin-bottom: 6px; padding: 8px;
background: ${shift.isMultiDay ? 'linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1))' : (isWeekend ? 'rgba(231, 76, 60, 0.1)' : 'rgba(248, 250, 252, 0.8)')};
border-radius: 8px; font-size: 11px; font-family: 'Inter', sans-serif;
${shift.isMultiDay ? 'border-left: 3px solid #667eea;' : 'border: 1px solid rgba(226, 232, 240, 0.5);'}
transition: all 0.2s ease;">
<div style="color: #667eea; font-weight: 600; margin-bottom: 2px;">${shift.teams.join(', ')}</div>
<div style="color: #4a5568; font-weight: 500;">${shift.time}</div>
</div>
`).join('')}
</div>
`;
}
calendarHtml += '</div></div></div>';
return calendarHtml;
}
function initialize() {
// Wrap the initialization code in a try-catch block
try {
let attempts = 0;
const maxAttempts = 30;
function tryInit() {
attempts++;
console.log(`Initialization attempt ${attempts}`);
const table = document.querySelector('table[data-test-component="StencilTable"]');
if (table) {
console.log('Table found, starting script');
processTable();
return;
}
if (attempts < maxAttempts) {
setTimeout(tryInit, 1000);
} else {
console.log('Max attempts reached, trying alternative initialization...');
const altTable = document.querySelector('table');
if (altTable) {
console.log('Found table with alternative selector, starting script');
processTable();
} else {
console.log('Script initialization failed completely');
}
}
}
setTimeout(tryInit, 2000);
} catch (error) {
console.error('Error during initialization:', error);
}
}
// SECOND TEST 1: Adding more comments
// SECOND TEST 2: Verifying code updates work
// SECOND TEST 3: This is the second batch
// SECOND TEST 4: File modification test
// SECOND TEST 5: End of file comments
// UPDATED: Fixed Excel output with Notes column and continuous shift handling
})();