Dashboard corrections

This commit is contained in:
2026-08-11 16:27:14 -04:00
parent 2d19bcdf60
commit 568dfbb91c
7 changed files with 85 additions and 13 deletions

View File

@@ -19,7 +19,17 @@
<ul v-else class="dc-rows">
<li v-for="(row, index) in visibleRows(card)" :key="index" class="dc-row"
:class="{ 'dc-row-stacked': card.layout === 'stacked' }">
<router-link v-if="row.link" :to="row.link" class="dc-row-title"
<!-- With coordinates, the name carries the same floor-plan preview
the asset's own page uses. Without them, a plain link. -->
<LocationMapTooltip v-if="row.link && row.maphover"
:left="row.maphover.x" :top="row.maphover.y"
:machineName="row.maphover.label">
<router-link :to="row.link" class="dc-row-title"
:title="row.titletip || undefined">
{{ row.title }}
</router-link>
</LocationMapTooltip>
<router-link v-else-if="row.link" :to="row.link" class="dc-row-title"
:title="row.titletip || undefined">
{{ row.title }}
</router-link>
@@ -64,6 +74,7 @@
import { ref, computed, onMounted } from 'vue'
import api from '../api'
import { useAuthStore } from '@/stores/auth'
import LocationMapTooltip from '@/components/LocationMapTooltip.vue'
import {
toApiPath, visibleRows, overflowCount, metricValue, cardVisible, sortCards,
permittedCards, renderableCards, rows as cardData,
@@ -109,7 +120,12 @@ defineExpose({ load })
<style scoped>
.dc-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
/* Four across on a wide screen, not five. A card holds a hostname, a machine
number and a state on one line; at five columns those lines truncate on
the very rows that matter most. auto-fit rather than auto-fill so two
cards fill the width instead of leaving three empty tracks. */
grid-template-columns: repeat(auto-fit, minmax(28rem, 1fr));
max-width: 120rem;
gap: 1rem;
margin-bottom: 1.75rem;
align-items: start;

View File

@@ -118,10 +118,23 @@ export function mapTitleTip(card, item) {
return key ? (item[key] || '') : ''
}
// Floor-plan preview on hover, for rows that carry coordinates. Returns null
// when the card declares none or the row has not been placed on the map - an
// unplaced printer still belongs on the card, it just has nothing to preview.
export function mapHover(card, item) {
const spec = card.map && card.map.maphover
if (!spec) return null
const x = item[spec.x]
const y = item[spec.y]
if (x === null || x === undefined || y === null || y === undefined) return null
return { x, y, label: spec.label ? (item[spec.label] || '') : '' }
}
export function cardRows(card) {
return rows(card).map((item) => ({
title: mapTitle(card, item),
titletip: mapTitleTip(card, item),
maphover: mapHover(card, item),
detail: mapDetail(card, item),
detailtip: mapDetailTip(card, item),
meta: mapMeta(card, item),

View File

@@ -1034,6 +1034,8 @@ def _get_low_supplies_data():
'vendor': vendor_name,
'model': model_number,
'location': location_name,
'mapx': asset.mapx,
'mapy': asset.mapy,
'supplies': annotated
})
@@ -1464,6 +1466,11 @@ def dashboard_supplies():
'printerid': printer['printerid'],
'printername': printer['printername'] or printer['assetnumber'],
'location': printer['location'] or 'No location set',
# Coordinates for the hover preview. Either may be None - a
# printer never placed on the floor plan still belongs on the
# card, it just has nothing to preview.
'mapx': printer.get('mapx'),
'mapy': printer.get('mapy'),
'iscritical': any(s['status'] == 'critical' for s in depleted),
'supplies': [{
'text': '{} {}%'.format(_shortsupplyname(supply.get('name')),

View File

@@ -325,6 +325,12 @@ class PrintersPlugin(BasePlugin):
# for "where do I walk", not part of the finding, and it
# was the text pushing rows past the card edge.
'titletooltip': 'location',
# Hovering the name shows the floor-plan preview, the
# same component the printer's own page uses: a
# location name tells you the room, the map tells you
# where to walk.
'maphover': {'x': 'mapx', 'y': 'mapy',
'label': 'printername'},
'chips': 'supplies',
'link': '/printers/{printerid}',
},

View File

@@ -439,7 +439,11 @@ def dashboard_expiring():
machine = _related_machine(asset)
rows.append({
'assetid': asset.assetid,
'hostname': _hostname(asset) or asset.assetnumber or asset.name,
# Hostname, else the asset number - never asset.name. The name
# of a covered PC's ASSET is often the machine's descriptive name
# ("Haas VF-2"), so falling back to it put a machine name in a
# column that is supposed to identify the PC.
'hostname': _hostname(asset) or asset.assetnumber or str(asset.assetid),
'machinenumber': machine['machinenumber'] if machine else None,
'state': 'expired' if remaining < 0 else 'expiring',
'daysleft': remaining,

View File

@@ -139,7 +139,10 @@ class WarrantyPlugin(BasePlugin):
# 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'}],
# Bare number, no label. The row is hostname, machine,
# state - three short facts, and "machine 3015" spends a
# word saying what the position already says.
'meta': [{'key': 'machinenumber'}],
'link': '/assets/{assetid}',
},
},

View File

@@ -37,6 +37,32 @@ def _count_by_type(assettype):
return query.count()
COUNTEDTYPES = ('machine', 'computer', 'network_device', 'printer',
'measuring_tool')
def _countedassets():
"""Base query for the assets the dashboard totals describe.
The status tiles and the type tiles MUST count the same population. They
did not: the totals summed five specific types and subtracted dual-bay
secondaries, while the status counts took every asset row of any type with
no collapse. So "all assets 704" sat beside "in use 737", and both were
correct about different things - which makes them worse than either alone.
"""
from shopdb.core.services.dualpath import (
resolve_dualpath_pairs, dualpath_single_machine_enabled)
query = (db.session.query(Asset).join(AssetType)
.filter(Asset.isactive == True,
AssetType.assettype.in_(COUNTEDTYPES)))
if dualpath_single_machine_enabled():
secondaryassetids = resolve_dualpath_pairs().secondaryassetids
if secondaryassetids:
query = query.filter(Asset.assetid.notin_(secondaryassetids))
return query
@dashboard_bp.route('/summary', methods=['GET'])
@dashboard_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@@ -49,15 +75,12 @@ def get_dashboard():
measuringtool_count = _count_by_type('measuring_tool')
total = machine_count + pc_count + network_count + printer_count + measuringtool_count
# Count by status
status_counts = db.session.query(
AssetStatus.status,
db.func.count(Asset.assetid)
).outerjoin(
Asset,
db.and_(Asset.statusid == AssetStatus.statusid, Asset.isactive == True)
).group_by(AssetStatus.status).all()
status_dict = {status: count for status, count in status_counts}
# Count by status over the SAME population as the totals above, so the
# tiles can be read against each other.
status_dict = {}
for asset in _countedassets().all():
name = asset.status.status if asset.status else 'Unknown'
status_dict[name] = status_dict.get(name, 0) + 1
# Recent assets
recent = Asset.query.filter_by(isactive=True).order_by(