TWO STRIPS OF CHROME SAYING THE SAME KIND OF THING
The machine page opened with a banner - "Dual-bay machine - sibling bay: 2022" -
directly above a hero whose band said "Controlled by". Both answer "what am I
looking at", and splitting them across a banner and a band cost a strip of
vertical space to say so.
The sibling is now a chip in the band beside the controlling PC. The band takes
an `extraitems` prop rather than learning what dualpath is: the pairing is
resolved by the machines plugin, not from the relationships endpoint, so core
cannot derive it, and a dualpath-shaped prop on a core component would be the
machines plugin leaking upward. A page passes [{label, links:[{text, to}]}] and
the band renders it with its own markup, which also keeps the chips visually
identical to the ones it derives itself.
The label stays "Dual-bay sibling" rather than "Dualpath". The banner spelled
the meaning out because a dualpath pair is ONE physical machine in two bays; a
chip reading "Dualpath" would have dropped that.
THE GAGE PAGE FILTERED FOR A COUNTERPART THAT CANNOT EXIST
MeasuringToolDetail was wired counterparttype="machine" labelled "Used on".
Wrong on both counts. The import synthesizes one measuring-tool asset per
metrology PC and a 'controls' edge PC -> tool (stage_metrology), so a gage's
counterpart is a COMPUTER, exactly as on the machine page. Filtering for a
machine peer meant the band rendered nothing on every gage that has a PC.
It looked merely empty rather than wrong because this dev database holds one
measuring tool with no relationships at all, so there was nothing to be missing.
A hand-held gage genuinely has no such edge and correctly shows nothing; a
metrology station now shows the PC that drives it.
The relationships card stays. It is the only surface with Add Relationship and
the per-row delete, and it lists types the band filters out - the band is
triage, the card is management. It does now repeat what the band shows, which
is worth revisiting separately.
Verified on screen. Frontend 280 passed.
250 lines
11 KiB
Vue
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>
|