Files
shopdb-flask/plugins/measuringtools/frontend/views/MeasuringToolForm.vue
benjamin.baker 539af3073f Read listAll results as rows, not as a response
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.
2026-09-05 04:39:11 -04:00

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>