diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index ad886ee4..1555f7c3 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -69,14 +69,24 @@ {{ stats.totalassets || 0 }} - -

- {{ stats.activeassets || 0 }} in use - - - {{ stats.inrepair || 0 }} in repair + +

+ {{ row.name }} + {{ row.count }} +
+ + +

+ Active records only, dual-bay + pairs counted once. + + {{ stats.inactiveexcluded }} deactivated record{{ stats.inactiveexcluded === 1 ? '' : 's' }} + excluded.

@@ -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); } diff --git a/shopdb/core/api/dashboard.py b/shopdb/core/api/dashboard.py index d8350d15..5d197e22 100644 --- a/shopdb/core/api/dashboard.py +++ b/shopdb/core/api/dashboard.py @@ -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,