dashboard: tighten the printer, warranty and notification cards
PRINTER CARD is now supplies at or below 5 percent, a new printers_dashboardpercent setting. The report and the card want different scopes: the report lists anything the thresholds call low, which is right for planning an order, while the dashboard is asking what to walk out and change today - and a cartridge at 18 percent is not that. Lowest first. PRINTER LOCATION WAS ALWAYS EMPTY. The lookup went through db.session.get on locationid and produced nothing even where a location is set; the printers list has always read it through the asset relationship, so the card does too now. WARRANTY ROWS are identified the way the floor identifies them: the PC's hostname and the MACHINE it drives, reusing the same lookup behind the warranty page's machine column so the board and the report cannot disagree about which bay a PC belongs to. No dates - expired or expiring is the whole decision when scanning a board, and the exact day belongs on the report you order from. NOTIFICATIONS are stacked: the type in full on one line, the message beneath, trimmed to 100 characters with the rest on hover. Inline, the type was truncated to make room for prose that was then truncated anyway, and neither read. A tooltip is omitted when the text was not trimmed, because one repeating what is already on screen is noise. Two general additions: layout: 'stacked' on a card, and map.detailtooltip.
This commit is contained in:
@@ -897,10 +897,16 @@ def dashboard_active():
|
||||
out = []
|
||||
for row in rows:
|
||||
notificationtype = getattr(row, 'notificationtype', None)
|
||||
text = (row.notification or '').strip()
|
||||
out.append({
|
||||
'notificationid': row.notificationid,
|
||||
'message': (row.notification or '').strip()[:120],
|
||||
'typename': notificationtype.typename if notificationtype else None,
|
||||
# Trimmed for the card, whole text for the tooltip. A notification
|
||||
# is prose written for the floor, so the first hundred characters
|
||||
# are usually enough to recognise which one it is, and the rest is
|
||||
# there when it is not.
|
||||
'message': text[:100] + ('...' if len(text) > 100 else ''),
|
||||
'fullmessage': text,
|
||||
'typename': notificationtype.typename if notificationtype else 'Notification',
|
||||
'endtime': row.endtime.isoformat() + 'Z' if row.endtime else None,
|
||||
})
|
||||
return success_response(out)
|
||||
|
||||
@@ -211,11 +211,15 @@ class NotificationsPlugin(BasePlugin):
|
||||
'severity': 'info',
|
||||
'empty': 'hide',
|
||||
'position': 60,
|
||||
# Stacked, not inline: the type is a label and the message is
|
||||
# prose. On one line the type gets truncated to make room for
|
||||
# text that then gets truncated anyway, and neither is
|
||||
# readable.
|
||||
'layout': 'stacked',
|
||||
'map': {
|
||||
'title': 'typename',
|
||||
'detail': 'message',
|
||||
'meta': [{'key': 'endtime', 'label': 'until',
|
||||
'format': 'date'}],
|
||||
'detailtooltip': 'fullmessage',
|
||||
'link': '/notifications/{notificationid}',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1020,12 +1020,11 @@ def _get_low_supplies_data():
|
||||
|
||||
if has_low:
|
||||
# location name for the report row
|
||||
location_name = None
|
||||
if asset.locationid:
|
||||
from shopdb.api import Location
|
||||
loc = db.session.get(Location, asset.locationid)
|
||||
if loc:
|
||||
location_name = loc.locationname
|
||||
# Via the relationship, the way the printers list does it. The
|
||||
# previous lookup went through db.session.get on locationid, and
|
||||
# every row came back with no location even where one is set.
|
||||
location_name = (asset.location.locationname
|
||||
if asset.location else None)
|
||||
|
||||
results.append({
|
||||
'printerid': printer.printerid,
|
||||
@@ -1441,27 +1440,37 @@ def dashboard_supplies():
|
||||
"""
|
||||
data = _get_low_supplies_data()
|
||||
|
||||
threshold = 5
|
||||
setting = Setting.query.filter_by(key='printers_dashboardpercent').first()
|
||||
if setting and (setting.value or '').strip():
|
||||
try:
|
||||
threshold = int(setting.value)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
rows = []
|
||||
for printer in data.get('printers', []):
|
||||
criticals = [s for s in printer['supplies'] if s['status'] == 'critical']
|
||||
lows = [s for s in printer['supplies'] if s['status'] == 'low']
|
||||
if not criticals and not lows:
|
||||
# The CARD is tighter than the report. The report lists anything the
|
||||
# thresholds call low, which is the right scope for planning an order;
|
||||
# the dashboard is asking what to walk out and change today, and a
|
||||
# cartridge at 18% is not that. Anything at or below the threshold.
|
||||
depleted = [supply for supply in printer['supplies']
|
||||
if isinstance(supply.get('remaining'), (int, float))
|
||||
and supply['remaining'] <= threshold]
|
||||
if not depleted:
|
||||
continue
|
||||
# Criticals first. Every depleted supply is listed, not just the worst
|
||||
# class - whoever walks out there wants to carry both cartridges.
|
||||
depleted.sort(key=lambda supply: supply['remaining'])
|
||||
rows.append({
|
||||
'printerid': printer['printerid'],
|
||||
'printername': printer['printername'] or printer['assetnumber'],
|
||||
'location': printer['location'] or 'No location set',
|
||||
'iscritical': bool(criticals),
|
||||
# One chip per depleted supply: what is low, and on hover what to
|
||||
# order to fix it.
|
||||
'iscritical': any(s['status'] == 'critical' for s in depleted),
|
||||
'supplies': [{
|
||||
'text': '{} {}%'.format(_shortsupplyname(supply.get('name')),
|
||||
supply.get('remaining')),
|
||||
'title': _reordertip(supply),
|
||||
'level': supply.get('status'),
|
||||
} for supply in criticals + lows],
|
||||
} for supply in depleted],
|
||||
})
|
||||
|
||||
rows.sort(key=lambda r: (not r['iscritical'], r['printername']))
|
||||
|
||||
@@ -286,6 +286,21 @@ class PrintersPlugin(BasePlugin):
|
||||
|
||||
return [printerscli]
|
||||
|
||||
def get_settings_defaults(self) -> List[Dict]:
|
||||
"""Settings this plugin owns for the dashboard card."""
|
||||
return [
|
||||
{
|
||||
'key': 'printers_dashboardpercent',
|
||||
'value': '5',
|
||||
'valuetype': 'integer',
|
||||
'category': 'printers',
|
||||
'description': 'Supply percentage at or below which a printer '
|
||||
'appears on the dashboard. Tighter than the '
|
||||
'low-supplies report, which is for planning an '
|
||||
'order rather than walking out to change one.',
|
||||
},
|
||||
]
|
||||
|
||||
def get_dashboard_widgets(self) -> List[Dict]:
|
||||
"""Dashboard card: printers needing a cartridge.
|
||||
|
||||
|
||||
@@ -399,13 +399,16 @@ def warranty_report():
|
||||
def dashboard_expiring():
|
||||
"""Warranties running out, and ones that already have.
|
||||
|
||||
Already-expired first, then soonest. Expired stays on the list rather than
|
||||
dropping off: a machine out of warranty is a purchasing decision someone
|
||||
still has to make, and silently removing it the day it lapses is how it gets
|
||||
missed entirely.
|
||||
Identified the way the floor identifies them: the PC's hostname and the
|
||||
MACHINE it drives. Nobody walks out looking for an asset number, and a
|
||||
warranty row for a bay PC is only actionable if you know which bay.
|
||||
|
||||
Horizon is a setting because 90 days suits a site that budgets quarterly and
|
||||
nobody else (ADR-015).
|
||||
No dates. "Expired" or "expiring" is the whole decision - the exact day
|
||||
matters when you are placing the order, which is what the warranty report
|
||||
is for, not when you are scanning a board.
|
||||
|
||||
Already-expired stays listed rather than dropping off the day it lapses,
|
||||
which is how one gets missed entirely.
|
||||
"""
|
||||
from shopdb.api import Setting
|
||||
|
||||
@@ -421,7 +424,7 @@ def dashboard_expiring():
|
||||
horizon = today + timedelta(days=days)
|
||||
|
||||
rows = []
|
||||
query = (db.session.query(Warranty, WarrantyAsset, Asset)
|
||||
query = (db.session.query(Warranty, Asset)
|
||||
.join(WarrantyAsset, WarrantyAsset.warrantyid == Warranty.warrantyid)
|
||||
.join(Asset, Asset.assetid == WarrantyAsset.assetid)
|
||||
.filter(Warranty.isactive.is_(True),
|
||||
@@ -429,16 +432,33 @@ def dashboard_expiring():
|
||||
Warranty.enddate <= horizon,
|
||||
Asset.isactive.is_(True)))
|
||||
|
||||
for warranty, _link, asset in query.all():
|
||||
for warranty, asset in query.all():
|
||||
remaining = (warranty.enddate - today).days
|
||||
# Reuses the machine lookup behind the warranty page's machine column,
|
||||
# so the board and the report agree about which bay a PC belongs to.
|
||||
machine = _related_machine(asset)
|
||||
rows.append({
|
||||
'assetid': asset.assetid,
|
||||
'assetnumber': asset.assetnumber or asset.name or str(asset.assetid),
|
||||
'enddate': warranty.enddate.isoformat(),
|
||||
'provider': warranty.provider,
|
||||
'daysleft': remaining,
|
||||
'hostname': _hostname(asset) or asset.assetnumber or asset.name,
|
||||
'machinenumber': machine['machinenumber'] if machine else None,
|
||||
'state': 'expired' if remaining < 0 else 'expiring',
|
||||
'daysleft': remaining,
|
||||
})
|
||||
|
||||
rows.sort(key=lambda r: r['daysleft'])
|
||||
return success_response(rows[:50])
|
||||
|
||||
|
||||
def _hostname(asset):
|
||||
"""The PC's hostname, when the covered asset is a PC.
|
||||
|
||||
Optional import: a lean site may not install the computers plugin, and a
|
||||
warranty on a printer or a machine has no hostname at all. Falls back to
|
||||
the asset number, which is what the row shows either way.
|
||||
"""
|
||||
try:
|
||||
from plugins.computers.models import Computer
|
||||
except ImportError:
|
||||
return None
|
||||
computer = Computer.query.filter_by(assetid=asset.assetid).first()
|
||||
return computer.hostname if computer else None
|
||||
|
||||
@@ -133,12 +133,13 @@ class WarrantyPlugin(BasePlugin):
|
||||
'empty': 'hide',
|
||||
'position': 50,
|
||||
'map': {
|
||||
'title': 'assetnumber',
|
||||
'title': 'hostname',
|
||||
'detail': 'state',
|
||||
'meta': [
|
||||
{'key': 'enddate', 'label': 'ends', 'format': 'date'},
|
||||
{'key': 'provider'},
|
||||
],
|
||||
# The bay, which is how the floor identifies the PC. No
|
||||
# date: expired or expiring is the whole decision when
|
||||
# scanning a board, and the exact day belongs on the report
|
||||
# you order from.
|
||||
'meta': [{'key': 'machinenumber', 'label': 'machine'}],
|
||||
'link': '/assets/{assetid}',
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user