Files
test/staff.html
2025-12-09 00:20:32 +00:00

279 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.13.1/jquery-ui.js" integrity="sha256-6XMVI0zB8cRzfZjqKcD01PBsAy3FlDASrlC8SxCpInY=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.1/themes/smoothness/jquery-ui.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Table Chart - Office Stats</title>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
background-color: #f9f9f9;
}
#main-container {
display: flex;
flex-direction: column;
}
h1 {
text-align: center;
margin: 5px 0;
}
#chart-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: auto; /* Allows scrolling for the whole chart if it's too big */
}
table {
border-collapse: collapse;
background-color: #fff;
border: 1px solid #ddd;
}
/* --- MODIFIED/ADDED CSS FOR CELL SIZING --- */
th, td {
border: 1px solid #ddd;
padding: 2px; /* Reduced padding for smaller cells */
text-align: center;
font-size: 10px; /* Reduced font size for compactness */
width: 35px; /* Set a fixed width */
height: 22px; /* Set a fixed height */
box-sizing: border-box; /* Ensures padding & border are included in width/height */
overflow: hidden; /* Hide content that overflows cell boundaries */
text-overflow: ellipsis; /* Show '...' for truncated text (horizontally) */
white-space: nowrap; /* Prevent text from wrapping to new lines */
}
th {
background-color: #f2f2f2;
/* Header cells (dates) will also adhere to the width/height above. */
/* Ellipsis will show if date strings are too long for 35px width. */
}
/* Special styling for the first column header cells (office names in tbody) */
tbody tr th:first-child {
width: auto; /* Allow this column to be wider based on content */
min-width: 70px; /* Set a minimum width for office names */
text-align: left; /* Align office names to the left for readability */
white-space: normal; /* Allow office names to wrap if they are too long */
text-overflow: clip; /* Default behavior for overflow when wrapping is normal */
padding-left: 5px; /* Add some left padding */
/* Height will still be 22px from the th,td rule. If more height needed, override here. */
}
td {
/* max-width: 50px; */ /* Superseded by width in th,td */
/* max-height: 10px; */ /* Typo corrected and superseded by height in th,td */
transition: opacity 0.2s;
}
/* --- END OF MODIFIED/ADDED CSS --- */
td:hover {
opacity: 0.7;
}
.tooltip {
position: absolute;
background-color: #333;
color: #fff;
padding: 5px 10px;
border-radius: 3px;
font-size: 12px; /* Tooltip font size can remain larger for readability */
pointer-events: none;
visibility: hidden;
max-width: 300px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<div id="main-container">
<h1>WFO Staffing Based on ICAM Account Status</h1>
<div id="chart-container"></div>
</div>
<div class="tooltip" id="tooltip"></div>
<script>
// Process data
function processData(data) {
const dates = data.map(item => {
const dt = new Date(item.provided_datetime);
return `${dt.getMonth() + 1}/${dt.getDate()}/${dt.getFullYear()}`;
});
const offices = [...new Set(data.flatMap(item => item.data.map(d => d.office)))];
const officeDateMap = {};
const titleBreakdownMap = {};
offices.forEach(office => {
officeDateMap[office] = {};
titleBreakdownMap[office] = {};
dates.forEach((date, i) => {
const item = data[i];
const officeData = item.data.find(d => d.office === office);
officeDateMap[office][date] = officeData ? parseInt(officeData.unique_person_count, 10) : null;
if (officeData && officeData.title_counts_array) {
try {
const titlesString = officeData.title_counts_array
.replace(/^{|}$/g, '')
.replace(/\\"/g, '"');
const titlesArray = titlesString
.split('","')
.map(str => str.replace(/^"|"$/g, ''))
.map(str => {
try {
return JSON.parse(str);
} catch (e) {
console.error(`Failed to parse individual title: ${str}`, e);
return null;
}
})
.filter(t => t !== null);
titleBreakdownMap[office][date] = titlesArray;
} catch (e) {
console.error(`Failed to parse title_counts_array for ${office} on ${date}:`, officeData.title_counts_array, e);
titleBreakdownMap[office][date] = null;
}
} else {
titleBreakdownMap[office][date] = null;
}
});
});
return { dates, offices, officeDateMap, titleBreakdownMap };
}
// Color calculation (Modified for better change visualization)
function getColor(count, firstCount) {
if (count === null) {
return '#e0e0e0';
}
if (firstCount === undefined || firstCount === null || (firstCount === 0 && count === 0)) {
return 'hsl(0, 0%, 92%)';
}
if (firstCount === 0) {
if (count > 0) {
return 'hsl(120, 70%, 50%)';
}
else {
return 'hsl(0, 0%, 92%)';
}
}
const diff = (count - firstCount) / firstCount;
const maxExpectedChange = 0.5;
const intensity = Math.min(Math.abs(diff) / maxExpectedChange, 1.0);
let hue, saturation, lightness;
if (count < firstCount) {
hue = 0;
saturation = 70;
lightness = 92 - (42 * intensity);
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
} else if (count > firstCount) {
hue = 120;
saturation = 70;
lightness = 92 - (42 * intensity);
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
} else {
return 'hsl(0, 0%, 92%)';
}
}
// Generate table chart
function generateChart(dates, offices, officeDateMap, titleBreakdownMap) {
const table = document.createElement('table');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
const headerRow = document.createElement('tr');
headerRow.appendChild(document.createElement('th'));
dates.forEach(date => {
const th = document.createElement('th');
th.textContent = date;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
offices.forEach(office => {
const row = document.createElement('tr');
const officeCell = document.createElement('th'); // This will be styled by 'tbody tr th:first-child'
officeCell.textContent = office.split('/').pop();
row.appendChild(officeCell);
const firstCount = officeDateMap[office][dates[0]];
dates.forEach(date => {
const count = officeDateMap[office][date];
const titles = titleBreakdownMap[office][date];
const td = document.createElement('td');
td.textContent = count !== null ? count : '';
td.style.backgroundColor = getColor(count, firstCount);
td.addEventListener('mouseover', (e) => {
const tooltip = document.getElementById('tooltip');
tooltip.style.visibility = 'visible';
if (titles && titles.length > 0) {
const breakdown = titles.map(t => `${t.otitle}: ${t.count}`).join('\n');
tooltip.textContent = `${office} on ${date}\n${breakdown}`;
} else {
tooltip.textContent = `${office} on ${date}: No data`;
}
tooltip.style.left = `${e.pageX + 10}px`;
tooltip.style.top = `${e.pageY - 10}px`;
});
td.addEventListener('mouseout', () => {
document.getElementById('tooltip').style.visibility = 'hidden';
});
row.appendChild(td);
});
tbody.appendChild(row);
});
table.appendChild(tbody);
const container = document.getElementById('chart-container');
container.innerHTML = '';
container.appendChild(table);
}
function fetchWeatherData(url, callback) {
$.getJSON(url, function(weatherdata) {
callback(weatherdata);
}).fail(function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching weather data:', textStatus, errorThrown);
callback(null);
});
}
function main() {
fetchWeatherData('https://wx.stoat.org/main.php?service=nws&officestats', function(weatherdata) {
if (weatherdata) {
const jsonData = weatherdata;
const { dates, offices, officeDateMap, titleBreakdownMap } = processData(jsonData);
generateChart(dates, offices, officeDateMap, titleBreakdownMap);
window.addEventListener('resize', () => {
// Note: Re-processing data on resize is fine for this dataset size,
// but for very large datasets, you might only regenerate the chart.
const { dates, offices, officeDateMap, titleBreakdownMap } = processData(jsonData);
generateChart(dates, offices, officeDateMap, titleBreakdownMap);
});
} else {
console.log('Failed to retrieve weather data.');
}
});
}
main();
</script>
</body>
</html>