Add search result type filters and fix dashboard caching
- Add granular filter buttons for search results: Equipment, PCs, Servers, Switches, Access Points (instead of single "Machines" filter) - Fix network devices showing as "Machine" - now shows actual type (Server, Switch, Access Point, etc.) with appropriate icons - Add cache-busting timestamp to shopfloor dashboard API fetch to prevent stale data from browser/proxy caching - Fix search results page width (col-lg-auto to col-12) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<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>
|
||||
<title>West Jefferson Dashboard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||
@@ -23,13 +23,13 @@
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 20px 40px;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -174,7 +174,7 @@
|
||||
}
|
||||
|
||||
.events-section {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.events-grid {
|
||||
@@ -196,7 +196,7 @@
|
||||
.section-title {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 8px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
@@ -414,7 +414,6 @@
|
||||
.recognition-carousel-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.recognition-card {
|
||||
@@ -537,22 +536,16 @@
|
||||
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;
|
||||
.event-counter {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 15px;
|
||||
background: rgba(0, 0, 61, 0.7);
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Upcoming events carousel */
|
||||
@@ -565,7 +558,6 @@
|
||||
.upcoming-carousel-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.event-card.upcoming.carousel-item {
|
||||
@@ -578,6 +570,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.event-card.upcoming.carousel-item.active {
|
||||
@@ -596,13 +589,111 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Current events carousel (2-column pairs) */
|
||||
.current-carousel-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.current-carousel-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.current-carousel-pair {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
transition: transform 0.8s ease-in-out, opacity 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
.current-carousel-pair:not(.active) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.current-carousel-pair.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.current-carousel-pair.exit-up {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.current-carousel-pair.enter-down {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.current-carousel-pair .event-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.current-pair-counter {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #eaeaea;
|
||||
margin-top: 10px;
|
||||
padding: 5px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Non-incident events carousel (for Change/Awareness - Incidents stay pinned) */
|
||||
.non-incident-carousel-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.non-incident-carousel-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.non-incident-carousel-wrapper .event-card.carousel-item {
|
||||
width: 100%;
|
||||
transition: transform 0.8s ease-in-out, opacity 0.8s ease-in-out;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.non-incident-carousel-wrapper .event-card.carousel-item:not(.active) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.non-incident-carousel-wrapper .event-card.carousel-item.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.non-incident-carousel-wrapper .event-card.carousel-item.exit-up {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.non-incident-carousel-wrapper .event-card.carousel-item.enter-down {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
/* RESPONSIVE SIZING FOR LOW-RES DISPLAYS (720p) */
|
||||
/* ============================================ */
|
||||
@media screen and (max-height: 900px) {
|
||||
.container {
|
||||
padding: 15px 30px;
|
||||
padding-bottom: 70px;
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -750,8 +841,26 @@
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
.event-counter {
|
||||
font-size: 9px;
|
||||
padding: 2px 6px;
|
||||
top: 6px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.recognition-carousel-wrapper {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.current-carousel-wrapper {
|
||||
}
|
||||
|
||||
.current-carousel-pair {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.current-pair-counter {
|
||||
font-size: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,7 +870,6 @@
|
||||
@media screen and (min-width: 2560px) {
|
||||
.container {
|
||||
padding: 40px 80px;
|
||||
padding-bottom: 120px;
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -903,14 +1011,27 @@
|
||||
padding: 10px 24px;
|
||||
}
|
||||
|
||||
.recognition-carousel-wrapper {
|
||||
min-height: 300px;
|
||||
.event-counter {
|
||||
font-size: 24px;
|
||||
padding: 8px 20px;
|
||||
top: 20px;
|
||||
right: 25px;
|
||||
}
|
||||
|
||||
.recognition-dots .dot {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 0 10px;
|
||||
.recognition-carousel-wrapper {
|
||||
}
|
||||
|
||||
.current-carousel-wrapper {
|
||||
}
|
||||
|
||||
.current-carousel-pair {
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.current-pair-counter {
|
||||
font-size: 28px;
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -923,7 +1044,7 @@
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<div class="fiscal-week" id="fiscalWeek"></div>
|
||||
<h1>West Jefferson Events</h1>
|
||||
<h1>West Jefferson Dashboard</h1>
|
||||
</div>
|
||||
<div class="filter-container">
|
||||
<div class="clock" id="clock"></div>
|
||||
@@ -967,6 +1088,19 @@
|
||||
let recognitionCarouselInterval = null;
|
||||
let isRecognitionTransitioning = false;
|
||||
|
||||
// Current events carousel state - NOW FOR NON-INCIDENTS ONLY
|
||||
// Incidents stay pinned, other events rotate
|
||||
let nonIncidentEvents = [];
|
||||
let currentNonIncidentIndex = 0;
|
||||
let nonIncidentCarouselInterval = null;
|
||||
let isNonIncidentTransitioning = false;
|
||||
|
||||
// Keep old variables for compatibility but they won't be used
|
||||
let currentEventPairs = [];
|
||||
let currentPairIndex = 0;
|
||||
let currentCarouselInterval = null;
|
||||
let isCurrentTransitioning = false;
|
||||
|
||||
// Get business unit from URL parameter
|
||||
function getBusinessUnitFromURL() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -1054,8 +1188,8 @@
|
||||
|
||||
// Render events on the page (with smart delay during transitions)
|
||||
function renderEvents(data) {
|
||||
// If carousel is mid-transition, delay the render
|
||||
if (isTransitioning) {
|
||||
// If any carousel is mid-transition, delay the render
|
||||
if (isTransitioning || isNonIncidentTransitioning || isRecognitionTransitioning) {
|
||||
console.log('Carousel: Transition in progress, delaying render by 800ms');
|
||||
pendingDataRender = data;
|
||||
return;
|
||||
@@ -1068,92 +1202,7 @@
|
||||
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 = 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,
|
||||
'Recognition': 4,
|
||||
'TBD': 5
|
||||
};
|
||||
|
||||
const sortByTypeAndDate = (a, b) => {
|
||||
// First, sort by type priority
|
||||
const priorityA = typePriority[a.typename] || 999;
|
||||
const priorityB = typePriority[b.typename] || 999;
|
||||
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityA - priorityB;
|
||||
}
|
||||
|
||||
// If same type, sort by start time (earliest first)
|
||||
const dateA = new Date(a.starttime);
|
||||
const dateB = new Date(b.starttime);
|
||||
return dateA - dateB;
|
||||
};
|
||||
|
||||
// Sort both active and resolved events
|
||||
activeEvents.sort(sortByTypeAndDate);
|
||||
resolvedEvents.sort(sortByTypeAndDate);
|
||||
|
||||
// Combine with active first, resolved last
|
||||
const sortedEvents = [...activeEvents, ...resolvedEvents];
|
||||
|
||||
// Determine badge color based on highest severity
|
||||
const severity = getHighestSeverity(data.current);
|
||||
|
||||
html += '<div class="events-section">';
|
||||
html += `<div class="section-title current severity-${severity}">CURRENT EVENTS</div>`;
|
||||
|
||||
// Determine grid class based on number of events
|
||||
const gridClass = sortedEvents.length === 1 ? 'single' : 'multi';
|
||||
html += `<div class="events-grid ${gridClass}">`;
|
||||
|
||||
sortedEvents.forEach(event => {
|
||||
const borderColor = getColorFromType(event.typecolor);
|
||||
const ticketBadge = event.ticketnumber
|
||||
? `<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)';
|
||||
|
||||
// Calculate progressive fade for resolved incidents (fade over 30 minutes)
|
||||
let opacity = 1.0;
|
||||
let resolvedClass = '';
|
||||
if (event.resolved && event.minutes_since_end !== null && event.minutes_since_end !== undefined) {
|
||||
// Fade from 1.0 to 0.5 over 30 minutes, then drop off
|
||||
opacity = Math.max(0.5, 1.0 - (event.minutes_since_end / 30) * 0.5);
|
||||
resolvedClass = ' resolved';
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="event-card current${resolvedClass}" style="border-left-color: ${borderColor}; opacity: ${opacity};">
|
||||
${resolvedIndicator}
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>'; // Close grid
|
||||
html += '</div>'; // Close section
|
||||
}
|
||||
|
||||
// Recognition Carousel Section - AFTER current events, BEFORE upcoming
|
||||
// Recognition Carousel Section - FIRST (top of dashboard)
|
||||
if (recognitions.length > 0) {
|
||||
recognitionEvents = recognitions;
|
||||
|
||||
@@ -1197,22 +1246,148 @@
|
||||
});
|
||||
|
||||
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 = [];
|
||||
}
|
||||
|
||||
// Current Events - NEW LOGIC: Incidents pinned, others rotate
|
||||
// Separate incidents from other event types
|
||||
const incidents = otherCurrentEvents.filter(e => e.typename === 'Incident');
|
||||
const nonIncidents = otherCurrentEvents.filter(e => e.typename !== 'Incident');
|
||||
|
||||
// Sort function: by date (earliest first), then resolved status
|
||||
const sortByDateAndResolved = (a, b) => {
|
||||
// Active events before resolved
|
||||
if (a.resolved !== b.resolved) {
|
||||
return a.resolved ? 1 : -1;
|
||||
}
|
||||
// Then by start time (earliest first)
|
||||
const dateA = new Date(a.starttime);
|
||||
const dateB = new Date(b.starttime);
|
||||
return dateA - dateB;
|
||||
};
|
||||
|
||||
// Sort incidents and non-incidents
|
||||
incidents.sort(sortByDateAndResolved);
|
||||
nonIncidents.sort(sortByDateAndResolved);
|
||||
|
||||
// Helper function to render a single event card
|
||||
const renderEventCard = (event, index, total, showCounter = true) => {
|
||||
const borderColor = getColorFromType(event.typecolor);
|
||||
const ticketBadge = event.ticketnumber
|
||||
? `<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 eventCounter = (showCounter && total > 1) ? `<div class="event-counter">${index + 1} of ${total}</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)';
|
||||
|
||||
// Calculate progressive fade for resolved incidents (fade over 30 minutes)
|
||||
let opacity = 1.0;
|
||||
let resolvedClass = '';
|
||||
if (event.resolved && event.minutes_since_end !== null && event.minutes_since_end !== undefined) {
|
||||
opacity = Math.max(0.5, 1.0 - (event.minutes_since_end / 30) * 0.5);
|
||||
resolvedClass = ' resolved';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="event-card current${resolvedClass}" style="border-left-color: ${borderColor}; opacity: ${opacity};">
|
||||
${resolvedIndicator}
|
||||
${eventCounter}
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
// Determine badge color based on highest severity
|
||||
const severity = getHighestSeverity(data.current);
|
||||
|
||||
// Only show current events section if there are incidents OR non-incidents
|
||||
if (incidents.length > 0 || nonIncidents.length > 0) {
|
||||
html += '<div class="events-section">';
|
||||
html += `<div class="section-title current severity-${severity}">CURRENT EVENTS</div>`;
|
||||
|
||||
// ========================================
|
||||
// INCIDENTS SECTION - Always visible (pinned)
|
||||
// ========================================
|
||||
if (incidents.length > 0) {
|
||||
html += '<div class="incidents-pinned-section" style="margin-bottom: 10px;">';
|
||||
|
||||
const gridClass = incidents.length === 1 ? 'single' : 'multi';
|
||||
html += `<div class="events-grid ${gridClass}">`;
|
||||
incidents.forEach((event, index) => {
|
||||
html += renderEventCard(event, index, incidents.length, false); // No counter for pinned
|
||||
});
|
||||
html += '</div>'; // Close incidents grid
|
||||
html += '</div>'; // Close incidents section
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NON-INCIDENTS SECTION - Rotating carousel
|
||||
// ========================================
|
||||
if (nonIncidents.length > 0) {
|
||||
nonIncidentEvents = nonIncidents;
|
||||
|
||||
// Preserve current index if valid
|
||||
if (currentNonIncidentIndex >= nonIncidents.length) {
|
||||
currentNonIncidentIndex = 0;
|
||||
}
|
||||
|
||||
html += '<div class="non-incidents-carousel-section">';
|
||||
|
||||
if (nonIncidents.length === 1) {
|
||||
// Single non-incident - no carousel needed
|
||||
html += '<div class="events-grid single">';
|
||||
html += renderEventCard(nonIncidents[0], 0, 1, false);
|
||||
html += '</div>';
|
||||
} else {
|
||||
// Multiple non-incidents - use carousel
|
||||
html += '<div class="non-incident-carousel-container">';
|
||||
html += '<div class="non-incident-carousel-wrapper" id="nonIncidentCarousel">';
|
||||
|
||||
nonIncidents.forEach((event, index) => {
|
||||
const activeClass = index === currentNonIncidentIndex ? 'active' : 'enter-down';
|
||||
html += `
|
||||
<div class="event-card current carousel-item ${activeClass}" data-index="${index}" style="border-left-color: ${getColorFromType(event.typecolor)};">
|
||||
<div class="event-counter">${index + 1} of ${nonIncidents.length}</div>
|
||||
<div>${event.ticketnumber ? `<a href="${getServiceNowUrl(event.ticketnumber)}" target="_blank" class="event-ticket">${escapeHtml(event.ticketnumber)}</a>` : ''}</div>
|
||||
<div class="event-header">
|
||||
<div class="event-title">${escapeHtml(event.notification)}</div>
|
||||
</div>
|
||||
<div class="event-time">
|
||||
<strong>Started:</strong> ${formatDateTime(event.starttime)}
|
||||
${event.resolved ? `<br><strong>Resolved:</strong> ${formatDateTime(event.endtime)}` : event.endtime ? `<br><strong>Ends:</strong> ${formatDateTime(event.endtime)}` : '<br><strong>Status:</strong> Ongoing'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>'; // Close carousel wrapper
|
||||
html += '</div>'; // Close carousel container
|
||||
}
|
||||
html += '</div>'; // Close non-incidents section
|
||||
} else {
|
||||
nonIncidentEvents = [];
|
||||
}
|
||||
|
||||
html += '</div>'; // Close events-section
|
||||
} else {
|
||||
nonIncidentEvents = [];
|
||||
}
|
||||
|
||||
// Clear old carousel state (not used anymore)
|
||||
currentEventPairs = [];
|
||||
|
||||
// Upcoming Events - carousel with slide-up transition
|
||||
if (data.upcoming && data.upcoming.length > 0) {
|
||||
// Sort upcoming events by type priority, then by date
|
||||
@@ -1262,8 +1437,10 @@
|
||||
? `<a href="${getServiceNowUrl(event.ticketnumber)}" target="_blank" class="event-ticket">${escapeHtml(event.ticketnumber)}</a>`
|
||||
: '';
|
||||
const activeClass = index === currentUpcomingIndex ? 'active' : 'enter-down';
|
||||
const upcomingCounter = sortedUpcoming.length > 1 ? `<div class="event-counter">${index + 1} of ${sortedUpcoming.length}</div>` : '';
|
||||
html += `
|
||||
<div class="event-card upcoming carousel-item ${activeClass}" data-index="${index}" style="border-left-color: ${borderColor};">
|
||||
${upcomingCounter}
|
||||
<div>${ticketBadge}</div>
|
||||
<div class="event-header">
|
||||
<div class="event-title">${escapeHtml(event.notification)}</div>
|
||||
@@ -1289,6 +1466,13 @@
|
||||
container.innerHTML = html;
|
||||
|
||||
// Start carousels if there are events
|
||||
// NEW: Non-incident carousel (replaces old current events carousel)
|
||||
if (nonIncidentEvents.length > 1) {
|
||||
startNonIncidentCarousel();
|
||||
} else {
|
||||
stopNonIncidentCarousel();
|
||||
}
|
||||
|
||||
if (upcomingEvents.length > 1) {
|
||||
startUpcomingCarousel();
|
||||
} else {
|
||||
@@ -1303,6 +1487,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NON-INCIDENT CAROUSEL FUNCTIONS (NEW)
|
||||
// Incidents stay pinned, other events rotate
|
||||
// ========================================
|
||||
function startNonIncidentCarousel() {
|
||||
console.log(`Non-Incident Carousel: Starting with ${nonIncidentEvents.length} events`);
|
||||
stopNonIncidentCarousel();
|
||||
nonIncidentCarouselInterval = setInterval(rotateNonIncidentEvent, 5000);
|
||||
}
|
||||
|
||||
function stopNonIncidentCarousel() {
|
||||
if (nonIncidentCarouselInterval) {
|
||||
clearInterval(nonIncidentCarouselInterval);
|
||||
nonIncidentCarouselInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function rotateNonIncidentEvent() {
|
||||
if (nonIncidentEvents.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const carousel = document.getElementById('nonIncidentCarousel');
|
||||
if (!carousel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = carousel.querySelectorAll('.carousel-item');
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark transition as active
|
||||
isNonIncidentTransitioning = true;
|
||||
|
||||
const currentItem = items[currentNonIncidentIndex];
|
||||
const nextIndex = (currentNonIncidentIndex + 1) % nonIncidentEvents.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');
|
||||
|
||||
// After transition, reset the exited item
|
||||
setTimeout(() => {
|
||||
currentItem.classList.remove('exit-up');
|
||||
currentItem.classList.add('enter-down');
|
||||
isNonIncidentTransitioning = false;
|
||||
}, 800);
|
||||
|
||||
currentNonIncidentIndex = nextIndex;
|
||||
}
|
||||
|
||||
// Start the upcoming events carousel
|
||||
function startUpcomingCarousel() {
|
||||
console.log(`Carousel: Starting carousel with ${upcomingEvents.length} events`);
|
||||
@@ -1435,12 +1676,6 @@
|
||||
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');
|
||||
@@ -1451,6 +1686,66 @@
|
||||
currentRecognitionIndex = nextIndex;
|
||||
}
|
||||
|
||||
// Start the current events carousel (2-column pairs)
|
||||
function startCurrentCarousel() {
|
||||
console.log(`Current Carousel: Starting with ${currentEventPairs.length} pairs`);
|
||||
|
||||
// Clear any existing interval
|
||||
stopCurrentCarousel();
|
||||
|
||||
// Start new interval (5 seconds)
|
||||
currentCarouselInterval = setInterval(rotateCurrentPair, 5000);
|
||||
}
|
||||
|
||||
// Stop the current events carousel
|
||||
function stopCurrentCarousel() {
|
||||
if (currentCarouselInterval) {
|
||||
clearInterval(currentCarouselInterval);
|
||||
currentCarouselInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate to next pair of current events
|
||||
function rotateCurrentPair() {
|
||||
if (currentEventPairs.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const carousel = document.getElementById('currentCarousel');
|
||||
if (!carousel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pairs = carousel.querySelectorAll('.current-carousel-pair');
|
||||
if (pairs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark transition as active
|
||||
isCurrentTransitioning = true;
|
||||
|
||||
const currentPair = pairs[currentPairIndex];
|
||||
const nextIndex = (currentPairIndex + 1) % currentEventPairs.length;
|
||||
const nextPair = pairs[nextIndex];
|
||||
|
||||
// Slide current pair up and out
|
||||
currentPair.classList.remove('active');
|
||||
currentPair.classList.add('exit-up');
|
||||
|
||||
// Slide next pair up from bottom
|
||||
nextPair.classList.remove('enter-down');
|
||||
nextPair.classList.add('active');
|
||||
|
||||
// After transition, reset the exited pair for next rotation
|
||||
setTimeout(() => {
|
||||
currentPair.classList.remove('exit-up');
|
||||
currentPair.classList.add('enter-down');
|
||||
isCurrentTransitioning = false;
|
||||
}, 800);
|
||||
|
||||
currentPairIndex = nextIndex;
|
||||
}
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
@@ -1543,9 +1838,11 @@
|
||||
// Fetch notifications from API
|
||||
async function fetchNotifications() {
|
||||
try {
|
||||
let url = '../apishopfloor.asp';
|
||||
// Add timestamp to prevent any caching (browser, proxy, etc.)
|
||||
let timestamp = new Date().getTime();
|
||||
let url = '../apishopfloor.asp?_t=' + timestamp;
|
||||
if (selectedBusinessUnit) {
|
||||
url += '?businessunit=' + encodeURIComponent(selectedBusinessUnit);
|
||||
url += '&businessunit=' + encodeURIComponent(selectedBusinessUnit);
|
||||
}
|
||||
const response = await fetch(url);
|
||||
|
||||
@@ -1614,6 +1911,7 @@
|
||||
if (document.hidden) {
|
||||
clearInterval(updateInterval);
|
||||
clearInterval(clockInterval);
|
||||
stopNonIncidentCarousel(); // Incidents stay pinned, others rotate
|
||||
stopUpcomingCarousel();
|
||||
stopRecognitionCarousel();
|
||||
} else {
|
||||
@@ -1622,7 +1920,7 @@
|
||||
clockInterval = setInterval(updateClock, 1000);
|
||||
fetchNotifications();
|
||||
updateInterval = setInterval(fetchNotifications, 10000);
|
||||
// Carousel will restart in renderEvents when data loads
|
||||
// Carousels will restart in renderEvents when data loads
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user