Dashboard corrections
This commit is contained in:
@@ -19,7 +19,17 @@
|
|||||||
<ul v-else class="dc-rows">
|
<ul v-else class="dc-rows">
|
||||||
<li v-for="(row, index) in visibleRows(card)" :key="index" class="dc-row"
|
<li v-for="(row, index) in visibleRows(card)" :key="index" class="dc-row"
|
||||||
:class="{ 'dc-row-stacked': card.layout === 'stacked' }">
|
: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">
|
:title="row.titletip || undefined">
|
||||||
{{ row.title }}
|
{{ row.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -64,6 +74,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import api from '../api'
|
import api from '../api'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import LocationMapTooltip from '@/components/LocationMapTooltip.vue'
|
||||||
import {
|
import {
|
||||||
toApiPath, visibleRows, overflowCount, metricValue, cardVisible, sortCards,
|
toApiPath, visibleRows, overflowCount, metricValue, cardVisible, sortCards,
|
||||||
permittedCards, renderableCards, rows as cardData,
|
permittedCards, renderableCards, rows as cardData,
|
||||||
@@ -109,7 +120,12 @@ defineExpose({ load })
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.dc-grid {
|
.dc-grid {
|
||||||
display: 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;
|
gap: 1rem;
|
||||||
margin-bottom: 1.75rem;
|
margin-bottom: 1.75rem;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|||||||
@@ -118,10 +118,23 @@ export function mapTitleTip(card, item) {
|
|||||||
return key ? (item[key] || '') : ''
|
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) {
|
export function cardRows(card) {
|
||||||
return rows(card).map((item) => ({
|
return rows(card).map((item) => ({
|
||||||
title: mapTitle(card, item),
|
title: mapTitle(card, item),
|
||||||
titletip: mapTitleTip(card, item),
|
titletip: mapTitleTip(card, item),
|
||||||
|
maphover: mapHover(card, item),
|
||||||
detail: mapDetail(card, item),
|
detail: mapDetail(card, item),
|
||||||
detailtip: mapDetailTip(card, item),
|
detailtip: mapDetailTip(card, item),
|
||||||
meta: mapMeta(card, item),
|
meta: mapMeta(card, item),
|
||||||
|
|||||||
@@ -1034,6 +1034,8 @@ def _get_low_supplies_data():
|
|||||||
'vendor': vendor_name,
|
'vendor': vendor_name,
|
||||||
'model': model_number,
|
'model': model_number,
|
||||||
'location': location_name,
|
'location': location_name,
|
||||||
|
'mapx': asset.mapx,
|
||||||
|
'mapy': asset.mapy,
|
||||||
'supplies': annotated
|
'supplies': annotated
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1464,6 +1466,11 @@ def dashboard_supplies():
|
|||||||
'printerid': printer['printerid'],
|
'printerid': printer['printerid'],
|
||||||
'printername': printer['printername'] or printer['assetnumber'],
|
'printername': printer['printername'] or printer['assetnumber'],
|
||||||
'location': printer['location'] or 'No location set',
|
'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),
|
'iscritical': any(s['status'] == 'critical' for s in depleted),
|
||||||
'supplies': [{
|
'supplies': [{
|
||||||
'text': '{} {}%'.format(_shortsupplyname(supply.get('name')),
|
'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
|
# for "where do I walk", not part of the finding, and it
|
||||||
# was the text pushing rows past the card edge.
|
# was the text pushing rows past the card edge.
|
||||||
'titletooltip': 'location',
|
'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',
|
'chips': 'supplies',
|
||||||
'link': '/printers/{printerid}',
|
'link': '/printers/{printerid}',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -439,7 +439,11 @@ def dashboard_expiring():
|
|||||||
machine = _related_machine(asset)
|
machine = _related_machine(asset)
|
||||||
rows.append({
|
rows.append({
|
||||||
'assetid': asset.assetid,
|
'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,
|
'machinenumber': machine['machinenumber'] if machine else None,
|
||||||
'state': 'expired' if remaining < 0 else 'expiring',
|
'state': 'expired' if remaining < 0 else 'expiring',
|
||||||
'daysleft': remaining,
|
'daysleft': remaining,
|
||||||
|
|||||||
@@ -139,7 +139,10 @@ class WarrantyPlugin(BasePlugin):
|
|||||||
# date: expired or expiring is the whole decision when
|
# date: expired or expiring is the whole decision when
|
||||||
# scanning a board, and the exact day belongs on the report
|
# scanning a board, and the exact day belongs on the report
|
||||||
# you order from.
|
# 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}',
|
'link': '/assets/{assetid}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,6 +37,32 @@ def _count_by_type(assettype):
|
|||||||
return query.count()
|
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('/summary', methods=['GET'])
|
||||||
@dashboard_bp.route('', methods=['GET'])
|
@dashboard_bp.route('', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
@@ -49,15 +75,12 @@ def get_dashboard():
|
|||||||
measuringtool_count = _count_by_type('measuring_tool')
|
measuringtool_count = _count_by_type('measuring_tool')
|
||||||
total = machine_count + pc_count + network_count + printer_count + measuringtool_count
|
total = machine_count + pc_count + network_count + printer_count + measuringtool_count
|
||||||
|
|
||||||
# Count by status
|
# Count by status over the SAME population as the totals above, so the
|
||||||
status_counts = db.session.query(
|
# tiles can be read against each other.
|
||||||
AssetStatus.status,
|
status_dict = {}
|
||||||
db.func.count(Asset.assetid)
|
for asset in _countedassets().all():
|
||||||
).outerjoin(
|
name = asset.status.status if asset.status else 'Unknown'
|
||||||
Asset,
|
status_dict[name] = status_dict.get(name, 0) + 1
|
||||||
db.and_(Asset.statusid == AssetStatus.statusid, Asset.isactive == True)
|
|
||||||
).group_by(AssetStatus.status).all()
|
|
||||||
status_dict = {status: count for status, count in status_counts}
|
|
||||||
|
|
||||||
# Recent assets
|
# Recent assets
|
||||||
recent = Asset.query.filter_by(isactive=True).order_by(
|
recent = Asset.query.filter_by(isactive=True).order_by(
|
||||||
|
|||||||
Reference in New Issue
Block a user