Files
shopdb/uptimemap.asp
cproudlock e382a3246e Fix dualpath propagation, getShopfloorPCs filtering, USB management, and printer features
- Fix dualpath PC propagation direction (Equipment->PC) in api.asp and db_helpers.asp
- Fix early exit in CreatePCMachineRelationship preventing propagation
- Fix getShopfloorPCs to filter machinetypeid IN (33,34,35) instead of >= 33
- Fix getShopfloorPCs to show equipment numbers via GROUP_CONCAT subquery
- Add detailed PropagateDP logging for dualpath debugging
- Default "Show on Shopfloor Dashboard" checkbox to checked in addnotification.asp
- Add USB label batch printing, single USB labels, and USB history pages
- Add printer supplies tracking and toner report enhancements
- Add uptime map visualization page
- Add dashboard/lobby display SQL migration
- Update CLAUDE.md with IIS 401 workaround documentation
- Update TODO.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 10:44:55 -05:00

596 lines
24 KiB
Plaintext

<!DOCTYPE html>
<html lang="en">
<head>
<!--#include file="./includes/header.asp"-->
<!--#include file="./includes/sql.asp"-->
<link rel="stylesheet" href="./leaflet/leaflet.css">
<script src="./leaflet/leaflet.js"></script>
</head>
<%
theme = Request.Cookies("theme")
IF theme = "" THEN
theme="bg-theme1"
END IF
' Get minimum uptime filter from query string (default 10 days)
Dim minUptime
minUptime = Request.QueryString("minuptime")
If minUptime = "" Or Not IsNumeric(minUptime) Then
minUptime = 10
Else
minUptime = CInt(minUptime)
End If
%>
<body class="bg-theme <%Response.Write(theme)%>">
<!-- start loader -->
<div id="pageloader-overlay" class="visible incoming"><div class="loader-wrapper-outer"><div class="loader-wrapper-inner" ><div class="loader"></div></div></div></div>
<!-- end loader -->
<!-- Start wrapper-->
<div id="wrapper">
<!--#include file="./includes/leftsidebar.asp"-->
<!--Start topbar header-->
<!--#include file="./includes/topbarheader.asp"-->
<!--End topbar header-->
<div class="clearfix"></div>
<div class="content-wrapper">
<div class="row">
<div class="col-lg-9">
<div class="card">
<div class="card-body" style="padding:0;">
<div style="padding:15px; border-bottom:1px solid #444;">
<h5 class="card-title" style="margin:0; display:inline-block;">
<i class='zmdi zmdi-time-interval'></i>&nbsp;&nbsp;High Uptime PCs with Machine Relationships
</h5>
<div style="float:right;">
<input type="text" id="pcSearch" class="form-control form-control-sm" placeholder="Search hostname, machine..." style="display:inline-block; width:180px; margin-right:10px;">
<label style="margin-right:5px; display:inline-block; color:#aaa;">Min Uptime:</label>
<select id="uptimeFilter" class="form-control form-control-sm" style="display:inline-block; width:100px; margin-right:10px;">
<option value="10" <%If minUptime = 10 Then Response.Write("selected")%>>10+ days</option>
<option value="20" <%If minUptime = 20 Then Response.Write("selected")%>>20+ days</option>
<option value="30" <%If minUptime = 30 Then Response.Write("selected")%>>30+ days</option>
<option value="60" <%If minUptime = 60 Then Response.Write("selected")%>>60+ days</option>
<option value="90" <%If minUptime = 90 Then Response.Write("selected")%>>90+ days</option>
</select>
<label style="margin-right:5px; display:inline-block; color:#aaa;">BU:</label>
<select id="businessUnitFilter" class="form-control form-control-sm" style="display:inline-block; width:120px;">
<option value="all">All</option>
<%
' Get business units for dropdown
Dim rsBU, strBUSQL
strBUSQL = "SELECT businessunitid, businessunit FROM businessunits WHERE isactive = 1 ORDER BY businessunit"
Set rsBU = objConn.Execute(strBUSQL)
Do While Not rsBU.EOF
Response.Write("<option value='" & rsBU("businessunitid") & "'>" & Server.HTMLEncode(rsBU("businessunit")) & "</option>")
rsBU.MoveNext
Loop
rsBU.Close
Set rsBU = Nothing
%>
</select>
</div>
</div>
<div id="map"></div>
</div>
</div>
</div>
<div class="col-lg-3">
<div class="card">
<div class="card-header" style="background: linear-gradient(45deg, #ff6b6b, #ee5a24); color: white;">
<i class="zmdi zmdi-time-interval"></i> Uptime Legend
</div>
<div class="card-body">
<p style="font-size:12px; color:#aaa; margin-bottom:15px;">
PCs with uptime > <%=minUptime%> days that need to be rebooted.
</p>
<div style="margin-bottom:20px;">
<div style="margin:8px 0; display:flex; align-items:center;">
<span style="display:inline-block; width:14px; height:14px; background:#F44336; border-radius:50%; margin-right:10px; border:2px solid #fff; box-shadow:0 1px 3px rgba(0,0,0,0.5);"></span>
<span style="font-size:13px; color:#fff;">Needs Reboot (<%=minUptime%>+ days)</span>
</div>
</div>
<div id="statsPanel" style="margin-top:20px; padding:15px; background:#2a2a2a; border-radius:4px;">
<strong style="color:#4fc3f7;">Statistics:</strong>
<div id="pcCount" style="margin-top:10px; font-size:14px; color:#fff;">Loading...</div>
</div>
<div style="margin-top:20px; padding:15px; background:#2a2a2a; border-radius:4px; font-size:12px;">
<strong style="color:#4fc3f7;">Tips:</strong>
<ul style="margin:8px 0; padding-left:20px; color:#aaa;">
<li style="margin:5px 0;">Hover over markers for PC details</li>
<li style="margin:5px 0;">Markers placed at related machine location</li>
<li style="margin:5px 0;">Click "View PC" for full information</li>
<li style="margin:5px 0;">Red markers need attention!</li>
</ul>
</div>
</div>
</div>
</div>
</div><!--End Row-->
<!-- PCs without machine relationships (can't be mapped) -->
<div class="row" style="margin-top:20px;">
<div class="col-12">
<div class="card">
<div class="card-header" style="background: linear-gradient(45deg, #6c757d, #495057); color: white; cursor:pointer;" onclick="toggleUnmappedTable()">
<i class="zmdi zmdi-alert-circle"></i> PCs That Cannot Be Mapped (No Relationship or No Location)
<span id="unmappedToggle" style="float:right;"><i class="zmdi zmdi-chevron-down"></i></span>
</div>
<div id="unmappedTable" class="card-body" style="display:none; padding:0;">
<p style="padding:15px 15px 0 15px; font-size:12px; color:#aaa;">
These PCs have high uptime but cannot be placed on the map because they either have no machine relationship,
or their related machine has no map coordinates. <span style="color:#FFC107;">*</span> = has relationship but machine needs location.
</p>
<div class="table-responsive">
<table class="table table-sm table-hover" style="margin:0;">
<thead>
<tr>
<th>Hostname</th>
<th>Uptime (Days)</th>
<th>Last Boot</th>
<th>Business Unit</th>
<th>Related Machine</th>
<th>Serial Number</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<%
' Query PCs with high uptime that CANNOT be mapped:
' 1. No machine relationship at all, OR
' 2. Has relationship but equipment has no map coordinates
Dim rsUnmapped, strUnmappedSQL
Dim unmappedDisplayCount
unmappedDisplayCount = 0
strUnmappedSQL = "SELECT " &_
"pc.machineid, " &_
"pc.hostname, " &_
"pc.serialnumber, " &_
"pc.lastboottime, " &_
"DATEDIFF(NOW(), pc.lastboottime) as uptime_days, " &_
"bu.businessunit, " &_
"eq.machinenumber as related_machine " &_
"FROM machines pc " &_
"LEFT JOIN machinerelationships mr ON pc.machineid = mr.related_machineid AND mr.isactive = 1 " &_
"LEFT JOIN machines eq ON mr.machineid = eq.machineid " &_
"LEFT JOIN businessunits bu ON pc.businessunitid = bu.businessunitid " &_
"WHERE pc.pctypeid IS NOT NULL " &_
"AND pc.isactive = 1 " &_
"AND pc.lastboottime IS NOT NULL " &_
"AND DATEDIFF(NOW(), pc.lastboottime) >= " & minUptime & " " &_
"AND (mr.relationshipid IS NULL OR eq.mapleft IS NULL OR eq.maptop IS NULL) " &_
"ORDER BY uptime_days DESC"
Set rsUnmapped = objConn.Execute(strUnmappedSQL)
Do While Not rsUnmapped.EOF
unmappedDisplayCount = unmappedDisplayCount + 1
Dim umHostname, umUptime, umLastBoot, umBU, umSerial, umId, umColor
umId = rsUnmapped("machineid")
umHostname = rsUnmapped("hostname") & ""
If IsNull(rsUnmapped("uptime_days")) Then
umUptime = 0
Else
umUptime = CLng(rsUnmapped("uptime_days"))
End If
umLastBoot = rsUnmapped("lastboottime")
umSerial = rsUnmapped("serialnumber") & ""
If IsNull(rsUnmapped("businessunit")) Then
umBU = "-"
Else
umBU = rsUnmapped("businessunit") & ""
End If
Dim umRelatedMachine
If IsNull(rsUnmapped("related_machine")) Then
umRelatedMachine = ""
Else
umRelatedMachine = rsUnmapped("related_machine") & ""
End If
' Color based on uptime
If umUptime >= 90 Then
umColor = "#F44336"
ElseIf umUptime >= 60 Then
umColor = "#FF9800"
ElseIf umUptime >= 30 Then
umColor = "#FFC107"
Else
umColor = "#4CAF50"
End If
%>
<tr>
<td><a href="./displaypc.asp?machineid=<%=umId%>" target="_blank"><%=Server.HTMLEncode(umHostname)%></a></td>
<td><span style="color:<%=umColor%>; font-weight:bold;"><%=umUptime%></span></td>
<td><%=umLastBoot%></td>
<td><%=Server.HTMLEncode(umBU)%></td>
<td><%If umRelatedMachine <> "" Then%><span style="color:#FFC107;" title="Has relationship but machine has no map location"><%=Server.HTMLEncode(umRelatedMachine)%> *</span><%Else%><span style="color:#aaa;">None</span><%End If%></td>
<td><%=Server.HTMLEncode(umSerial)%></td>
<td><a href="./displaypc.asp?machineid=<%=umId%>" target="_blank" class="btn btn-sm btn-outline-info">View</a></td>
</tr>
<%
rsUnmapped.MoveNext
Loop
rsUnmapped.Close
Set rsUnmapped = Nothing
%>
</tbody>
</table>
</div>
<div style="padding:10px 15px; background:#2a2a2a; font-size:12px; color:#aaa;">
Total: <strong style="color:#fff;"><%=unmappedDisplayCount%></strong> PCs without machine relationships
</div>
</div>
</div>
</div>
</div>
</div><!--End content-wrapper-->
<!--Start Back To Top Button-->
<a href="javaScript:void();" class="back-to-top"><i class="fa fa-angle-double-up"></i> </a>
<!--End Back To Top Button-->
<!--Start footer-->
<footer class="footer">
</div>
</footer>
<!--End footer-->
</div><!--End wrapper-->
<!-- Bootstrap core JavaScript-->
<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/popper.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/plugins/simplebar/js/simplebar.js"></script>
<script src="assets/js/sidebar-menu.js"></script>
<script src="assets/js/app-script.js"></script>
<style>
#map {
width: 100%;
height: calc(100vh - 250px);
min-height: 600px;
background-color: #1a1a1a;
}
.leaflet-control-zoom a {
background-color: #2a2a2a !important;
color: #fff !important;
border-color: #444 !important;
}
.leaflet-control-zoom a:hover {
background-color: #3a3a3a !important;
}
.leaflet-bar {
border: 1px solid #444 !important;
}
.leaflet-popup-content-wrapper {
background: #1f1f1f !important;
color: #fff !important;
box-shadow: 0 3px 14px rgba(0,0,0,0.6) !important;
border-radius: 4px !important;
padding: 0 !important;
}
.leaflet-popup-content {
margin: 0 !important;
}
.leaflet-popup-tip-container {
display: none !important;
}
.leaflet-popup-close-button {
color: #fff !important;
font-size: 24px !important;
padding: 4px 8px 0 0 !important;
}
.leaflet-control-attribution {
display: none !important;
}
.uptime-pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.8; }
}
</style>
<script>
// Get current theme
var bodyClass = document.body.className;
var themeMatch = bodyClass.match(/bg-theme(\d+)/);
var theme = themeMatch ? 'bg-theme' + themeMatch[1] : 'bg-theme1';
var themeConfig = {
'bg-theme1': { bg: '#2a2a2a', filter: 'brightness(0.7) contrast(1.1)', gradient: 'linear-gradient(45deg, #ff6b6b, #ee5a24)' },
'bg-theme7': { bg: '#0c675e', filter: 'brightness(0.8) contrast(1.1) hue-rotate(-10deg)', gradient: 'linear-gradient(45deg, #ff6b6b, #ee5a24)' },
'bg-theme11': { bg: '#1565C0', filter: 'brightness(0.85) contrast(1.05) hue-rotate(-5deg)', gradient: 'linear-gradient(45deg, #ff6b6b, #ee5a24)' }
};
var config = themeConfig[theme] || { bg: '#1a1a1a', filter: 'brightness(0.7) contrast(1.1)', gradient: 'linear-gradient(45deg, #ff6b6b, #ee5a24)' };
document.getElementById('map').style.backgroundColor = config.bg;
var map = L.map('map', {
crs: L.CRS.Simple,
minZoom: -3
});
var bounds = [[0,0], [2550,3300]];
var lightThemes = ['bg-theme11', 'bg-theme13'];
var mapImage = lightThemes.includes(theme) ? './images/sitemap2025-light.png' : './images/sitemap2025-dark.png';
var image = L.imageOverlay(mapImage, bounds);
image.on('load', function() {
var imgElement = this.getElement();
if (imgElement) {
imgElement.style.filter = config.filter;
}
});
image.addTo(map);
var center = [1275, 1650];
map.setView(center, -2.3);
// Store PC markers
var pcMarkers = [];
// Uptime color function - single color for all (reboot needed)
function getUptimeColor(days) {
return '#F44336'; // Red - all PCs need reboot
}
function getUptimeLabel(days) {
return 'Needs Reboot';
}
<%
' First, get count of PCs without relationships (for stats display)
Dim rsUnmappedCount, unmappedCount
unmappedCount = 0
Set rsUnmappedCount = objConn.Execute("SELECT COUNT(*) as cnt FROM machines pc " &_
"LEFT JOIN machinerelationships mr ON pc.machineid = mr.related_machineid AND mr.isactive = 1 " &_
"LEFT JOIN machines eq ON mr.machineid = eq.machineid " &_
"WHERE pc.pctypeid IS NOT NULL AND pc.isactive = 1 " &_
"AND pc.lastboottime IS NOT NULL " &_
"AND DATEDIFF(NOW(), pc.lastboottime) >= " & minUptime & " " &_
"AND (mr.relationshipid IS NULL OR eq.mapleft IS NULL OR eq.maptop IS NULL)")
If Not rsUnmappedCount.EOF Then
unmappedCount = CLng(rsUnmappedCount("cnt"))
End If
rsUnmappedCount.Close
Set rsUnmappedCount = Nothing
' Query PCs with machine relationships and high uptime
' Use the RELATED MACHINE's map coordinates since PCs don't have their own
Dim strSQL, rs
Dim pcId, pcHostname, pcUptime, lastBoot
Dim eqId, eqNumber, eqMapLeft, eqMapTop, eqType
Dim businessunitid, businessunit
strSQL = "SELECT " &_
"pc.machineid as pc_id, " &_
"pc.hostname as pc_hostname, " &_
"pc.lastboottime, " &_
"DATEDIFF(NOW(), pc.lastboottime) as uptime_days, " &_
"eq.machineid as eq_id, " &_
"eq.machinenumber as eq_number, " &_
"eq.mapleft as eq_mapleft, " &_
"eq.maptop as eq_maptop, " &_
"mt.machinetype as eq_type, " &_
"pc.businessunitid, " &_
"bu.businessunit " &_
"FROM machines pc " &_
"INNER JOIN machinerelationships mr ON pc.machineid = mr.related_machineid AND mr.isactive = 1 " &_
"INNER JOIN machines eq ON mr.machineid = eq.machineid " &_
"LEFT JOIN models mo ON eq.modelnumberid = mo.modelnumberid " &_
"LEFT JOIN machinetypes mt ON mo.machinetypeid = mt.machinetypeid " &_
"LEFT JOIN businessunits bu ON pc.businessunitid = bu.businessunitid " &_
"WHERE pc.pctypeid IS NOT NULL " &_
"AND pc.isactive = 1 " &_
"AND pc.lastboottime IS NOT NULL " &_
"AND DATEDIFF(NOW(), pc.lastboottime) >= " & minUptime & " " &_
"AND eq.mapleft IS NOT NULL " &_
"AND eq.maptop IS NOT NULL " &_
"ORDER BY uptime_days DESC"
Set rs = objConn.Execute(strSQL)
Dim pcCount
pcCount = 0
Do While Not rs.EOF
pcCount = pcCount + 1
pcId = rs("pc_id")
pcHostname = rs("pc_hostname") & ""
pcUptime = rs("uptime_days")
lastBoot = rs("lastboottime")
eqId = rs("eq_id")
eqNumber = rs("eq_number") & ""
eqMapLeft = rs("eq_mapleft")
eqMapTop = 2550 - rs("eq_maptop") ' Flip Y coordinate
If Not IsNull(rs("eq_type")) Then
eqType = rs("eq_type")
Else
eqType = "Unknown"
End If
If Not IsNull(rs("businessunitid")) Then
businessunitid = rs("businessunitid")
Else
businessunitid = 0
End If
If Not IsNull(rs("businessunit")) Then
businessunit = rs("businessunit")
Else
businessunit = "N/A"
End If
%>
(function() {
var pcId = '<%=pcId%>';
var pcHostname = '<%=Server.HTMLEncode(pcHostname)%>';
var uptimeDays = <%=pcUptime%>;
var lastBoot = '<%=lastBoot%>';
var eqId = '<%=eqId%>';
var eqNumber = '<%=Server.HTMLEncode(eqNumber)%>';
var eqType = '<%=Server.HTMLEncode(eqType)%>';
var businessUnitId = '<%=businessunitid%>';
var businessUnit = '<%=Server.HTMLEncode(businessunit)%>';
var color = getUptimeColor(uptimeDays);
var uptimeLabel = getUptimeLabel(uptimeDays);
// Create custom marker icon
var icon = L.divIcon({
html: '<div style="background:' + color + '; width:14px; height:14px; border-radius:50%; border:2px solid #fff; box-shadow:0 2px 5px rgba(0,0,0,0.5);"></div>',
iconSize: [14, 14],
iconAnchor: [7, 7],
popupAnchor: [0, -5],
className: 'custom-marker'
});
var marker = L.marker([<%=eqMapTop%>, <%=eqMapLeft%>], {
title: pcHostname + ' (' + uptimeDays + ' days)',
icon: icon,
pcId: pcId,
uptimeDays: uptimeDays
}).addTo(map);
// Store marker with searchable data
pcMarkers.push({
marker: marker,
pcId: pcId,
uptimeDays: uptimeDays,
businessUnitId: businessUnitId,
searchData: {
hostname: pcHostname.toLowerCase(),
machine: eqNumber.toLowerCase(),
bu: businessUnit.toLowerCase()
}
});
// Popup on hover
var popupTimeout;
marker.on('mouseover', function() {
clearTimeout(popupTimeout);
this.openPopup();
});
marker.on('mouseout', function(e) {
popupTimeout = setTimeout(function() {
marker.closePopup();
}, 800);
});
var pcUrl = './displaypc.asp?machineid=' + pcId;
var eqUrl = './displaymachine.asp?machineid=' + eqId;
var popupContent = '<div style="background:#1f1f1f; color:#fff; min-width:280px; border-radius:4px; overflow:hidden;">' +
'<div style="background:' + config.gradient + '; padding:10px 15px; border-bottom:1px solid #444;">' +
'<h6 style="margin:0; color:#fff; font-size:14px;"><i class="zmdi zmdi-desktop-mac"></i> ' + pcHostname + '</h6>' +
'</div>' +
'<div style="padding:10px 15px; font-size:12px;">' +
'<div style="margin:8px 0; padding:10px; background:' + color + '22; border-left:3px solid ' + color + '; border-radius:0 4px 4px 0;">' +
'<strong style="color:' + color + '; font-size:16px;">' + uptimeDays + ' days uptime</strong>' +
'<div style="color:#aaa; font-size:11px; margin-top:3px;">Status: ' + uptimeLabel + '</div>' +
'</div>' +
'<div style="margin:5px 0;"><strong style="color:#aaa;">Last Boot:</strong> <span style="color:#fff;">' + lastBoot + '</span></div>' +
'<div style="margin:5px 0;"><strong style="color:#aaa;">Related Machine:</strong> <a href="' + eqUrl + '" target="_blank" style="color:#4fc3f7;">' + eqNumber + '</a> (' + eqType + ')</div>' +
(businessUnit !== 'N/A' ? '<div style="margin:5px 0;"><strong style="color:#aaa;">Business Unit:</strong> <span style="color:#fff;">' + businessUnit + '</span></div>' : '') +
'</div>' +
'<div style="padding:10px 15px; border-top:1px solid #444; text-align:center;">' +
'<a href="' + pcUrl + '" style="display:inline-block; background:' + config.gradient + '; color:#fff; padding:8px 18px; border-radius:4px; text-decoration:none; font-size:13px; font-weight:500;" target="_blank"><i class="zmdi zmdi-desktop-mac"></i> View PC</a>' +
'</div>' +
'</div>';
marker.bindPopup(popupContent);
})();
<%
rs.MoveNext
Loop
rs.Close
Set rs = Nothing
objConn.Close
%>
// Update stats
document.getElementById('pcCount').innerHTML =
'<div style="margin:5px 0;"><i class="zmdi zmdi-pin"></i> <strong><%=pcCount%></strong> PCs on map</div>' +
'<div style="margin:5px 0;"><i class="zmdi zmdi-alert-circle" style="color:#6c757d;"></i> <strong><%=unmappedCount%></strong> PCs without location</div>' +
'<div style="margin:5px 0; font-size:12px; color:#aaa;">Total: <%=pcCount + unmappedCount%> high-uptime PCs</div>';
// Toggle unmapped PCs table
function toggleUnmappedTable() {
var table = document.getElementById('unmappedTable');
var toggle = document.getElementById('unmappedToggle');
if (table.style.display === 'none') {
table.style.display = 'block';
toggle.innerHTML = '<i class="zmdi zmdi-chevron-up"></i>';
} else {
table.style.display = 'none';
toggle.innerHTML = '<i class="zmdi zmdi-chevron-down"></i>';
}
}
// Filter functionality
function applyFilters() {
var selectedBU = document.getElementById('businessUnitFilter').value;
var searchTerm = document.getElementById('pcSearch').value.toLowerCase().trim();
var minUptimeVal = parseInt(document.getElementById('uptimeFilter').value);
var visibleCount = 0;
pcMarkers.forEach(function(item) {
var buMatch = (selectedBU === 'all' || item.businessUnitId == selectedBU);
var uptimeMatch = item.uptimeDays >= minUptimeVal;
var searchMatch = true;
if (searchTerm !== '') {
searchMatch = item.searchData.hostname.indexOf(searchTerm) > -1 ||
item.searchData.machine.indexOf(searchTerm) > -1 ||
item.searchData.bu.indexOf(searchTerm) > -1;
}
if (buMatch && uptimeMatch && searchMatch) {
item.marker.setOpacity(1);
visibleCount++;
} else {
item.marker.setOpacity(0.15);
}
});
// Update visible count
document.getElementById('pcCount').innerHTML =
'<div style="margin:5px 0;"><i class="zmdi zmdi-desktop-mac"></i> <strong>' + visibleCount + '</strong> PCs visible</div>' +
'<div style="margin:5px 0; font-size:12px; color:#aaa;">Filtered from <%=pcCount%> total</div>';
}
// Uptime filter changes page
document.getElementById('uptimeFilter').addEventListener('change', function() {
window.location.href = '?minuptime=' + this.value;
});
// Listen to filter changes
document.getElementById('businessUnitFilter').addEventListener('change', applyFilters);
// Listen to search input with debouncing
var searchTimeout;
document.getElementById('pcSearch').addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(applyFilters, 300);
});
</script>
</body>
</html>