Files
shopdb-flask/plugins/measuringtools/frontend/views/MeasuringToolDetail.vue
Cam Proudlock dd7dd435c0 Put the dual-bay sibling in the triage band, and fix the gage counterpart
The machine page opened with a dual-bay banner directly above a hero band saying
'Controlled by'. Both answer 'what am I looking at', so the sibling is now a chip
in the band. The band takes a generic extraitems prop rather than learning what
dualpath is - the pairing is resolved by the machines plugin, not from the
relationships endpoint.

MeasuringToolDetail filtered for a machine counterpart, but the import creates a
controls edge PC -> tool, so a gage's counterpart is a computer. It rendered
nothing on every metrology station that has a PC.

Verified on screen. Full suite 2178 passed, frontend 280 passed.
2026-08-29 19:38:07 -04:00

250 lines
11 KiB
Vue

<template>
<div class="detail-page">
<div class="page-header">
<h2>Measuring Tool Details</h2>
<div class="header-actions">
<router-link :to="`/print/asset-label/measuring_tool/${tool?.measuringtool?.measuringtoolid}`" class="btn btn-secondary" v-if="tool" target="_blank">
Print Label
</router-link>
<router-link :to="`/measuringtools/${tool?.measuringtool?.measuringtoolid}/edit`" class="btn btn-primary" v-if="tool">
Edit
</router-link>
<button @click="confirmRetire" class="btn btn-danger" v-if="tool">Retire</button>
<router-link to="/measuringtools" class="btn btn-secondary">Back to List</router-link>
</div>
</div>
<div v-if="loading" class="loading">Loading...</div>
<template v-else-if="tool">
<!-- Hero Section -->
<div class="hero-card">
<div class="hero-content">
<div class="hero-title">
<h1>{{ tool.assetnumber }}</h1>
<span v-if="tool.name" class="hero-alias">{{ tool.name }}</span>
</div>
<div class="hero-meta">
<span class="badge badge-lg badge-primary">
{{ tool.assettypename || 'Measuring Tool' }}
</span>
<span class="badge badge-lg" :style="colorStyle(tool.measuringtool?.calibrationstatuscolor)">
{{ statusLabel(tool.measuringtool?.calibrationstatus) }}
</span>
</div>
<div class="hero-details">
<div class="hero-detail" v-if="tool.measuringtool?.measuringtooltypename">
<span class="hero-detail-label">Type</span>
<span class="hero-detail-value">{{ tool.measuringtool.measuringtooltypename }}</span>
</div>
<div class="hero-detail" v-if="tool.measuringtool?.nextcalibrationdate">
<span class="hero-detail-label">Next Calibration</span>
<span class="hero-detail-value">{{ formatDate(tool.measuringtool.nextcalibrationdate) }}</span>
</div>
<div class="hero-detail" v-if="tool.locationname">
<span class="hero-detail-label">Location</span>
<span class="hero-detail-value">{{ tool.locationname }}</span>
</div>
</div>
<!-- The 6am band: which PC drives this instrument.
A metrology tool is reached through the PC that runs it - the
import synthesizes one measuring-tool asset per metrology PC and
a 'controls' edge PC -> tool (stage_metrology), so the
counterpart here is a COMPUTER, exactly as on the machine page.
This was first written as counterparttype="machine" labelled
"Used on", which filtered for a counterpart that cannot exist
and so rendered nothing on every gage that had a PC. A hand-held
gage genuinely has no such edge, and the band correctly shows
nothing for it. -->
<AssetTriageBand
:assetid="tool.assetid"
counterparttype="computer"
counterpartlabel="Controlled by"
/>
</div>
</div>
<!-- Canonical card order: Identity -> type-specific -> status -> Location & Organization -> domain -> Custom Fields -> Warranty -> Relationships -> Notes -> audit footer -->
<!-- Main Content Grid -->
<div class="content-grid">
<!-- Left Column -->
<div class="content-column">
<!-- Identity Section -->
<div class="section-card">
<h3 class="section-title">Identity</h3>
<div class="info-list">
<div class="info-row">
<span class="info-label">Asset Number (Gage Tag)</span>
<span class="info-value">{{ tool.assetnumber }}</span>
</div>
<div class="info-row" v-if="tool.name">
<span class="info-label">Name</span>
<span class="info-value">{{ tool.name }}</span>
</div>
<div class="info-row" v-if="tool.gaugelabreference && isEnabled('gaugelabreference', 'measuring_tool')">
<span class="info-label">Gauge Lab Reference</span>
<span class="info-value mono">{{ tool.gaugelabreference }}</span>
</div>
<div class="info-row" v-if="tool.maintenancereference && isEnabled('maintenancereference', 'measuring_tool')">
<span class="info-label">Maintenance Reference</span>
<span class="info-value mono">{{ tool.maintenancereference }}</span>
</div>
<div class="info-row" v-if="tool.serialnumber">
<span class="info-label">Serial Number</span>
<span class="info-value mono">{{ tool.serialnumber }}</span>
</div>
<div class="info-row">
<span class="info-label">Type</span>
<span class="info-value">{{ tool.measuringtool?.measuringtooltypename || '-' }}</span>
</div>
</div>
</div>
<!-- Calibration Section -->
<div class="section-card">
<h3 class="section-title">Calibration</h3>
<div class="info-list">
<div class="info-row">
<span class="info-label">Status</span>
<span class="info-value">
<span class="badge" :style="colorStyle(tool.measuringtool?.calibrationstatuscolor)">
{{ statusLabel(tool.measuringtool?.calibrationstatus) }}
</span>
</span>
</div>
<div class="info-row">
<span class="info-label">Last Calibration</span>
<span class="info-value">{{ tool.measuringtool?.lastcalibrationdate ? formatDate(tool.measuringtool.lastcalibrationdate) : '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Next Calibration</span>
<span class="info-value">{{ tool.measuringtool?.nextcalibrationdate ? formatDate(tool.measuringtool.nextcalibrationdate) : '-' }}</span>
</div>
<div class="info-row" v-if="tool.measuringtool?.calibrationintervaldays">
<span class="info-label">Interval</span>
<span class="info-value">{{ tool.measuringtool.calibrationintervaldays }} days</span>
</div>
<div class="info-row" v-if="tool.measuringtool?.calibrationprovider">
<span class="info-label">Provider</span>
<span class="info-value">{{ tool.measuringtool.calibrationprovider }}</span>
</div>
</div>
</div>
</div>
<!-- Right Column -->
<div class="content-column">
<!-- Location & Organization -->
<div class="section-card">
<h3 class="section-title">Location & Organization</h3>
<div class="info-list">
<div class="info-row">
<span class="info-label">Location</span>
<span class="info-value">{{ tool.locationname || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Business Unit</span>
<span class="info-value">{{ tool.businessunitname || '-' }}</span>
</div>
</div>
</div>
<!-- Custom Fields -->
<CustomFieldsSection :assetid="tool.assetid" />
<!-- Warranty -->
<PluginAssetPanels :assetid="tool.assetid" />
<!-- All relationships (partof, connectedto, ...) -->
<AssetRelationships v-if="tool.assetid" :assetid="tool.assetid" />
<!-- Notes -->
<div class="section-card" v-if="tool.notes">
<h3 class="section-title">Notes</h3>
<p class="notes-text">{{ tool.notes }}</p>
</div>
</div>
</div>
<!-- Audit Footer -->
<div class="audit-footer">
<span>Created {{ formatDateTime(tool.createddate) }}<template v-if="tool.createdby"> by {{ tool.createdby }}</template></span>
<span>Modified {{ formatDateTime(tool.modifieddate) }}<template v-if="tool.modifiedby"> by {{ tool.modifiedby }}</template></span>
</div>
</template>
<div v-else class="card">
<p style="text-align: center; color: var(--text-light);">Measuring tool not found</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { colorStyle } from '@/utils/colorStyle'
import { measuringtoolsApi } from '@/api'
import AssetTriageBand from '@/components/AssetTriageBand.vue'
import CustomFieldsSection from '@/components/CustomFieldsSection.vue'
import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
import AssetRelationships from '@/components/AssetRelationships.vue'
import { useIdentifierFlags } from '@/composables/identifierSettings'
import { setRecordTitle } from '@/composables/pageTitle'
import { useToast } from '@/composables/toast'
import { apiError } from '@/utils/apiError'
const route = useRoute()
const router = useRouter()
const toast = useToast()
const { isEnabled } = useIdentifierFlags()
const loading = ref(true)
const tool = ref(null)
const STATUS_LABELS = { overdue: 'Overdue', duesoon: 'Due Soon', current: 'Current', unknown: 'Unknown' }
function statusLabel(status) { return STATUS_LABELS[status] || 'Unknown' }
function formatDate(d) { if (!d) return '-'; return new Date(d + 'T00:00:00').toLocaleDateString() }
function formatDateTime(d) { if (!d) return '-'; return new Date(d).toLocaleString() }
onMounted(async () => {
try {
// by-asset route resolves the tool from a core asset id (search/cross-link
// rows carry assetid, not the extension id); detail route keys on the id.
const response = route.params.assetid
? await measuringtoolsApi.getByAsset(route.params.assetid)
: await measuringtoolsApi.get(route.params.id)
tool.value = response.data.data
setRecordTitle(tool.value?.assetnumber || tool.value?.name)
} catch (err) {
console.error('Error loading measuring tool:', err)
} finally {
loading.value = false
}
})
// Retire, not delete: the server deactivates the asset, so the measuring tool
// leaves every list and report while its history and relationships stay put.
// Reversible by an admin setting isactive back to true.
async function confirmRetire() {
const label = tool.value?.assetnumber || tool.value?.name || 'this measuring tool'
if (!confirm(`Retire ${label}?\n\nIt will no longer appear in ShopDB. `
+ `History and relationships are kept, and it can be restored.`)) return
try {
await measuringtoolsApi.remove(tool.value?.measuringtool?.measuringtoolid)
toast.success(`Retired ${label}`)
router.push('/measuringtools')
} catch (error) {
console.error('Error retiring measuring tool:', error)
toast.error(apiError(error, 'Failed to retire measuring tool'))
}
}
</script>
<style scoped>
.mono {
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}
</style>