Add Employee Recognition feature to notifications system

- Add Recognition notification type (ID 5) with blue color
- Add employeesso field to notifications table
- Create carousel display for Recognition on shopfloor dashboard
- Show employee names (lookup from wjf_employees) instead of SSO
- Auto-set starttime to NOW and endtime to 4AM next day
- Auto-enable shopfloor display for Recognition type
- Add Achievements tab to employee profile (displayprofile.asp)
- Hide Recognition from calendar view
- Add lookupemployee.asp AJAX endpoint for name preview
- Fix datetime double-formatting bug in save/update files
- Fix URL parameter loading on shopfloor dashboard init

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-01-09 07:27:37 -05:00
parent dd8729393f
commit 28e8071570
10 changed files with 938 additions and 51 deletions

View File

@@ -3,6 +3,9 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>West Jefferson - Events & Notifications</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -391,6 +394,122 @@
font-style: italic;
}
/* Recognition carousel styles */
.recognition-section {
margin-bottom: 20px;
}
.section-title.recognition {
background: #0d6efd;
color: #fff;
box-shadow: 0 4px 20px rgba(13, 110, 253, 0.4);
}
.recognition-carousel-container {
position: relative;
overflow: hidden;
width: 100%;
}
.recognition-carousel-wrapper {
position: relative;
width: 100%;
min-height: 150px;
}
.recognition-card {
background: linear-gradient(135deg, #1e3a5f 0%, #0d2137 100%);
border: 3px solid #0d6efd;
border-radius: 12px;
padding: 25px 30px;
box-shadow: 0 4px 20px rgba(13, 110, 253, 0.3);
transition: transform 0.8s ease-in-out, opacity 0.8s ease-in-out;
width: 100%;
}
.recognition-card:not(.active) {
position: absolute;
top: 0;
left: 0;
}
.recognition-card.active {
transform: translateY(0);
opacity: 1;
position: relative;
}
.recognition-card.exit-up {
transform: translateY(-100%);
opacity: 0;
}
.recognition-card.enter-down {
transform: translateY(100%);
opacity: 0;
}
.recognition-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.recognition-star {
font-size: 48px;
color: #ffc107;
margin-right: 20px;
text-shadow: 0 0 10px rgba(255, 193, 7, 0.5);
animation: starPulse 2s ease-in-out infinite;
}
@keyframes starPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.recognition-employee {
font-size: 32px;
font-weight: 700;
color: #fff;
}
.recognition-achievement {
font-size: 24px;
color: #ccc;
line-height: 1.4;
margin-left: 68px;
}
.recognition-counter {
position: absolute;
top: 15px;
right: 20px;
background: rgba(0, 0, 0, 0.5);
padding: 5px 12px;
border-radius: 15px;
font-size: 14px;
color: #fff;
}
.recognition-dots {
text-align: center;
margin-top: 15px;
}
.recognition-dots .dot {
display: inline-block;
width: 10px;
height: 10px;
background: #444;
border-radius: 50%;
margin: 0 5px;
}
.recognition-dots .dot.active {
background: #0d6efd;
}
/* Upcoming events carousel */
.upcoming-carousel-container {
position: relative;
@@ -553,6 +672,33 @@
font-size: 14px;
padding: 10px;
}
.recognition-card {
padding: 15px 20px;
}
.recognition-star {
font-size: 28px;
margin-right: 12px;
}
.recognition-employee {
font-size: 18px;
}
.recognition-achievement {
font-size: 14px;
margin-left: 40px;
}
.recognition-counter {
font-size: 10px;
padding: 3px 8px;
}
.recognition-carousel-wrapper {
min-height: 100px;
}
}
/* ============================================ */
@@ -670,6 +816,39 @@
font-size: 48px;
padding: 30px;
}
.recognition-card {
padding: 50px 60px;
}
.recognition-star {
font-size: 96px;
margin-right: 40px;
}
.recognition-employee {
font-size: 64px;
}
.recognition-achievement {
font-size: 48px;
margin-left: 136px;
}
.recognition-counter {
font-size: 28px;
padding: 10px 24px;
}
.recognition-carousel-wrapper {
min-height: 300px;
}
.recognition-dots .dot {
width: 20px;
height: 20px;
margin: 0 10px;
}
}
</style>
</head>
@@ -719,6 +898,12 @@
let isTransitioning = false;
let pendingDataRender = null;
// Recognition carousel state
let recognitionEvents = [];
let currentRecognitionIndex = 0;
let recognitionCarouselInterval = null;
let isRecognitionTransitioning = false;
// Get business unit from URL parameter
function getBusinessUnitFromURL() {
const urlParams = new URLSearchParams(window.location.search);
@@ -816,18 +1001,23 @@
const container = document.getElementById('eventsContainer');
let html = '';
// Current Events - show all in grid layout
if (data.current && data.current.length > 0) {
// Separate Recognition events from other current events
const recognitions = data.current ? data.current.filter(e => e.typecolor === 'recognition') : [];
const otherCurrentEvents = data.current ? data.current.filter(e => e.typecolor !== 'recognition') : [];
// Current Events - show all in grid layout (excluding Recognition) - FIRST priority
if (otherCurrentEvents.length > 0) {
// Split into active and resolved events
const activeEvents = data.current.filter(e => !e.resolved);
const resolvedEvents = data.current.filter(e => e.resolved);
const activeEvents = otherCurrentEvents.filter(e => !e.resolved);
const resolvedEvents = otherCurrentEvents.filter(e => e.resolved);
// Sort function: by type priority (Incident > Change > Awareness > TBD), then by date (earliest first)
const typePriority = {
'Incident': 1,
'Change': 2,
'Awareness': 3,
'TBD': 4
'Recognition': 4,
'TBD': 5
};
const sortByTypeAndDate = (a, b) => {
@@ -868,6 +1058,7 @@
? `<a href="${getServiceNowUrl(event.ticketnumber)}" target="_blank" class="event-ticket">${escapeHtml(event.ticketnumber)}</a>`
: '';
const resolvedIndicator = event.resolved ? '<div class="resolved-indicator">RESOLVED</div>' : '';
const endTimeText = event.resolved
? `<br><strong>Resolved:</strong> ${formatDateTime(event.endtime)}`
: event.endtime ? `<br><strong>Ends:</strong> ${formatDateTime(event.endtime)}` : '<br><strong>Status:</strong> Ongoing (no end time)';
@@ -887,11 +1078,9 @@
<div>${ticketBadge}</div>
<div class="event-header">
<div class="event-title">${escapeHtml(event.notification)}</div>
</div>
<div class="event-time">
<strong>Started:</strong> ${formatDateTime(event.starttime)}
${endTimeText}
<strong>Started:</strong> ${formatDateTime(event.starttime)}${endTimeText}
</div>
</div>
`;
@@ -901,6 +1090,54 @@
html += '</div>'; // Close section
}
// Recognition Carousel Section - AFTER current events, BEFORE upcoming
if (recognitions.length > 0) {
recognitionEvents = recognitions;
// Preserve current index if valid
if (currentRecognitionIndex >= recognitions.length) {
currentRecognitionIndex = 0;
}
html += '<div class="recognition-section">';
html += '<div class="section-title recognition">EMPLOYEE RECOGNITION</div>';
html += '<div class="recognition-carousel-container">';
html += '<div class="recognition-carousel-wrapper" id="recognitionCarousel">';
recognitions.forEach((event, index) => {
const activeClass = index === currentRecognitionIndex ? 'active' : 'enter-down';
const displayName = event.employeename || event.employeesso || 'Employee';
console.log('Recognition event:', event);
console.log('employeename:', event.employeename, 'employeesso:', event.employeesso, 'displayName:', displayName);
html += `
<div class="recognition-card ${activeClass}" data-index="${index}">
${recognitions.length > 1 ? `<div class="recognition-counter">${index + 1} of ${recognitions.length}</div>` : ''}
<div class="recognition-header">
<span class="recognition-star">&#9733;</span>
<span class="recognition-employee">${escapeHtml(displayName)}</span>
</div>
<div class="recognition-achievement">${escapeHtml(event.notification)}</div>
</div>
`;
});
html += '</div>'; // Close carousel wrapper
// Add dots if multiple recognitions
if (recognitions.length > 1) {
html += '<div class="recognition-dots">';
recognitions.forEach((_, index) => {
html += `<span class="dot ${index === currentRecognitionIndex ? 'active' : ''}" data-index="${index}"></span>`;
});
html += '</div>';
}
html += '</div>'; // Close carousel container
html += '</div>'; // Close section
} else {
recognitionEvents = [];
}
// Upcoming Events - carousel with slide-up transition
if (data.upcoming && data.upcoming.length > 0) {
// Sort upcoming events by type priority, then by date
@@ -908,7 +1145,8 @@
'Incident': 1,
'Change': 2,
'Awareness': 3,
'TBD': 4
'Recognition': 4,
'TBD': 5
};
const sortedUpcoming = [...data.upcoming].sort((a, b) => {
@@ -975,12 +1213,19 @@
container.innerHTML = html;
// Start carousel if there are upcoming events
// Start carousels if there are events
if (upcomingEvents.length > 1) {
startUpcomingCarousel();
} else {
stopUpcomingCarousel();
}
// Start recognition carousel if there are multiple recognitions
if (recognitionEvents.length > 1) {
startRecognitionCarousel();
} else {
stopRecognitionCarousel();
}
}
// Start the upcoming events carousel
@@ -1059,6 +1304,78 @@
currentUpcomingIndex = nextIndex;
}
// Start the recognition carousel
function startRecognitionCarousel() {
console.log(`Recognition Carousel: Starting with ${recognitionEvents.length} recognitions`);
// Clear any existing interval
stopRecognitionCarousel();
// Start new interval (5 seconds)
recognitionCarouselInterval = setInterval(rotateRecognitionEvent, 5000);
}
// Stop the recognition carousel
function stopRecognitionCarousel() {
if (recognitionCarouselInterval) {
clearInterval(recognitionCarouselInterval);
recognitionCarouselInterval = null;
}
}
// Rotate to next recognition
function rotateRecognitionEvent() {
if (recognitionEvents.length <= 1) {
return;
}
const carousel = document.getElementById('recognitionCarousel');
if (!carousel) {
return;
}
const items = carousel.querySelectorAll('.recognition-card');
if (items.length === 0) {
return;
}
// Mark transition as active
isRecognitionTransitioning = true;
const currentItem = items[currentRecognitionIndex];
const nextIndex = (currentRecognitionIndex + 1) % recognitionEvents.length;
const nextItem = items[nextIndex];
// Slide current item up and out
currentItem.classList.remove('active');
currentItem.classList.add('exit-up');
// Slide next item up from bottom
nextItem.classList.remove('enter-down');
nextItem.classList.add('active');
// Update counter if present
const counter = nextItem.querySelector('.recognition-counter');
if (counter) {
counter.textContent = `${nextIndex + 1} of ${recognitionEvents.length}`;
}
// Update dots
const dots = document.querySelectorAll('.recognition-dots .dot');
dots.forEach((dot, i) => {
dot.classList.toggle('active', i === nextIndex);
});
// After transition, reset the exited item for next rotation
setTimeout(() => {
currentItem.classList.remove('exit-up');
currentItem.classList.add('enter-down');
isRecognitionTransitioning = false;
}, 800);
currentRecognitionIndex = nextIndex;
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
@@ -1079,6 +1396,7 @@
'success': '#0ad64f', // Green (GE Avionics Green) - Awareness, TBD
'warning': '#ffc107', // Yellow - Change
'danger': '#dc3545', // Red - Incident
'recognition': '#0d6efd', // Blue - Recognition
'secondary': '#6c757d' // Gray - fallback
};
return colorMap[typecolor] || colorMap['secondary'];
@@ -1195,7 +1513,7 @@
}
// Initialize dashboard
function init() {
async function init() {
// Display fiscal week
updateFiscalWeek();
@@ -1203,8 +1521,11 @@
updateClock();
clockInterval = setInterval(updateClock, 1000);
// Set business unit from URL BEFORE first fetch
selectedBusinessUnit = getBusinessUnitFromURL();
// Load business units dropdown (only once)
loadBusinessUnits();
await loadBusinessUnits();
// Initial data fetch
fetchNotifications();
@@ -1219,6 +1540,7 @@
clearInterval(updateInterval);
clearInterval(clockInterval);
stopUpcomingCarousel();
stopRecognitionCarousel();
} else {
// Resume updates without reinitializing everything
updateClock();