diff --git a/plugins/warranty/api/routes.py b/plugins/warranty/api/routes.py index fa51ae0..9111a55 100644 --- a/plugins/warranty/api/routes.py +++ b/plugins/warranty/api/routes.py @@ -33,12 +33,57 @@ def _parse_date(value): return None +def _related_machine(asset): + """The machine a covered asset is associated with, or None. + + A shopfloor PC is bought, warranted and replaced as a PC, but it is FOUND + by the machine it drives - nobody walks the floor looking for an asset + number. So a warranty row for a PC carries the machine's number, and its + map position so the row can point at where to go. + + Walks the asset relationship graph in BOTH directions: the canonical edge is + PC --controls--> machine, but a dual-bay pair carries controls on both bays + and hand-made links are not guaranteed to be oriented. Returns the first + active related asset whose type is 'machine'. + """ + from shopdb.api import Asset as CoreAsset + + related = [] + for rel in (getattr(asset, 'outgoing_relationships', None) or []): + if rel.isactive and rel.targetasset: + related.append(rel.targetasset) + for rel in (getattr(asset, 'incoming_relationships', None) or []): + if rel.isactive and rel.sourceasset: + related.append(rel.sourceasset) + + for candidate in related: + typename = candidate.assettype.assettype if candidate.assettype else None + if typename != 'machine': + continue + return { + 'assetid': candidate.assetid, + 'machinenumber': candidate.assetnumber, + 'name': candidate.name, + # Map position for the hover preview. Either may be None: a machine + # that has never been placed on the floor map still has a number + # worth showing, so the caller decides what to do with a missing + # position rather than the row being dropped. + 'mapx': candidate.mapx, + 'mapy': candidate.mapy, + 'locationid': candidate.locationid, + 'locationname': (candidate.location.locationname + if candidate.location else None), + } + return None + + def _asset_summary(asset): return { 'assetid': asset.assetid, 'assetnumber': asset.assetnumber, 'name': asset.name, 'assettypename': asset.assettype.assettype if asset.assettype else None, + 'machine': _related_machine(asset), } diff --git a/plugins/warranty/frontend/components/MachineMapChip.vue b/plugins/warranty/frontend/components/MachineMapChip.vue new file mode 100644 index 0000000..87d5ca1 --- /dev/null +++ b/plugins/warranty/frontend/components/MachineMapChip.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/plugins/warranty/frontend/views/WarrantiesList.vue b/plugins/warranty/frontend/views/WarrantiesList.vue index 9e549f9..fdbf34e 100644 --- a/plugins/warranty/frontend/views/WarrantiesList.vue +++ b/plugins/warranty/frontend/views/WarrantiesList.vue @@ -40,6 +40,7 @@ Service Level Ends Covers + Machine # Actions @@ -53,6 +54,12 @@ - {{ a.assetnumber }} + + + - + @@ -163,6 +170,7 @@ import { colorStyle } from '@/utils/colorStyle' import { warrantyApi, assetsApi, vendorsApi } from '@/api' import { useToast } from '@/composables/toast' import { apiError } from '@/utils/apiError' +import MachineMapChip from '../components/MachineMapChip.vue' const toast = useToast() const route = useRoute() @@ -390,7 +398,7 @@ async function refresh(w) { .asset-search { position: relative; } .asset-results { position: absolute; z-index: 10; left: 0; right: 0; margin: 2px 0 0; - padding: 0; list-style: none; background: var(--bg-card); + padding: 0; list-style: none; background: var(--bg-card-solid); border: 1px solid var(--border); border-radius: 6px; max-height: 200px; overflow-y: auto; } .asset-results li { padding: 0.45rem 0.6rem; cursor: pointer; } diff --git a/plugins/warranty/frontend/views/WarrantyReport.vue b/plugins/warranty/frontend/views/WarrantyReport.vue index 01add33..634db51 100644 --- a/plugins/warranty/frontend/views/WarrantyReport.vue +++ b/plugins/warranty/frontend/views/WarrantyReport.vue @@ -33,6 +33,7 @@ Service Level Ends Covers + Machine # @@ -44,6 +45,12 @@ {{ a.assetnumber }} - + + + - + @@ -58,6 +65,7 @@ import { ref, computed, onMounted } from 'vue' import { warrantyApi } from '@/api' import EmailReportButton from '@/components/EmailReportButton.vue' +import MachineMapChip from '../components/MachineMapChip.vue' const loading = ref(true) const counts = ref({})