listAll goes through fetchAllPages, which pages past the server's 100-row cap and returns the rows themselves. Three asset forms read .data.data off that array. An array has no .data, so the second access threw and the surrounding Promise.all rejected, taking the whole load into the catch. On the PC edit page the throw landed before the record fetch, so no request was made for the computer at all. The form rendered with its reference data and no record, and the banner said only "Failed to load data" - which reads like a backend or schema fault and was twice diagnosed as one. The server logs settled it: every reference endpoint answered 200 and the record was never requested. MachineForm already carried a comment describing this exact trap, added when pcsRes hit it. Two adjacent lines had the same defect and were missed, so the comment now sits above all of them. Report the failure honestly while here. apiError only reads err.response, so a client-side TypeError produced nothing but the fallback string no matter how many times the page was reloaded. Fall back to err.message when there is no response, which is what identified this.
299 lines
11 KiB
Vue
299 lines
11 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>{{ isEdit ? 'Edit Measuring Tool' : 'New Measuring Tool' }}</h2>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
|
|
<form v-else @submit.prevent="saveTool">
|
|
<!-- Identity Section -->
|
|
<h3 class="form-section-title">Identity</h3>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="assetnumber">Asset Number (Gage Tag) *</label>
|
|
<input id="assetnumber" v-model="form.assetnumber" type="text" class="form-control" required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="name">Name / Alias</label>
|
|
<input id="name" v-model="form.name" type="text" class="form-control" />
|
|
<small class="form-help">Layperson-friendly label</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="serialnumber">Serial Number (vendor)</label>
|
|
<input id="serialnumber" v-model="form.serialnumber" type="text" class="form-control" />
|
|
</div>
|
|
<div class="form-group" v-if="isEnabled('gaugelabreference', 'measuring_tool')">
|
|
<label for="gaugelabreference">Gauge Lab Reference</label>
|
|
<input id="gaugelabreference" v-model="form.gaugelabreference" type="text" class="form-control" />
|
|
<small class="form-help">Authoritative gauge lab asset reference (if tracked)</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row" v-if="isEnabled('maintenancereference', 'measuring_tool')">
|
|
<div class="form-group">
|
|
<label for="maintenancereference">Maintenance Reference</label>
|
|
<input id="maintenancereference" v-model="form.maintenancereference" type="text" class="form-control" />
|
|
<small class="form-help">Maintenance system asset reference (if tracked)</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="measuringtooltypeid">Type</label>
|
|
<select id="measuringtooltypeid" v-model="form.measuringtooltypeid" class="form-control">
|
|
<option value="">Select type...</option>
|
|
<option v-for="t in types" :key="t.measuringtooltypeid" :value="t.measuringtooltypeid">
|
|
{{ t.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="statusid">Status</label>
|
|
<select id="statusid" v-model="form.statusid" class="form-control">
|
|
<option value="">Select status...</option>
|
|
<option v-for="s in statuses" :key="s.statusid" :value="s.statusid">{{ s.status }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calibration Section -->
|
|
<h3 class="form-section-title">Calibration</h3>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="lastcalibrationdate">Last Calibration</label>
|
|
<input id="lastcalibrationdate" v-model="form.lastcalibrationdate" type="date" class="form-control" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="nextcalibrationdate">Next Calibration</label>
|
|
<input id="nextcalibrationdate" v-model="form.nextcalibrationdate" type="date" class="form-control" />
|
|
<small class="form-help">Drives the derived calibration status (overdue / due soon / current)</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="calibrationintervaldays">Interval (days)</label>
|
|
<input id="calibrationintervaldays" v-model.number="form.calibrationintervaldays" type="number" min="0" class="form-control" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="calibrationprovider">Calibration Provider</label>
|
|
<input id="calibrationprovider" v-model="form.calibrationprovider" type="text" class="form-control" />
|
|
<small class="form-help">Lab or in-house team that performs the calibration</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Location Section -->
|
|
<h3 class="form-section-title">Location & Organization</h3>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="locationid">Location</label>
|
|
<select id="locationid" v-model="form.locationid" class="form-control">
|
|
<option value="">Select location...</option>
|
|
<option v-for="l in locations" :key="l.locationid" :value="l.locationid">{{ l.locationname }}</option>
|
|
</select>
|
|
<small class="form-help">Operation locations (e.g. "0615 Blisk Inspection") hold measuring tools</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="businessunitid">Business Unit</label>
|
|
<select id="businessunitid" v-model="form.businessunitid" class="form-control">
|
|
<option value="">Select business unit...</option>
|
|
<option v-for="bu in businessunits" :key="bu.businessunitid" :value="bu.businessunitid">{{ bu.businessunit }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes Section -->
|
|
<h3 class="form-section-title">Notes</h3>
|
|
<div class="form-group">
|
|
<textarea id="notes" v-model="form.notes" class="form-control" rows="3" placeholder="Additional notes..."></textarea>
|
|
</div>
|
|
|
|
<!-- Site-defined custom fields for measuring tools -->
|
|
<CustomFieldsInputs ref="customFieldsRef" :assettypeid="assettypeid" :assetid="currentAssetId" />
|
|
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
|
|
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
|
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
|
{{ saving ? 'Saving...' : 'Save Measuring Tool' }}
|
|
</button>
|
|
<router-link to="/measuringtools" class="btn btn-secondary">Cancel</router-link>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { measuringtoolsApi, assetsApi, locationsApi, businessunitsApi } from '@/api'
|
|
import CustomFieldsInputs from '@/components/CustomFieldsInputs.vue'
|
|
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
|
import { apiError } from '@/utils/apiError'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const { isEnabled } = useIdentifierFlags()
|
|
|
|
const isEdit = computed(() => !!route.params.id)
|
|
|
|
// Resolved dynamically from /api/assets/types (no hardcoded id) so custom
|
|
// fields load for the measuring_tool asset type.
|
|
const assettypeid = ref(null)
|
|
const customFieldsRef = ref(null)
|
|
const currentAssetId = ref(null)
|
|
|
|
const loading = ref(true)
|
|
const saving = ref(false)
|
|
const error = ref('')
|
|
|
|
const form = ref({
|
|
assetnumber: '',
|
|
name: '',
|
|
gaugelabreference: '',
|
|
maintenancereference: '',
|
|
serialnumber: '',
|
|
statusid: '',
|
|
measuringtooltypeid: '',
|
|
lastcalibrationdate: '',
|
|
nextcalibrationdate: '',
|
|
calibrationintervaldays: null,
|
|
calibrationprovider: '',
|
|
locationid: '',
|
|
businessunitid: '',
|
|
notes: '',
|
|
})
|
|
|
|
const types = ref([])
|
|
const statuses = ref([])
|
|
const locations = ref([])
|
|
const businessunits = ref([])
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const [typesResponse, statusResponse, locResponse, buResponse, assetTypesResponse] = await Promise.all([
|
|
measuringtoolsApi.types.list({ perpage: 100 }),
|
|
assetsApi.statuses.list(),
|
|
locationsApi.listAll(), // backend caps perpage at 100; page through all
|
|
businessunitsApi.listAll(),
|
|
assetsApi.types.list(),
|
|
])
|
|
types.value = typesResponse.data.data || []
|
|
statuses.value = statusResponse.data.data || []
|
|
// listAll resolves to the ARRAY, not a response: fetchAllPages already
|
|
// unwrapped every page. Reading .data.data off it throws and takes the
|
|
// whole parallel load into the catch.
|
|
locations.value = locResponse || []
|
|
businessunits.value = buResponse || []
|
|
|
|
const assetType = (assetTypesResponse.data.data || []).find(a => a.assettype === 'measuring_tool')
|
|
assettypeid.value = assetType ? assetType.assettypeid : null
|
|
|
|
if (isEdit.value) {
|
|
const response = await measuringtoolsApi.get(route.params.id)
|
|
const data = response.data.data
|
|
currentAssetId.value = data.assetid || null
|
|
const ext = data.measuringtool || {}
|
|
form.value = {
|
|
assetnumber: data.assetnumber || '',
|
|
name: data.name || '',
|
|
gaugelabreference: data.gaugelabreference || '',
|
|
maintenancereference: data.maintenancereference || '',
|
|
serialnumber: data.serialnumber || '',
|
|
statusid: data.statusid || '',
|
|
measuringtooltypeid: ext.measuringtooltypeid || '',
|
|
lastcalibrationdate: ext.lastcalibrationdate || '',
|
|
nextcalibrationdate: ext.nextcalibrationdate || '',
|
|
calibrationintervaldays: ext.calibrationintervaldays ?? null,
|
|
calibrationprovider: ext.calibrationprovider || '',
|
|
locationid: data.locationid || '',
|
|
businessunitid: data.businessunitid || '',
|
|
notes: data.notes || '',
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Error loading data:', err)
|
|
error.value = 'Failed to load data'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
async function saveTool() {
|
|
error.value = ''
|
|
saving.value = true
|
|
try {
|
|
const data = {
|
|
assetnumber: form.value.assetnumber,
|
|
name: form.value.name || null,
|
|
gaugelabreference: form.value.gaugelabreference || null,
|
|
maintenancereference: form.value.maintenancereference || null,
|
|
serialnumber: form.value.serialnumber || null,
|
|
statusid: form.value.statusid || null,
|
|
measuringtooltypeid: form.value.measuringtooltypeid || null,
|
|
lastcalibrationdate: form.value.lastcalibrationdate || null,
|
|
nextcalibrationdate: form.value.nextcalibrationdate || null,
|
|
calibrationintervaldays: form.value.calibrationintervaldays ?? null,
|
|
calibrationprovider: form.value.calibrationprovider || null,
|
|
locationid: form.value.locationid || null,
|
|
businessunitid: form.value.businessunitid || null,
|
|
notes: form.value.notes || null,
|
|
}
|
|
|
|
let saved
|
|
if (isEdit.value) {
|
|
const response = await measuringtoolsApi.update(route.params.id, data)
|
|
saved = response.data.data
|
|
} else {
|
|
const response = await measuringtoolsApi.create(data)
|
|
saved = response.data.data
|
|
}
|
|
|
|
const assetId = saved.assetid
|
|
if (assetId && customFieldsRef.value) {
|
|
try {
|
|
await customFieldsRef.value.save(assetId)
|
|
} catch (customFieldsErr) {
|
|
console.error('Error saving custom fields:', customFieldsErr)
|
|
}
|
|
}
|
|
|
|
router.push(`/measuringtools/${saved.measuringtool?.measuringtoolid || route.params.id}`)
|
|
} catch (err) {
|
|
console.error('Error saving measuring tool:', err)
|
|
error.value = apiError(err, 'Failed to save measuring tool')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.form-section-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
margin: 1.5rem 0 0.75rem 0;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.form-section-title:first-of-type {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.form-help {
|
|
display: block;
|
|
margin-top: 0.25rem;
|
|
font-size: 0.8rem;
|
|
color: var(--text-light);
|
|
}
|
|
</style>
|