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:
@@ -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">★</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();
|
||||
|
||||
Reference in New Issue
Block a user