diff --git a/frontend/src/components/DashboardCards.vue b/frontend/src/components/DashboardCards.vue
index 35752f8..847f3a1 100644
--- a/frontend/src/components/DashboardCards.vue
+++ b/frontend/src/components/DashboardCards.vue
@@ -18,11 +18,17 @@
-
-
+
{{ row.title }}
- {{ row.title }}
+ {{ row.title }}
{{ row.detail }}
+
+ {{ chip.text }}
+
{{ row.meta.map((m) => m.text).join(' / ') }}
@@ -204,6 +210,21 @@ defineExpose({ load })
overflow: hidden;
text-overflow: ellipsis;
}
+/* One chip per depleted supply. Bordered rather than filled: a row of solid
+ red pills reads as an emergency even when a cartridge is merely low. */
+.dc-chip {
+ flex: none;
+ padding: 0.05rem 0.4rem;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ font-size: 0.75rem;
+ color: var(--text);
+ white-space: nowrap;
+ cursor: help;
+}
+.dc-chip-critical { border-color: var(--danger); color: var(--danger); }
+.dc-chip-low { border-color: var(--warning); }
+
.dc-more {
display: block;
margin: 0.5rem 0 0;
diff --git a/frontend/src/components/dashboardCards.js b/frontend/src/components/dashboardCards.js
index 2b95d59..8f9e236 100644
--- a/frontend/src/components/dashboardCards.js
+++ b/frontend/src/components/dashboardCards.js
@@ -89,11 +89,34 @@ export function overflowCount(card) {
return Math.max(0, rows(card).length - MAXROWS)
}
+// Chips are the row's own small facts, each carrying a tooltip: a depleted
+// cartridge shows "Black 4%" and reveals the part number to order on hover.
+// The percentage says something is wrong; the tooltip says what to do about
+// it, which otherwise means opening the printer's page to find out.
+export function mapChips(card, item) {
+ const key = card.map && card.map.chips
+ const list = key ? item[key] : null
+ if (!Array.isArray(list)) return []
+ return list
+ .filter((chip) => chip && chip.text)
+ .map((chip) => ({ text: chip.text, title: chip.title || '', level: chip.level || '' }))
+}
+
+// A tooltip on the row title, for context that would clutter the line - a
+// printer's location, say. Never load-bearing: hover is not discoverable and
+// does not exist on touch, so nothing essential goes here.
+export function mapTitleTip(card, item) {
+ const key = card.map && card.map.titletooltip
+ return key ? (item[key] || '') : ''
+}
+
export function cardRows(card) {
return rows(card).map((item) => ({
title: mapTitle(card, item),
+ titletip: mapTitleTip(card, item),
detail: mapDetail(card, item),
meta: mapMeta(card, item),
+ chips: mapChips(card, item),
link: mapLink(card, item),
timestamp: card.map && card.map.timestamp ? item[card.map.timestamp] : null,
}))
diff --git a/frontend/src/components/dashboardCards.spec.js b/frontend/src/components/dashboardCards.spec.js
index 3e7aa69..db7779b 100644
--- a/frontend/src/components/dashboardCards.spec.js
+++ b/frontend/src/components/dashboardCards.spec.js
@@ -163,3 +163,42 @@ describe('gating', () => {
expect(kept.map((c) => c.id)).toEqual(['ok'])
})
})
+
+describe('chips and tooltips', () => {
+ const printerCard = {
+ render: 'exceptions',
+ map: {
+ title: 'printername',
+ titletooltip: 'location',
+ chips: 'supplies',
+ link: '/printers/{printerid}',
+ },
+ _data: [{
+ printerid: 7,
+ printername: 'WJ-HP-402',
+ location: 'Cell B, north wall',
+ supplies: [
+ { text: 'Black 4%', title: 'CF226X (high)', level: 'critical' },
+ { text: 'Cyan 9%', title: 'No part number on file for this model', level: 'low' },
+ ],
+ }],
+ }
+
+ it('builds one chip per depleted supply, each with its reorder tooltip', () => {
+ const [row] = cardRows(printerCard)
+ expect(row.chips.map((c) => c.text)).toEqual(['Black 4%', 'Cyan 9%'])
+ expect(row.chips[0].title).toBe('CF226X (high)')
+ expect(row.chips[0].level).toBe('critical')
+ })
+
+ it('puts the location on the title as a tooltip, not in the line', () => {
+ const [row] = cardRows(printerCard)
+ expect(row.titletip).toBe('Cell B, north wall')
+ expect(row.detail).toBe('')
+ })
+
+ it('has no chips when a card declares none', () => {
+ expect(cardRows({ render: 'exceptions', map: { title: 'x' }, _data: [{ x: 'y' }] })[0].chips)
+ .toEqual([])
+ })
+})
diff --git a/plugins/printers/api/asset_routes.py b/plugins/printers/api/asset_routes.py
index 399f753..1aaed1e 100644
--- a/plugins/printers/api/asset_routes.py
+++ b/plugins/printers/api/asset_routes.py
@@ -1407,6 +1407,24 @@ def _shortsupplyname(name):
return text or 'supply'
+def _reordertip(supply):
+ """What to order, for the chip's tooltip.
+
+ A percentage says a cartridge is nearly out; the part number says what to
+ buy, which is the next thing someone needs and today means opening the
+ printer's page to find it. Every capacity tier is listed, because the
+ report has always shown all reorder options.
+ """
+ parts = supply.get('partnumbers') or []
+ if not parts:
+ return 'No part number on file for this model'
+ return ', '.join(
+ '{}{}'.format(part['partnumber'],
+ ' ({})'.format(part['capacitytier'])
+ if part.get('capacitytier') else '')
+ for part in parts)
+
+
@printers_asset_bp.route('/dashboard/supplies', methods=['GET'])
@jwt_required()
@require_permission('printers.view')
@@ -1429,22 +1447,21 @@ def dashboard_supplies():
lows = [s for s in printer['supplies'] if s['status'] == 'low']
if not criticals and not lows:
continue
- # Every depleted supply, criticals first. Listing only the worst class
- # would hide a low cartridge behind a critical one on the same printer,
- # and whoever walks out there wants to carry both.
- worst = criticals + lows
+ # Criticals first. Every depleted supply is listed, not just the worst
+ # class - whoever walks out there wants to carry both cartridges.
rows.append({
'printerid': printer['printerid'],
'printername': printer['printername'] or printer['assetnumber'],
- 'location': printer['location'],
- 'status': 'critical' if criticals else 'low',
- # 'remaining' is the percent left. There is no 'percent' key -
- # reading one rendered every cartridge as "None%" on the board.
- 'supplies': ', '.join(
- '{} {}%'.format(_shortsupplyname(s.get('name')),
- s.get('remaining'))
- for s in worst),
+ '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.
+ 'supplies': [{
+ 'text': '{} {}%'.format(_shortsupplyname(supply.get('name')),
+ supply.get('remaining')),
+ 'title': _reordertip(supply),
+ 'level': supply.get('status'),
+ } for supply in criticals + lows],
})
rows.sort(key=lambda r: (not r['iscritical'], r['printername']))
diff --git a/plugins/printers/plugin.py b/plugins/printers/plugin.py
index 85a75cd..b4df675 100644
--- a/plugins/printers/plugin.py
+++ b/plugins/printers/plugin.py
@@ -306,8 +306,11 @@ class PrintersPlugin(BasePlugin):
'position': 40,
'map': {
'title': 'printername',
- 'detail': 'supplies',
- 'meta': [{'key': 'location'}, {'key': 'status'}],
+ # Location on hover rather than on the line: it is context
+ # for "where do I walk", not part of the finding, and it
+ # was the text pushing rows past the card edge.
+ 'titletooltip': 'location',
+ 'chips': 'supplies',
'link': '/printers/{printerid}',
},
},