Dashboard corrections
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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}',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user