Printer card chips

This commit is contained in:
2026-08-11 15:46:21 -04:00
parent 34cb9a5e3e
commit 793dfab5c8
5 changed files with 119 additions and 16 deletions

View File

@@ -18,11 +18,17 @@
<!-- exceptions / list: one line per thing, each linking to itself -->
<ul v-else class="dc-rows">
<li v-for="(row, index) in visibleRows(card)" :key="index" class="dc-row">
<router-link v-if="row.link" :to="row.link" class="dc-row-title">
<router-link v-if="row.link" :to="row.link" class="dc-row-title"
:title="row.titletip || undefined">
{{ row.title }}
</router-link>
<span v-else class="dc-row-title dc-row-nolink">{{ row.title }}</span>
<span v-else class="dc-row-title dc-row-nolink"
:title="row.titletip || undefined">{{ row.title }}</span>
<span v-if="row.detail" class="dc-row-detail">{{ row.detail }}</span>
<span v-for="(chip, c) in row.chips" :key="c" class="dc-chip"
:class="'dc-chip-' + (chip.level || 'low')" :title="chip.title">
{{ chip.text }}
</span>
<span v-if="row.meta.length" class="dc-row-meta">
{{ row.meta.map((m) => m.text).join(' / ') }}
</span>
@@ -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;

View File

@@ -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,
}))

View File

@@ -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([])
})
})

View File

@@ -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']))

View File

@@ -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}',
},
},