Files
shopdb/tonerreport.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

1240 lines
43 KiB
Plaintext

<!DOCTYPE html>
<html lang="en">
<head>
<!--#include file="./includes/header.asp"-->
<!--#include file="./includes/sql.asp"-->
<!--#include file="./includes/printer_supplies.asp"-->
<!--#include file="./includes/zabbix_all_supplies_cached.asp"-->
</head>
<%
' ============================================================================
' FUNCTION: SafeGetZabbixData
' PURPOSE: Safely call Zabbix function with error handling
' ============================================================================
Function SafeGetZabbixData(ipaddress)
On Error Resume Next
Dim result
result = GetAllPrinterSuppliesCached(ipaddress)
If Err.Number <> 0 Then
result = Empty
Err.Clear
End If
On Error Goto 0
SafeGetZabbixData = result
End Function
theme = Request.Cookies("theme")
IF theme = "" THEN
theme="bg-theme1"
END IF
' ============================================================================
' FUNCTION: GetActualPartNumber
' PURPOSE: Replace generic "Genuine Xerox(R) Toner" with actual part number based on model
' ============================================================================
Function GetActualPartNumber(partNumber, printerModel, colorName)
Dim cleanPartNumber, cleanUpper
cleanPartNumber = Trim(partNumber)
cleanUpper = UCase(cleanPartNumber)
' Check if it contains generic Xerox text (handle various spacing)
' "Genuine Xerox(R) Toner" or "Genuine Xerox (R) Toner" or variations
If InStr(1, cleanUpper, "GENUINE XEROX", 1) = 0 Then
' Not a generic Xerox part number, return as-is
GetActualPartNumber = cleanPartNumber
Exit Function
End If
' Map generic Xerox part numbers to actual part numbers based on model
Dim modelUpper, colorUpper, actualPN
modelUpper = UCase(Trim(printerModel))
colorUpper = UCase(Trim(colorName))
actualPN = cleanPartNumber ' default to original
' Xerox VersaLink B400/B405 Monochrome
If InStr(modelUpper, "VERSALINK B4") > 0 Or InStr(modelUpper, " B400") > 0 Or InStr(modelUpper, " B405") > 0 Then
actualPN = "106R03581" ' Standard capacity
' Xerox VersaLink B600/B605/B610/B615 Monochrome
ElseIf InStr(modelUpper, "VERSALINK B6") > 0 Or InStr(modelUpper, " B600") > 0 Or InStr(modelUpper, " B605") > 0 Or InStr(modelUpper, " B610") > 0 Or InStr(modelUpper, " B615") > 0 Then
actualPN = "106R03940" ' Standard capacity
' Xerox VersaLink B7025/B7030/B7035 Monochrome (WorkCentre 7800 series)
ElseIf InStr(modelUpper, "B7025") > 0 Or InStr(modelUpper, "B7030") > 0 Or InStr(modelUpper, "B7035") > 0 Or InStr(modelUpper, "7800") > 0 Then
actualPN = "006R01756"
' Xerox VersaLink B7125/B7130/B7135 Monochrome
ElseIf InStr(modelUpper, "B7125") > 0 Or InStr(modelUpper, "B7130") > 0 Or InStr(modelUpper, "B7135") > 0 Then
actualPN = "006R01818" ' High capacity
End If
GetActualPartNumber = actualPN
End Function
' ============================================================================
' FUNCTION: GetMarketingName
' PURPOSE: Convert OEM part numbers to marketing names for easier supply closet matching
' ============================================================================
Function GetMarketingName(oemPartNumber)
Dim oem, marketing, hpPos, pnPos
' Extract just the part number from full descriptions
' Examples:
' "Black Cartridge HP W2020A" → "W2020A"
' "Black Toner Cartridge, PN 106R03536" → "106R03536"
' "W2020A" → "W2020A"
oem = UCase(Trim(oemPartNumber))
' Check for "HP " prefix (most HP printers)
hpPos = InStr(oem, "HP ")
If hpPos > 0 Then
oem = Trim(Mid(oem, hpPos + 3))
' Remove anything in parentheses at the end
If InStr(oem, "(") > 0 Then
oem = Trim(Left(oem, InStr(oem, "(") - 1))
End If
End If
' Check for "PN " prefix (Xerox and some others)
pnPos = InStr(oem, "PN ")
If pnPos > 0 Then
oem = Trim(Mid(oem, pnPos + 3))
End If
' Remove trailing parentheses content if any
If InStr(oem, ")") > 0 Then
oem = Trim(Left(oem, InStr(oem, "(") - 1))
End If
' HP M454dw / M454dn / M479fdw (414A/414X series)
If oem = "W2020A" Or oem = "W2020X" Then
marketing = "414A/414X Black"
ElseIf oem = "W2021A" Or oem = "W2021X" Then
marketing = "414A/414X Cyan"
ElseIf oem = "W2022A" Or oem = "W2022X" Then
marketing = "414A/414X Yellow"
ElseIf oem = "W2023A" Or oem = "W2023X" Then
marketing = "414A/414X Magenta"
' HP M254dw / M255dw (202A/202X series)
ElseIf oem = "CF500A" Or oem = "CF500X" Then
marketing = "202A/202X Black"
ElseIf oem = "CF501A" Or oem = "CF501X" Then
marketing = "202A/202X Cyan"
ElseIf oem = "CF502A" Or oem = "CF502X" Then
marketing = "202A/202X Yellow"
ElseIf oem = "CF503A" Or oem = "CF503X" Then
marketing = "202A/202X Magenta"
' HP M251nw / M252dw (201A/201X series)
ElseIf oem = "CF400A" Or oem = "CF400X" Then
marketing = "201A/201X Black"
ElseIf oem = "CF401A" Or oem = "CF401X" Then
marketing = "201A/201X Cyan"
ElseIf oem = "CF402A" Or oem = "CF402X" Then
marketing = "201A/201X Yellow"
ElseIf oem = "CF403A" Or oem = "CF403X" Then
marketing = "201A/201X Magenta"
' HP LaserJet 200 color M251nw (131A/131X series)
ElseIf oem = "CF210A" Or oem = "CF210X" Then
marketing = "131A/131X Black"
ElseIf oem = "CF211A" Then
marketing = "131A Cyan"
ElseIf oem = "CF212A" Then
marketing = "131A Yellow"
ElseIf oem = "CF213A" Then
marketing = "131A Magenta"
' HP M404n / M406 (58A/58X series)
ElseIf oem = "CF258A" Or oem = "CF258X" Or oem = "CF258XC" Then
marketing = "58A/58X Black"
' HP M607 / M608 / M609 (37A/37X series)
ElseIf oem = "CF237A" Or oem = "CF237X" Then
marketing = "37A/37X Black"
' HP M506 / M607 (87A/87X series)
ElseIf oem = "CF287A" Or oem = "CF287X" Then
marketing = "87A/87X Black"
' HP M602 (90A/90X series)
ElseIf oem = "CE390A" Or oem = "CE390X" Then
marketing = "90A/90X Black"
' HP P3015dn (55A/55X series)
ElseIf oem = "CE255A" Or oem = "CE255X" Then
marketing = "55A/55X Black"
' HP LaserJet 4250tn (42A/42X series)
ElseIf oem = "Q5942A" Or oem = "Q5942X" Then
marketing = "42A/42X Black"
' HP LaserJet Pro 4001n (147A/147X series)
ElseIf oem = "W1470A" Or oem = "W1470X" Or oem = "W1480X" Then
marketing = "147A/147X Black"
' HP M454 / M479 High Capacity (120X series)
ElseIf oem = "W1020X" Or oem = "W1020XC" Then
marketing = "120X Black"
' HP Imaging Drums
ElseIf oem = "CF234A" Then
marketing = "34A Drum"
ElseIf oem = "CF219A" Then
marketing = "19A Drum"
ElseIf oem = "W2030A" Or oem = "W2030X" Then
marketing = "415A/415X Drum"
' HP Maintenance Kits
ElseIf oem = "CF254A" Then
marketing = "54A Maintenance Kit"
ElseIf oem = "CF247A" Then
marketing = "47A Maintenance Kit"
' HP 730 DesignJet Inks (T1700/T1600 series)
ElseIf oem = "P2V68A" Then
marketing = "730 Cyan Ink"
ElseIf oem = "P2V69A" Then
marketing = "730 Magenta Ink"
ElseIf oem = "P2V70A" Then
marketing = "730 Yellow Ink"
ElseIf oem = "P2V71A" Then
marketing = "730 Matte Black Ink"
ElseIf oem = "P2V72A" Then
marketing = "730 Gray Ink"
ElseIf oem = "P2V73A" Then
marketing = "730 Photo Black Ink"
' Xerox VersaLink B400/B405 Monochrome
ElseIf oem = "106R03581" Then
marketing = "Xerox Black Toner (Standard)"
ElseIf oem = "106R03583" Then
marketing = "Xerox Black Toner (High Cap)"
ElseIf oem = "106R03585" Then
marketing = "Xerox Black Toner (Extra High Cap)"
' Xerox VersaLink B600/B610/B615 Monochrome
ElseIf oem = "106R03940" Then
marketing = "Xerox Black Toner (Standard)"
ElseIf oem = "106R03942" Then
marketing = "Xerox Black Toner (High Cap)"
ElseIf oem = "106R03944" Then
marketing = "Xerox Black Toner (Extra High Cap)"
' Xerox VersaLink B7025/B7030/B7035 Monochrome (WorkCentre 7800 series)
ElseIf oem = "006R01756" Then
marketing = "Xerox Black Toner"
' Xerox VersaLink C415
ElseIf oem = "006R04677" Then
marketing = "C415 Black Toner"
ElseIf oem = "006R04678" Then
marketing = "C415 Cyan Toner"
ElseIf oem = "006R04679" Then
marketing = "C415 Magenta Toner"
ElseIf oem = "006R04680" Then
marketing = "C415 Yellow Toner"
ElseIf oem = "006R04681" Then
marketing = "C415 Black Toner (Metered)"
ElseIf oem = "006R04682" Then
marketing = "C415 Cyan Toner (Metered)"
ElseIf oem = "006R04683" Then
marketing = "C415 Magenta Toner (Metered)"
ElseIf oem = "006R04684" Then
marketing = "C415 Yellow Toner (Metered)"
ElseIf oem = "008R13335" Then
marketing = "C415 Waste Cartridge"
ElseIf oem = "013R00701" Then
marketing = "C415 Drum/Imaging Unit"
' Xerox VersaLink C405
ElseIf oem = "106R03500" Then
marketing = "C405 Black Toner"
ElseIf oem = "106R03501" Then
marketing = "C405 Cyan Toner"
ElseIf oem = "106R03502" Then
marketing = "C405 Magenta Toner"
ElseIf oem = "106R03503" Then
marketing = "C405 Yellow Toner"
ElseIf oem = "108R01124" Then
marketing = "C405 Waste Container"
ElseIf oem = "101R00555" Then
marketing = "C405 Drum Cartridge"
' Xerox VersaLink C7125/C7100
ElseIf oem = "006R01824" Then
marketing = "C7125 Black Toner"
ElseIf oem = "006R01825" Then
marketing = "C7125 Cyan Toner"
ElseIf oem = "006R01826" Then
marketing = "C7125 Magenta Toner"
ElseIf oem = "006R01827" Then
marketing = "C7125 Yellow Toner"
ElseIf oem = "006R01820" Then
marketing = "C7125 Black Toner (Metered)"
ElseIf oem = "006R01821" Then
marketing = "C7125 Cyan Toner (Metered)"
ElseIf oem = "006R01822" Then
marketing = "C7125 Magenta Toner (Metered)"
ElseIf oem = "006R01823" Then
marketing = "C7125 Yellow Toner (Metered)"
ElseIf oem = "115R00129" Then
marketing = "VersaLink Waste Container"
ElseIf oem = "013R00688" Then
marketing = "C7125 Drum Cartridge"
' Xerox VersaLink B7125/B7130/B7135 Monochrome
ElseIf oem = "006R01817" Then
marketing = "B7125 Black Toner (Standard)"
ElseIf oem = "006R01818" Then
marketing = "B7125 Black Toner (High Capacity)"
ElseIf oem = "006R01819" Then
marketing = "B7125 Black Toner (Metered)"
ElseIf oem = "013R00687" Then
marketing = "B7125 Drum Cartridge"
' Xerox VersaLink C7000 series
ElseIf oem = "106R03536" Then
marketing = "Xerox Black Toner"
ElseIf oem = "106R03537" Then
marketing = "Xerox Yellow Toner"
ElseIf oem = "106R03538" Then
marketing = "Xerox Cyan Toner"
ElseIf oem = "106R03539" Then
marketing = "Xerox Magenta Toner"
' Xerox WorkCentre 7800 series
ElseIf oem = "006R01509" Then
marketing = "Xerox Black Toner"
ElseIf oem = "006R01510" Then
marketing = "Xerox Yellow Toner"
ElseIf oem = "006R01511" Then
marketing = "Xerox Magenta Toner"
ElseIf oem = "006R01512" Then
marketing = "Xerox Cyan Toner"
' Xerox AltaLink C8030/C8035 series
ElseIf oem = "006R01820" Then
marketing = "Xerox Black Toner"
ElseIf oem = "006R01821" Then
marketing = "Xerox Cyan Toner"
ElseIf oem = "006R01822" Then
marketing = "Xerox Magenta Toner"
ElseIf oem = "006R01823" Then
marketing = "Xerox Yellow Toner"
' Xerox WorkCentre 7970 series
ElseIf oem = "006R01742" Then
marketing = "Xerox Black Toner"
ElseIf oem = "006R01743" Then
marketing = "Xerox Cyan Toner"
ElseIf oem = "006R01744" Then
marketing = "Xerox Magenta Toner"
ElseIf oem = "006R01745" Then
marketing = "Xerox Yellow Toner"
' Xerox Phaser 4600/4620 series
ElseIf oem = "006R01817" Then
marketing = "Xerox Black Toner"
' Xerox AltaLink EC8036/C8036 - WC7800 series compatible
ElseIf oem = "006R01509" Then
marketing = "WC7800 Black"
ElseIf oem = "006R01512" Then
marketing = "WC7800 Cyan"
ElseIf oem = "006R01511" Then
marketing = "WC7800 Magenta"
ElseIf oem = "006R01510" Then
marketing = "WC7800 Yellow"
' Xerox AltaLink EC8036/C8036 - AltaLink C80xx series
ElseIf oem = "006R01697" Then
marketing = "AltaLink Black"
ElseIf oem = "006R01698" Then
marketing = "AltaLink Cyan"
ElseIf oem = "006R01699" Then
marketing = "AltaLink Magenta"
ElseIf oem = "006R01700" Then
marketing = "AltaLink Yellow"
ElseIf oem = "006R01701" Then
marketing = "AltaLink Black (Met)"
ElseIf oem = "006R01702" Then
marketing = "AltaLink Cyan (Met)"
ElseIf oem = "006R01703" Then
marketing = "AltaLink Magenta (Met)"
ElseIf oem = "006R01704" Then
marketing = "AltaLink Yellow (Met)"
ElseIf oem = "008R13061" Then
marketing = "EC8036 Waste Container"
ElseIf oem = "013R00677" Then
marketing = "EC8036 Drum Cartridge"
Else
marketing = "" ' No mapping found - will display OEM number only
End If
GetMarketingName = marketing
End Function
%>
<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-xl-auto">
<div class="card">
<div class="card-body">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div>
<h5 class="card-title"><i class='zmdi zmdi-collection-image text-yellow'></i>&nbsp;&nbsp;Supplies Alert Report</h5>
<p class="text-muted" style="font-size:13px; margin-top:5px; margin-bottom:0;">
Monitors: Toner/Ink &le;5%, Drums &le;5%, Maintenance Kits &le;5%, Waste &ge;95% (Xerox: &le;5% inverted)
</p>
</div>
<div style="display:flex; gap:10px; align-items:center;">
<select id="vendorFilter" class="form-control form-control-sm" style="width:150px;">
<option value="all">All Models</option>
<option value="HP">HP Models</option>
<option value="Xerox">Xerox Models</option>
</select>
<button type="button" class="btn btn-sm btn-secondary" id="refreshBtn" title="Clear Zabbix cache and refresh data">
<i class="zmdi zmdi-refresh"></i> Refresh Data
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead>
<tr>
<th scope="col">Printer</th>
<th scope="col">Location</th>
<th scope="col">Model</th>
<th scope="col">Level</th>
<th scope="col">Part Number</th>
</tr>
</thead>
<tbody>
<%
' Declare all variables at top level
Dim strSQL, rs, printerid, printerwindowsname, printercsfname, ipaddress, machinenumber, modelnumber, machineid, vendor
Dim printerData, zabbixConnected, pingStatus, suppliesJSON
Dim lowSuppliesFound
Dim itemStart, itemEnd, currentPos, itemBlock
Dim itemName, itemValue, itemStatus, itemState
Dim namePos, nameStart, nameEnd
Dim valuePos, valueStart, valueEnd
Dim statusPos, statusStart, statusEnd
Dim statePos, stateStart, stateEnd
Dim baseName, numericValue
Dim statusIcon, statusColor, statusText
Dim partNumber, lookupName
Dim partNumbers
Dim debugPartNumbers, debugAllItems, debugItemCount
Dim isSupplyItem, isWasteItem, showItem
Dim marketingName, displayPartNumber
Dim partKeyName, tryName, partKey, foundMatch
Dim primaryWord, supplyType, colorPos
Dim typeMatches, colorMatches
Dim urgencyScore, alertItem, alertItems(1000), alertCount, i, j, tempAlert, k, outputItem
Dim isXeroxPrinter
Dim stdPartNumber, metPartNumber, altPartNumber, stdMarketing, metMarketing, altMarketing, partNumberResult, partNumberParts
lowSuppliesFound = False
alertCount = 0
strSQL = "SELECT printers.printerid, printers.printerwindowsname, printers.printercsfname, printers.ipaddress, " &_
"machines.machinenumber, machines.machineid, models.modelnumber, machines.alias, vendors.vendor " &_
"FROM printers " &_
"INNER JOIN models ON printers.modelid = models.modelnumberid " &_
"INNER JOIN machines ON printers.machineid = machines.machineid " &_
"INNER JOIN vendors ON models.vendorid = vendors.vendorid " &_
"WHERE printers.isactive = 1 AND printers.ipaddress IS NOT NULL AND printers.ipaddress != '' " &_
"ORDER BY machines.machinenumber ASC"
set rs = objconn.Execute(strSQL)
While Not rs.EOF
printerid = rs("printerid")
printerwindowsname = rs("printerwindowsname")
printercsfname = rs("printercsfname")
ipaddress = rs("ipaddress")
modelnumber = rs("modelnumber")
machineid = rs("machineid")
vendor = rs("vendor")
' Detect if this is a Xerox printer for vendor-specific waste cartridge logic
' Xerox printers (VersaLink, AltaLink, EC series) report waste inverted:
' 100% = empty (capacity remaining), 0% = full (no capacity remaining)
isXeroxPrinter = (InStr(1, vendor, "Xerox", 1) > 0)
' Use alias if available, otherwise machinenumber
If NOT IsNull(rs("alias")) AND rs("alias") <> "" Then
machinenumber = rs("alias")
Else
machinenumber = rs("machinenumber")
End If
' Get cached Zabbix data for this printer (all supplies including maintenance)
printerData = SafeGetZabbixData(ipaddress)
If Not IsEmpty(printerData) And IsArray(printerData) Then
zabbixConnected = printerData(0)
pingStatus = printerData(1)
suppliesJSON = printerData(2)
' Parse supplies JSON to find items below 20%
If zabbixConnected = "1" And suppliesJSON <> "" And InStr(suppliesJSON, """result"":[") > 0 Then
' Check if result array is not empty
If InStr(suppliesJSON, """result"":[]") = 0 Then
' First pass: Build lookup of part numbers (type:info items)
' Use Dictionary object for more reliable storage
Set partNumbers = Server.CreateObject("Scripting.Dictionary")
debugPartNumbers = ""
debugAllItems = ""
debugItemCount = 0
currentPos = InStr(suppliesJSON, """result"":[") + 11
' Scan for part number items (containing "Part Number" in name)
Do While currentPos > 11 And currentPos < Len(suppliesJSON)
itemStart = InStr(currentPos, suppliesJSON, "{""itemid"":")
If itemStart = 0 Then Exit Do
itemEnd = InStr(itemStart, suppliesJSON, "},{")
If itemEnd = 0 Then itemEnd = InStr(itemStart, suppliesJSON, "}]")
If itemEnd = 0 Then Exit Do
itemBlock = Mid(suppliesJSON, itemStart, itemEnd - itemStart + 1)
' Extract name
namePos = InStr(itemBlock, """name"":""")
If namePos > 0 Then
nameStart = namePos + 8
nameEnd = InStr(nameStart, itemBlock, """")
itemName = Mid(itemBlock, nameStart, nameEnd - nameStart)
Else
itemName = ""
End If
' DEBUG: Track all items scanned
debugItemCount = debugItemCount + 1
If debugItemCount <= 10 Then
debugAllItems = debugAllItems & itemName & " | "
End If
' If this is a part number item, store it
' Look for various part number patterns (case-insensitive)
If InStr(1, itemName, "Part Number", 1) > 0 Or InStr(1, itemName, "Part number", 1) > 0 Or InStr(1, itemName, "OEM", 1) > 0 Or InStr(1, itemName, "SKU", 1) > 0 Then
valuePos = InStr(itemBlock, """lastvalue"":""")
If valuePos > 0 Then
valueStart = valuePos + 13
valueEnd = InStr(valueStart, itemBlock, """")
itemValue = Mid(itemBlock, valueStart, valueEnd - valueStart)
' Store in dictionary with full item name as key (e.g., "Black Toner Part Number")
If Not partNumbers.Exists(itemName) Then
partNumbers.Add itemName, itemValue
debugPartNumbers = debugPartNumbers & "[" & itemName & "=" & itemValue & "] "
End If
End If
End If
currentPos = itemEnd + 1
Loop
' Debug disabled - uncomment to show part number matching debug info
' Response.Write("<tr style='background:#1e3a5f;'><td colspan='7'><small>")
' Response.Write("<strong>DEBUG (" & ipaddress & "):</strong> Scanned " & debugItemCount & " items | ")
' Response.Write("<strong>First 10:</strong> " & Server.HTMLEncode(debugAllItems) & "<br>")
' If debugPartNumbers <> "" Then
' Response.Write("<strong>Part Numbers Found:</strong> " & Server.HTMLEncode(debugPartNumbers))
' Else
' Response.Write("<strong style='color:#ff6666;'>No part numbers found!</strong>")
' End If
' Response.Write("</small></td></tr>")
' Second pass: Find level items below 20%
currentPos = InStr(suppliesJSON, """result"":[") + 11
Do While currentPos > 11 And currentPos < Len(suppliesJSON)
' Find next item
itemStart = InStr(currentPos, suppliesJSON, "{""itemid"":")
If itemStart = 0 Then Exit Do
' Find end of this item
itemEnd = InStr(itemStart, suppliesJSON, "},{")
If itemEnd = 0 Then
' Last item in array
itemEnd = InStr(itemStart, suppliesJSON, "}]")
End If
If itemEnd = 0 Then Exit Do
itemBlock = Mid(suppliesJSON, itemStart, itemEnd - itemStart + 1)
' Extract item name - "name":" is 8 characters
namePos = InStr(itemBlock, """name"":""")
If namePos > 0 Then
nameStart = namePos + 8
nameEnd = InStr(nameStart, itemBlock, """")
itemName = Mid(itemBlock, nameStart, nameEnd - nameStart)
Else
itemName = "Unknown"
End If
' Extract lastvalue - "lastvalue":" is 13 characters
valuePos = InStr(itemBlock, """lastvalue"":""")
If valuePos > 0 Then
valueStart = valuePos + 13
valueEnd = InStr(valueStart, itemBlock, """")
itemValue = Mid(itemBlock, valueStart, valueEnd - valueStart)
Else
itemValue = "0"
End If
' Extract status (0 = enabled, 1 = disabled) - "status":" is 10 characters
statusPos = InStr(itemBlock, """status"":""")
If statusPos > 0 Then
statusStart = statusPos + 10
statusEnd = InStr(statusStart, itemBlock, """")
itemStatus = Mid(itemBlock, statusStart, statusEnd - statusStart)
Else
itemStatus = "0"
End If
' Extract state (0 = normal, 1 = not supported) - "state":" is 9 characters
statePos = InStr(itemBlock, """state"":""")
If statePos > 0 Then
stateStart = statePos + 9
stateEnd = InStr(stateStart, itemBlock, """")
itemState = Mid(itemBlock, stateStart, stateEnd - stateStart)
Else
itemState = "0"
End If
' Convert value to number and check if below 20%
On Error Resume Next
numericValue = CDbl(itemValue)
On Error Goto 0
' Filter: Only show actual supply level items (must have "Level" in name)
isSupplyItem = False
If InStr(1, itemName, "Level", 1) > 0 Then
' Exclude non-supply items
If InStr(1, itemName, "Part Number", 1) = 0 And _
InStr(1, itemName, "ICMP", 1) = 0 And _
InStr(1, itemName, "ping", 1) = 0 And _
InStr(1, itemName, "loss", 1) = 0 And _
InStr(1, itemName, "response", 1) = 0 And _
InStr(1, itemName, "Hostname", 1) = 0 And _
InStr(1, itemName, "Model", 1) = 0 And _
InStr(1, itemName, "Serial", 1) = 0 And _
InStr(1, itemName, "Location", 1) = 0 And _
InStr(1, itemName, "Firmware", 1) = 0 And _
InStr(1, itemName, "Current", 1) = 0 And _
InStr(1, itemName, " Max", 1) = 0 Then
isSupplyItem = True
End If
End If
' Detect if this is a waste cartridge (works backwards - high % is bad)
isWasteItem = (InStr(1, itemName, "Waste", 1) > 0)
' Check if item should be shown based on type
showItem = False
If isSupplyItem And itemStatus = "0" And itemState = "0" Then
If isWasteItem Then
' Waste cartridge logic - MODEL SPECIFIC!
' Standard (HP, etc.): 0% = empty/ok, 100% = full/bad (alert when >=95%)
' Xerox EC series (EC8036, etc.): 0% = full/bad, 100% = empty/ok (INVERTED - alert when <=5%)
If isXeroxPrinter Then
' Xerox EC series waste: alert when at or BELOW 5% (inverted - low % means full)
If numericValue <= 5 And numericValue >= 0 Then
showItem = True
End If
Else
' Standard waste: alert when at or ABOVE 95% (nearly full)
If numericValue >= 95 And numericValue <= 100 Then
showItem = True
End If
End If
Else
' Regular supplies: alert when at or below 5% (running low)
If numericValue <= 5 And numericValue >= 0 Then
showItem = True
End If
End If
End If
If showItem Then
lowSuppliesFound = True
' Determine status indicator
If isWasteItem Then
If isXeroxPrinter Then
' Xerox EC series waste: INVERTED - low % = full/bad
If numericValue <= 5 Then
statusIcon = "zmdi-alert-circle"
statusColor = "#ff0000"
statusText = "Critical - Nearly Full"
ElseIf numericValue <= 10 Then
statusIcon = "zmdi-alert-triangle"
statusColor = "#ff6600"
statusText = "Very High"
Else
statusIcon = "zmdi-info"
statusColor = "#ffaa00"
statusText = "High"
End If
Else
' Standard waste (HP, etc.): high % = full/bad
If numericValue >= 95 Then
statusIcon = "zmdi-alert-circle"
statusColor = "#ff0000"
statusText = "Critical - Nearly Full"
ElseIf numericValue >= 90 Then
statusIcon = "zmdi-alert-triangle"
statusColor = "#ff6600"
statusText = "Very High"
Else
statusIcon = "zmdi-info"
statusColor = "#ffaa00"
statusText = "High"
End If
End If
Else
' Regular supply status (low % = bad)
' 0% = Critical (red), 1-5% = Warning (orange)
If numericValue = 0 Then
statusIcon = "zmdi-alert-circle"
statusColor = "#ff0000"
statusText = "Critical"
Else
statusIcon = "zmdi-alert-triangle"
statusColor = "#ff6600"
statusText = "Warning"
End If
End If
' Look up part numbers from hardcoded list (SNMP is unreliable)
' Returns "standard|metered" or "standard|alt|metered" or just "standard"
partNumberResult = GetSupplyPartNumbers(modelnumber, itemName)
stdPartNumber = ""
metPartNumber = ""
altPartNumber = ""
If InStr(partNumberResult, "|") > 0 Then
partNumberParts = Split(partNumberResult, "|")
stdPartNumber = partNumberParts(0)
If UBound(partNumberParts) >= 2 Then
' 3 options: std|alt|met
altPartNumber = partNumberParts(1)
metPartNumber = partNumberParts(2)
Else
' 2 options: std|met
metPartNumber = partNumberParts(1)
End If
Else
stdPartNumber = partNumberResult
End If
If stdPartNumber = "" Then stdPartNumber = "-"
' Get marketing names
stdMarketing = GetMarketingName(stdPartNumber)
metMarketing = ""
altMarketing = ""
If metPartNumber <> "" Then metMarketing = GetMarketingName(metPartNumber)
If altPartNumber <> "" Then altMarketing = GetMarketingName(altPartNumber)
' Build display string showing all options
If altPartNumber <> "" Then
' 3 options (EC8036 style)
displayPartNumber = "<div style='line-height:1.3; font-size:12px;'>"
displayPartNumber = displayPartNumber & "<strong>WC:</strong> " & Server.HTMLEncode(stdPartNumber) & "<br>"
displayPartNumber = displayPartNumber & "<strong>Alt:</strong> " & Server.HTMLEncode(altPartNumber) & "<br>"
displayPartNumber = displayPartNumber & "<strong>Met:</strong> " & Server.HTMLEncode(metPartNumber)
displayPartNumber = displayPartNumber & "</div>"
ElseIf metPartNumber <> "" Then
' 2 options
displayPartNumber = "<div style='line-height:1.4;'>"
If stdMarketing <> "" Then
displayPartNumber = displayPartNumber & "<strong>Std:</strong> " & Server.HTMLEncode(stdMarketing) & " <small style='color:#999;'>(" & Server.HTMLEncode(stdPartNumber) & ")</small><br>"
Else
displayPartNumber = displayPartNumber & "<strong>Std:</strong> " & Server.HTMLEncode(stdPartNumber) & "<br>"
End If
If metMarketing <> "" Then
displayPartNumber = displayPartNumber & "<strong>Met:</strong> " & Server.HTMLEncode(metMarketing) & " <small style='color:#999;'>(" & Server.HTMLEncode(metPartNumber) & ")</small>"
Else
displayPartNumber = displayPartNumber & "<strong>Met:</strong> " & Server.HTMLEncode(metPartNumber)
End If
displayPartNumber = displayPartNumber & "</div>"
Else
' Single option
If stdMarketing <> "" Then
displayPartNumber = "<strong>" & Server.HTMLEncode(stdMarketing) & "</strong><br><small style='color:#999;'>" & Server.HTMLEncode(stdPartNumber) & "</small>"
Else
displayPartNumber = Server.HTMLEncode(stdPartNumber)
End If
End If
' Calculate urgency score for sorting
' For regular supplies: lower % = higher urgency (5% = 95 urgency)
' For standard waste: higher % = higher urgency (95% = 95 urgency)
' For Xerox EC series waste: INVERTED - lower % = higher urgency (5% = 95 urgency)
If isWasteItem Then
If isXeroxPrinter Then
' Xerox EC series waste: inverted - low % is bad
urgencyScore = 100 - numericValue
Else
' Standard waste: high % is bad
urgencyScore = numericValue
End If
Else
' Regular supplies: low % is bad
urgencyScore = 100 - numericValue
End If
' Store alert data for later sorting
alertItem = Array( _
urgencyScore, _
vendor, _
printerid, _
printerwindowsname, _
machineid, _
machinenumber, _
modelnumber, _
numericValue, _
statusColor, _
displayPartNumber, _
itemName _
)
alertItems(alertCount) = alertItem
alertCount = alertCount + 1
End If
' Move to next item
currentPos = itemEnd + 1
Loop
End If
End If
End If
rs.MoveNext
Wend
' Sort alerts by urgency (highest urgency first = most critical)
' Simple bubble sort with error handling
On Error Resume Next
If alertCount > 1 Then
For i = 0 To alertCount - 2
For j = 0 To alertCount - i - 2
' alertItems(j)(0) is the urgency score
If Not IsEmpty(alertItems(j)) And Not IsEmpty(alertItems(j + 1)) Then
If CDbl(alertItems(j)(0)) < CDbl(alertItems(j + 1)(0)) Then
' Swap items
tempAlert = alertItems(j)
alertItems(j) = alertItems(j + 1)
alertItems(j + 1) = tempAlert
End If
End If
Next
Next
End If
' Output sorted alerts
If alertCount > 0 Then
lowSuppliesFound = True
For k = 0 To alertCount - 1
If Not IsEmpty(alertItems(k)) And IsArray(alertItems(k)) Then
outputItem = alertItems(k)
' Array indices: 0=urgencyScore, 1=vendor, 2=printerid, 3=printerwindowsname,
' 4=machineid, 5=machinenumber, 6=modelnumber, 7=numericValue,
' 8=statusColor, 9=displayPartNumber, 10=itemName
Response.Write("<tr data-vendor='" & Server.HTMLEncode(outputItem(1)) & "'>")
Response.Write("<td><a href='./displayprinter.asp?printerid=" & outputItem(2) & "'>" & Server.HTMLEncode(outputItem(3)) & "</a></td>")
Response.Write("<td><span class='location-link' data-type='printer' data-id='" & outputItem(2) & "' style='cursor:pointer; color:#007bff;'><i class='zmdi zmdi-pin' style='margin-right:4px;'></i>" & Server.HTMLEncode(outputItem(5)) & "</span></td>")
Response.Write("<td>" & Server.HTMLEncode(outputItem(6)) & "</td>")
Response.Write("<td><strong style='color:" & outputItem(8) & ";'>" & Round(CDbl(outputItem(7)), 1) & "%</strong></td>")
Response.Write("<td>" & outputItem(9) & "</td>")
Response.Write("</tr>")
End If
Next
End If
On Error Goto 0
If Not lowSuppliesFound Then
Response.Write("<tr><td colspan='6' style='text-align:center; padding:20px;'>")
Response.Write("<i class='zmdi zmdi-check-circle' style='color:#00aa00; font-size:24px;'></i><br>")
Response.Write("No supply issues found - All printers have adequate supplies")
Response.Write("</td></tr>")
End If
objConn.Close
%>
</tbody>
</table>
</div>
<div class="card-footer">
<small class="text-muted">
<i class="zmdi zmdi-info-outline"></i> This report shows printers with low supplies (&le;5%) or waste cartridges near full (&ge;95%, Xerox inverted &le;5%).
Data refreshed from Zabbix every 5 minutes.
</small>
</div>
</div>
</div>
</div>
</div><!--End Row-->
<!-- End container-fluid-->
</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>
<!-- simplebar js -->
<script src="assets/plugins/simplebar/js/simplebar.js"></script>
<!-- sidebar-menu js -->
<script src="assets/js/sidebar-menu.js"></script>
<!-- Custom scripts -->
<script src="assets/js/app-script.js"></script>
<script>
$(document).ready(function() {
$('#refreshBtn').click(function() {
if (confirm('Clear all Zabbix cache and refresh? This will fetch fresh data from Zabbix for all printers.')) {
var btn = $(this);
var originalHtml = btn.html();
// Show loading state
btn.prop('disabled', true);
btn.html('<i class="zmdi zmdi-refresh zmdi-hc-spin"></i> Refreshing...');
// Call clear cache endpoint
$.ajax({
url: './adminclearcache.asp?confirm=yes&type=zabbix&ajax=1',
method: 'GET',
success: function() {
// Reload page after cache cleared
location.reload();
},
error: function() {
// Still reload on error
location.reload();
}
});
}
});
});
</script>
<!-- Print styles for readable printouts -->
<style>
@media print {
/* Reset dark theme for printing */
body, .bg-theme, .content-wrapper, .card, .card-body {
background: white !important;
color: black !important;
}
/* Make all text black and readable - override inline styles */
h1, h2, h3, h4, h5, h6, p, span, td, th, a, strong, small, div {
color: black !important;
}
/* Force small text (part numbers) to be dark and readable */
small, small[style], td small {
color: #333 !important;
font-size: 11px !important;
}
/* Location link styling for print */
.location-link, .location-link i {
color: black !important;
}
/* Table styling for print */
.table {
border-collapse: collapse !important;
}
.table th, .table td {
border: 1px solid #333 !important;
padding: 8px !important;
color: black !important;
background: white !important;
}
.table thead th {
background: #eee !important;
font-weight: bold !important;
}
.table-striped tbody tr:nth-of-type(odd) {
background: #f5f5f5 !important;
}
/* Keep status colors visible but darker for print */
strong[style*="color:#ff0000"] { color: #cc0000 !important; }
strong[style*="color:#ff6600"] { color: #cc5500 !important; }
/* Hide non-essential elements */
#sidebar-wrapper, .topbar-nav, .back-to-top, .footer,
#pageloader-overlay, #vendorFilter, #refreshBtn,
.location-popup, .location-popup-overlay {
display: none !important;
}
/* Expand content to full width */
.content-wrapper {
margin-left: 0 !important;
padding: 0 !important;
}
.card {
border: none !important;
box-shadow: none !important;
}
/* Part number styling */
.card-body small {
color: #666 !important;
}
}
</style>
<!-- Location map popup modal -->
<style>
/* Theme-specific styling for location links */
body.bg-theme .location-link {
color: #fff !important;
}
.location-popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9998;
display: none;
}
.location-popup {
position: fixed;
background: #1f1f1f;
border: 2px solid #667eea;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8);
z-index: 9999;
display: none;
max-width: 90vw;
max-height: 90vh;
}
.location-popup-header {
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 15px;
border-radius: 6px 6px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.location-popup-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.location-popup-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.location-popup-body {
padding: 0;
background: #2a2a2a;
}
.location-popup iframe {
display: block;
border: none;
border-radius: 0 0 6px 6px;
}
.location-link:hover {
text-decoration: underline;
}
</style>
<script>
$(document).ready(function() {
// Create popup elements
var $overlay = $('<div class="location-popup-overlay"></div>').appendTo('body');
var $popup = $('<div class="location-popup"></div>').appendTo('body');
$popup.html(
'<div class="location-popup-header">' +
'<h6 style="margin:0; font-size:16px;"><i class="zmdi zmdi-pin"></i> <span class="location-title">Loading...</span></h6>' +
'<button class="location-popup-close" title="Close (Esc)">&times;</button>' +
'</div>' +
'<div class="location-popup-body">' +
'<iframe src="" width="440" height="340"></iframe>' +
'</div>'
);
var $iframe = $popup.find('iframe');
var $title = $popup.find('.location-title');
var currentMachineId = null;
// Function to show popup with smart positioning
function showLocationPopup(deviceType, deviceId, locationName, mouseEvent) {
// Don't reload if same location
var locationKey = deviceType + '_' + deviceId;
if (currentMachineId === locationKey && $popup.is(':visible')) {
return;
}
currentMachineId = locationKey;
$title.text(locationName);
// Load iframe - use type and id parameters for printer-specific location
$iframe.attr('src', './displaylocation.asp?type=' + deviceType + '&id=' + deviceId);
// Position popup
var popupWidth = 440;
var popupHeight = 400;
var mouseX = mouseEvent.clientX;
var mouseY = mouseEvent.clientY;
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
var left, top;
// Horizontal positioning
left = mouseX + 10;
if (left + popupWidth > windowWidth - 10) {
left = mouseX - popupWidth - 10;
}
if (left < 10) {
left = 10;
}
// Vertical positioning
var spaceBelow = windowHeight - mouseY;
var spaceAbove = mouseY;
if (spaceBelow >= popupHeight + 20) {
top = mouseY + 10;
} else if (spaceAbove >= popupHeight + 20) {
top = mouseY - popupHeight - 10;
} else {
top = Math.max(10, (windowHeight - popupHeight) / 2);
}
if (top < 10) {
top = 10;
}
if (top + popupHeight > windowHeight - 10) {
top = windowHeight - popupHeight - 10;
}
$popup.css({
left: left + 'px',
top: top + 'px',
display: 'block'
});
$overlay.fadeIn(200);
$popup.fadeIn(200);
}
// Function to hide popup
function hideLocationPopup() {
$overlay.fadeOut(200);
$popup.fadeOut(200);
setTimeout(function() {
$iframe.attr('src', '');
currentMachineId = null;
}, 200);
}
var hoverTimer = null;
// Hover handler for location links
$(document).on('mouseenter', '.location-link', function(e) {
var $link = $(this);
var deviceType = $link.data('type') || 'machine';
var deviceId = $link.data('id') || $link.data('machineid');
var locationName = $link.text().trim();
var mouseEvent = e;
if (hoverTimer) {
clearTimeout(hoverTimer);
}
hoverTimer = setTimeout(function() {
showLocationPopup(deviceType, deviceId, locationName, mouseEvent);
}, 300);
});
// Cancel popup if mouse leaves before delay
$(document).on('mouseleave', '.location-link', function() {
if (hoverTimer) {
clearTimeout(hoverTimer);
hoverTimer = null;
}
});
// Keep popup open when hovering over it
$popup.on('mouseenter', function() {
// Keep open
});
// Close popup when mouse leaves popup
$popup.on('mouseleave', function() {
hideLocationPopup();
});
// Close on overlay click
$overlay.on('click', function() {
hideLocationPopup();
});
// Close on X button
$popup.find('.location-popup-close').on('click', function() {
hideLocationPopup();
});
// Close on Escape key
$(document).on('keydown', function(e) {
if (e.key === 'Escape' && $popup.is(':visible')) {
hideLocationPopup();
}
});
});
// Vendor filter functionality
$(document).ready(function() {
$('#vendorFilter').on('change', function() {
var selectedVendor = $(this).val();
if (selectedVendor === 'all') {
// Show all rows
$('tbody tr[data-vendor]').show();
} else {
// Hide all rows first
$('tbody tr[data-vendor]').hide();
// Show only matching vendor rows
$('tbody tr[data-vendor="' + selectedVendor + '"]').show();
}
});
});
</script>
</body>
</html>