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:
215
search.asp
215
search.asp
@@ -262,7 +262,7 @@ Set rs = Nothing
|
||||
<div class="clearfix"></div>
|
||||
<div class="content-wrapper">
|
||||
<div class="row">
|
||||
<div class="col-lg-auto">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<ul class="nav nav-tabs nav-tabs-primary top-icon nav-justified">
|
||||
@@ -319,6 +319,44 @@ Set rs = Nothing
|
||||
|
||||
<h5 class="card-title">Search Results</h5>
|
||||
|
||||
<!-- Result Type Filter -->
|
||||
<div id="resultFilters" class="result-filters" style="display:none; margin-bottom: 15px;">
|
||||
<span style="font-weight: 600; margin-right: 10px; color: #888;">Filter by:</span>
|
||||
<button class="filter-btn active" data-filter="all" onclick="filterResults('all')">
|
||||
<i class="zmdi zmdi-view-list"></i> All <span class="filter-count" id="count-all"></span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="app" onclick="filterResults('app')" style="display:none;">
|
||||
<i class="zmdi zmdi-apps"></i> Applications <span class="filter-count" id="count-app"></span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="kb" onclick="filterResults('kb')" style="display:none;">
|
||||
<i class="zmdi zmdi-book"></i> KB Articles <span class="filter-count" id="count-kb"></span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="equipment" onclick="filterResults('equipment')" style="display:none;">
|
||||
<i class="zmdi zmdi-reader"></i> Equipment <span class="filter-count" id="count-equipment"></span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="pc" onclick="filterResults('pc')" style="display:none;">
|
||||
<i class="zmdi zmdi-desktop-windows"></i> PCs <span class="filter-count" id="count-pc"></span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="server" onclick="filterResults('server')" style="display:none;">
|
||||
<i class="zmdi zmdi-dns"></i> Servers <span class="filter-count" id="count-server"></span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="switch" onclick="filterResults('switch')" style="display:none;">
|
||||
<i class="zmdi zmdi-device-hub"></i> Switches <span class="filter-count" id="count-switch"></span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="accesspoint" onclick="filterResults('accesspoint')" style="display:none;">
|
||||
<i class="zmdi zmdi-wifi"></i> Access Points <span class="filter-count" id="count-accesspoint"></span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="printer" onclick="filterResults('printer')" style="display:none;">
|
||||
<i class="zmdi zmdi-print"></i> Printers <span class="filter-count" id="count-printer"></span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="employee" onclick="filterResults('employee')" style="display:none;">
|
||||
<i class="zmdi zmdi-account"></i> Employees <span class="filter-count" id="count-employee"></span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="notification" onclick="filterResults('notification')" style="display:none;">
|
||||
<i class="zmdi zmdi-notifications-none"></i> Notifications <span class="filter-count" id="count-notification"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<%
|
||||
' Show educational banner if this is a shared link
|
||||
If isShared And search <> "" Then
|
||||
@@ -488,11 +526,13 @@ Set rsNotif = Nothing
|
||||
|
||||
' Now get Machines (by machine number, alias, notes, machine type, or vendor)
|
||||
' Also search by hostname for PCs
|
||||
' Note: Join machinetypes directly to machines.machinetypeid (not through models)
|
||||
' This ensures network devices show correct type (Firewall, Switch, etc.) instead of "Machine"
|
||||
Dim rsMachines
|
||||
strSQL = "SELECT m.machineid, m.machinenumber, m.alias, m.hostname, m.pctypeid, mt.machinetype " & _
|
||||
"FROM machines m " & _
|
||||
"LEFT JOIN machinetypes mt ON m.machinetypeid = mt.machinetypeid " & _
|
||||
"LEFT JOIN models mo ON m.modelnumberid = mo.modelnumberid " & _
|
||||
"LEFT JOIN machinetypes mt ON mo.machinetypeid = mt.machinetypeid " & _
|
||||
"LEFT JOIN vendors v ON mo.vendorid = v.vendorid " & _
|
||||
"WHERE (m.machinenumber LIKE ? OR m.alias LIKE ? OR m.machinenotes LIKE ? OR mt.machinetype LIKE ? OR v.vendor LIKE ? OR m.hostname LIKE ?) " & _
|
||||
" AND m.isactive = 1 " & _
|
||||
@@ -707,7 +747,7 @@ Next
|
||||
rowStyle = ""
|
||||
End If
|
||||
|
||||
Response.Write("<tr id='" & rowId & "'" & rowStyle & ">")
|
||||
Response.Write("<tr id='" & rowId & "' data-type='app'" & rowStyle & ">")
|
||||
|
||||
' Column 1: Empty for apps
|
||||
Response.Write("<td> </td>")
|
||||
@@ -740,7 +780,7 @@ Next
|
||||
rowStyle = ""
|
||||
End If
|
||||
|
||||
Response.Write("<tr id='" & rowId & "'" & rowStyle & ">")
|
||||
Response.Write("<tr id='" & rowId & "' data-type='kb'" & rowStyle & ">")
|
||||
|
||||
' Column 1: Edit icon
|
||||
Response.Write("<td><a href='./editlink.asp?linkid=" & linkid & "'><i class='zmdi zmdi-edit' title='Edit This Link'></i></a></td>")
|
||||
@@ -773,7 +813,7 @@ Next
|
||||
rowStyle = ""
|
||||
End If
|
||||
|
||||
Response.Write("<tr id='" & rowId & "'" & rowStyle & ">")
|
||||
Response.Write("<tr id='" & rowId & "' data-type='notification'" & rowStyle & ">")
|
||||
|
||||
' Column 1: Empty for notifications
|
||||
Response.Write("<td> </td>")
|
||||
@@ -802,19 +842,56 @@ Next
|
||||
Dim machineIsPC, machineDetailPage, machineListPage, machineIcon, machineIconColor, machineLabel
|
||||
machineIsPC = (UBound(dataFields) >= 3 And dataFields(3) = "True")
|
||||
|
||||
' Set page, icon and label based on PC vs Equipment
|
||||
' Set page, icon, label and filter data-type based on PC vs Equipment vs Network Device
|
||||
Dim machineDataType
|
||||
If machineIsPC Then
|
||||
machineDetailPage = "displaypc.asp"
|
||||
machineListPage = "displaypcs.asp"
|
||||
machineIcon = "zmdi-desktop-windows"
|
||||
machineIconColor = "text-primary"
|
||||
machineLabel = "PC"
|
||||
machineDataType = "pc"
|
||||
Else
|
||||
machineDetailPage = "displaymachine.asp"
|
||||
machineListPage = "displaymachines.asp"
|
||||
' Use actual machine type from database for label (Server, Switch, etc.)
|
||||
' Fall back to "Machine" if not available
|
||||
If Not IsNull(machineType) And machineType <> "" Then
|
||||
machineLabel = machineType
|
||||
Else
|
||||
machineLabel = "Machine"
|
||||
End If
|
||||
' Set icon and data-type based on machine type
|
||||
Select Case LCase(machineLabel)
|
||||
Case "server"
|
||||
machineIcon = "zmdi-dns"
|
||||
machineIconColor = "text-danger"
|
||||
machineDataType = "server"
|
||||
Case "switch"
|
||||
machineIcon = "zmdi-device-hub"
|
||||
machineIconColor = "text-info"
|
||||
machineDataType = "switch"
|
||||
Case "access point"
|
||||
machineIcon = "zmdi-wifi"
|
||||
machineIconColor = "text-success"
|
||||
machineDataType = "accesspoint"
|
||||
Case "firewall"
|
||||
machineIcon = "zmdi-shield-security"
|
||||
machineIconColor = "text-warning"
|
||||
machineDataType = "equipment"
|
||||
Case "idf"
|
||||
machineIcon = "zmdi-input-composite"
|
||||
machineIconColor = "text-secondary"
|
||||
machineDataType = "equipment"
|
||||
Case "camera"
|
||||
machineIcon = "zmdi-videocam"
|
||||
machineIconColor = "text-primary"
|
||||
machineDataType = "equipment"
|
||||
Case Else
|
||||
machineIcon = "zmdi-reader"
|
||||
machineIconColor = "text-warning"
|
||||
machineLabel = "Machine"
|
||||
machineDataType = "equipment"
|
||||
End Select
|
||||
End If
|
||||
|
||||
rowId = "machine-result-" & machineId
|
||||
@@ -824,7 +901,7 @@ Next
|
||||
rowStyle = ""
|
||||
End If
|
||||
|
||||
Response.Write("<tr id='" & rowId & "'" & rowStyle & ">")
|
||||
Response.Write("<tr id='" & rowId & "' data-type='" & machineDataType & "'" & rowStyle & ">")
|
||||
|
||||
' Column 1: Empty for machines
|
||||
Response.Write("<td> </td>")
|
||||
@@ -855,7 +932,7 @@ Next
|
||||
rowStyle = ""
|
||||
End If
|
||||
|
||||
Response.Write("<tr id='" & rowId & "'" & rowStyle & ">")
|
||||
Response.Write("<tr id='" & rowId & "' data-type='printer'" & rowStyle & ">")
|
||||
|
||||
' Column 1: Empty for printers
|
||||
Response.Write("<td> </td>")
|
||||
@@ -892,7 +969,7 @@ Next
|
||||
rowStyle = ""
|
||||
End If
|
||||
|
||||
Response.Write("<tr id='" & rowId & "'" & rowStyle & ">")
|
||||
Response.Write("<tr id='" & rowId & "' data-type='employee'" & rowStyle & ">")
|
||||
|
||||
' Column 1: Empty for employees
|
||||
Response.Write("<td> </td>")
|
||||
@@ -960,6 +1037,44 @@ Next
|
||||
<script src="assets/js/app-script.js"></script>
|
||||
|
||||
<style>
|
||||
/* Result type filter buttons */
|
||||
.result-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.filter-btn {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
border-radius: 20px;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.filter-btn:hover {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
border-color: rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
.filter-btn.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
}
|
||||
.filter-btn i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.filter-count {
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.filter-btn.active .filter-count {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Highlighter effect - works in both dark and light themes */
|
||||
.highlighted-result {
|
||||
background-color: rgba(255, 235, 59, 0.25) !important; /* Semi-transparent yellow */
|
||||
@@ -1182,8 +1297,88 @@ function showToast() {
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
// Filter results by type
|
||||
function filterResults(type) {
|
||||
var rows = document.querySelectorAll('#searchResults tbody tr[data-type]');
|
||||
var filterBar = document.getElementById('resultFilters');
|
||||
var buttons = filterBar.querySelectorAll('.filter-btn');
|
||||
|
||||
// Update active button
|
||||
buttons.forEach(function(btn) {
|
||||
btn.classList.remove('active');
|
||||
if (btn.getAttribute('data-filter') === type) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide rows based on type
|
||||
rows.forEach(function(row) {
|
||||
if (type === 'all' || row.getAttribute('data-type') === type) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize filter counts and show filter bar if multiple types
|
||||
function initializeFilters() {
|
||||
var rows = document.querySelectorAll('#searchResults tbody tr[data-type]');
|
||||
var counts = {
|
||||
all: 0,
|
||||
app: 0,
|
||||
kb: 0,
|
||||
equipment: 0,
|
||||
pc: 0,
|
||||
server: 0,
|
||||
switch: 0,
|
||||
accesspoint: 0,
|
||||
printer: 0,
|
||||
employee: 0,
|
||||
notification: 0
|
||||
};
|
||||
var typesFound = {};
|
||||
|
||||
// Count rows by type
|
||||
rows.forEach(function(row) {
|
||||
var type = row.getAttribute('data-type');
|
||||
counts.all++;
|
||||
if (counts[type] !== undefined) {
|
||||
counts[type]++;
|
||||
typesFound[type] = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Update count badges
|
||||
for (var type in counts) {
|
||||
var countEl = document.getElementById('count-' + type);
|
||||
if (countEl) {
|
||||
countEl.textContent = '(' + counts[type] + ')';
|
||||
}
|
||||
}
|
||||
|
||||
// Show filter buttons only for types that exist
|
||||
var filterBar = document.getElementById('resultFilters');
|
||||
var buttons = filterBar.querySelectorAll('.filter-btn[data-filter]');
|
||||
buttons.forEach(function(btn) {
|
||||
var filter = btn.getAttribute('data-filter');
|
||||
if (filter === 'all' || typesFound[filter]) {
|
||||
btn.style.display = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Show filter bar only if there are multiple types
|
||||
var numTypes = Object.keys(typesFound).length;
|
||||
if (numTypes > 1) {
|
||||
filterBar.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to highlighted result on page load
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize filter bar
|
||||
initializeFilters();
|
||||
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var highlightId = urlParams.get('highlight');
|
||||
var highlightType = urlParams.get('type');
|
||||
|
||||
@@ -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