Show every status the asset total contains, and what it excludes
Asked twice what "All assets" covered, which is the signal a label is underspecified rather than a reader being slow. The endpoint already counted every status to build its rollup and then returned two of them, so a reader could see In Use and had no way to learn where the rest of the total went. It now returns them all, and the panel lists them under the total - indented and quieter, because they break the total down rather than adding to it. Three things this makes visible that were not guessable: Retired assets are IN the total. Retired is a status on an active record, not the same as deactivated, and seven of them sit inside this site's 610. There is an "Inventory" status. I would have hardcoded In Use, In Repair and Retired and silently dropped it. The rows are built from whatever the API returns, sorted by count, so a status a site adds appears rather than disappearing - which matters for sites adopting with their own seed data. Rows held back by isactive are in NO status bucket, so they cannot be found by adding the buckets up. That gap is exactly what makes a dashboard total disagree with a list page, so the count is reported and captioned. The caption states the counting policy - active records only, dual-bay pairs counted once - and the dual-path clause renders only when that site toggle is on, since asserting it unconditionally would be false at a site that has it off. Full suite 2182, frontend 314.
This commit is contained in:
@@ -69,14 +69,24 @@
|
||||
<span class="assets-v">{{ stats.totalassets || 0 }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Status, on one line, and explicitly labelled as status. In
|
||||
repair is muted at zero rather than given a card of its own to
|
||||
announce that nothing is wrong. -->
|
||||
<p class="assets-status">
|
||||
<span class="assets-inuse">{{ stats.activeassets || 0 }}</span> in use
|
||||
<span aria-hidden="true"> / </span>
|
||||
<span :class="{ 'assets-zero': !stats.inrepair }">
|
||||
{{ stats.inrepair || 0 }} in repair
|
||||
<!-- Every status the total contains, not just In Use. These were
|
||||
counted server-side for the rollup and then thrown away, so a
|
||||
reader could see one number and had no way to learn where the
|
||||
rest of the total went. Retired assets in particular ARE in
|
||||
the total, which is not guessable. -->
|
||||
<div v-for="row in statusrows" :key="row.name" class="assets-row assets-sub">
|
||||
<span class="assets-k assets-subk">{{ row.name }}</span>
|
||||
<span class="assets-v assets-subv">{{ row.count }}</span>
|
||||
</div>
|
||||
|
||||
<!-- What the total covers. Asked twice what the number meant, which
|
||||
is the signal a label is underspecified. -->
|
||||
<p class="assets-caption">
|
||||
Active records only<span v-if="stats.dualpathcollapsed">, dual-bay
|
||||
pairs counted once</span>.
|
||||
<span v-if="stats.inactiveexcluded">
|
||||
{{ stats.inactiveexcluded }} deactivated record{{ stats.inactiveexcluded === 1 ? '' : 's' }}
|
||||
excluded.
|
||||
</span>
|
||||
</p>
|
||||
</section>
|
||||
@@ -133,6 +143,17 @@ const summaryloaded = ref(false)
|
||||
// hold - and to what the guest role reaches, when nobody is signed in.
|
||||
const navitems = ref([])
|
||||
|
||||
// Every status in the total, biggest first. Sorted rather than fixed-order
|
||||
// because the statuses are seed data a site can change - hardcoding "In Use,
|
||||
// In Repair, Retired" would silently omit a status somebody added.
|
||||
const statusrows = computed(() => {
|
||||
const statuses = stats.value.statuses || {}
|
||||
return Object.keys(statuses)
|
||||
.map((name) => ({ name, count: statuses[name] }))
|
||||
.filter((row) => row.count > 0)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
})
|
||||
|
||||
const openable = computed(
|
||||
// The dashboard itself is not somewhere to go from the dashboard.
|
||||
() => navitems.value.filter((item) => item.route && item.route !== '/'))
|
||||
@@ -224,11 +245,18 @@ span.assets-k { color: var(--text); font-weight: 600; }
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.assets-status {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
/* Status rows sit UNDER the total, indented and quieter, because they break
|
||||
the total down rather than adding to it. Same weight as the type rows would
|
||||
read as more things to add up. */
|
||||
.assets-sub { border-bottom: none; padding: 0.15rem 0 0.15rem 0.75rem; }
|
||||
.assets-subk { color: var(--text-light); font-size: 0.85rem; }
|
||||
a.assets-subk:hover { text-decoration: none; }
|
||||
.assets-subv { font-weight: 500; font-size: 0.85rem; color: var(--text-light); }
|
||||
|
||||
.assets-caption {
|
||||
margin: 0.6rem 0 0;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
color: var(--text-light);
|
||||
}
|
||||
.assets-inuse { color: var(--success); font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.assets-zero { color: var(--text-light); }
|
||||
</style>
|
||||
|
||||
@@ -38,6 +38,19 @@ def _count_by_type(assettype):
|
||||
return query.count()
|
||||
|
||||
|
||||
def _inactive_excluded():
|
||||
"""Counted-type assets held back by isactive, i.e. soft-deleted.
|
||||
|
||||
Reported so the page can account for itself. A total that silently drops
|
||||
rows is the reason somebody compares it with a list page and finds a
|
||||
difference they cannot explain.
|
||||
"""
|
||||
return (db.session.query(Asset).join(AssetType)
|
||||
.filter(Asset.isactive == False,
|
||||
AssetType.assettype.in_(COUNTEDTYPES))
|
||||
.count())
|
||||
|
||||
|
||||
COUNTEDTYPES = ('machine', 'computer', 'network_device', 'printer',
|
||||
'measuring_tool')
|
||||
|
||||
@@ -69,6 +82,10 @@ def _countedassets():
|
||||
@require_permission('assets.view')
|
||||
def get_dashboard():
|
||||
"""Get dashboard summary data (asset-based)."""
|
||||
# Imported here, as in the helpers above: the dualpath service reaches back
|
||||
# into models, so a module-level import is circular.
|
||||
from shopdb.core.services.dualpath import dualpath_single_machine_enabled
|
||||
|
||||
machine_count = _count_by_type('machine')
|
||||
pc_count = _count_by_type('computer')
|
||||
network_count = _count_by_type('network_device')
|
||||
@@ -99,6 +116,22 @@ def get_dashboard():
|
||||
'totalmeasuringtool': measuringtool_count,
|
||||
'activeassets': status_dict.get('In Use', 0),
|
||||
'inrepair': status_dict.get('In Repair', 0),
|
||||
# What the totals actually cover, so the page can say so rather than
|
||||
# leaving a reader to work it out from a number that surprised them.
|
||||
# Every count here filters isactive, and the dual-path collapse is a
|
||||
# per-site toggle - asserting it unconditionally would be false at a
|
||||
# site that has it off.
|
||||
'countsactiveonly': True,
|
||||
'dualpathcollapsed': bool(dualpath_single_machine_enabled()),
|
||||
# Every status, not just the two that had callers. The counts were
|
||||
# already built for the rollup above and then thrown away, so a reader
|
||||
# could see "In Use" and had no way to learn where the rest went.
|
||||
'statuses': status_dict,
|
||||
# A DIFFERENT thing to a status, and the one that makes a total
|
||||
# disagree with a list page. isactive is the soft-delete flag: these
|
||||
# rows are excluded from every count above, so they are in no status
|
||||
# bucket and cannot be found by adding the buckets up.
|
||||
'inactiveexcluded': _inactive_excluded(),
|
||||
# Structured data
|
||||
'counts': {
|
||||
'machines': machine_count,
|
||||
|
||||
Reference in New Issue
Block a user