Remediate the 2026-09-03 feature-test findings

This commit is contained in:
2026-09-03 17:15:03 -04:00
parent cf95daf3bb
commit fd513c08eb
112 changed files with 5182 additions and 857 deletions

View File

@@ -9,14 +9,24 @@
<router-link :to="`/print/asset-label/machine/${machine?.machine?.machineid}`" class="btn btn-secondary" v-if="machine" target="_blank">
Print Label
</router-link>
<router-link :to="`/machines/${machine?.machine?.machineid}/edit`" class="btn btn-primary" v-if="machine">
<router-link :to="`/machines/${machine?.machine?.machineid}/edit`" class="btn btn-primary" v-if="machine && !isRetired">
Edit
</router-link>
<button @click="confirmRetire" class="btn btn-danger" v-if="machine">Retire</button>
<button @click="confirmRetire" class="btn btn-danger" v-if="machine && !isRetired">Retire</button>
<!-- A retired record offered Edit and Retire exactly like a live one,
with nothing on the page saying it was retired. Restoring it was
possible through the API and nowhere in the product. -->
<button @click="restore" class="btn btn-primary" v-if="machine && isRetired">Restore</button>
<router-link to="/machines" class="btn btn-secondary">Back to List</router-link>
</div>
</div>
<div v-if="machine && isRetired" class="retired-banner card">
<strong>This machine is retired.</strong>
It stays out of every list and report, and its history and relationships
are intact. Restore puts it back.
</div>
<div v-if="loading" class="loading">Loading...</div>
<template v-else-if="machine">
@@ -295,7 +305,7 @@ const { heroWarranty, warrantyDate } = useWarrantyBadge(() => machine.value?.ass
// relationships render via the shared AssetRelationships card
onMounted(async () => {
async function loadMachine() {
try {
const response = await machinesApi.get(route.params.id)
machine.value = response.data.data
@@ -305,7 +315,9 @@ onMounted(async () => {
} finally {
loading.value = false
}
})
}
onMounted(loadMachine)
function getStatusClass(status) {
if (!status) return 'badge-info'
@@ -324,6 +336,20 @@ function formatDate(dateStr) {
// Retire, not delete: the server deactivates the asset, so the machine
// leaves every list and report while its history and relationships stay put.
// Reversible by an admin setting isactive back to true.
const isRetired = computed(() => machine.value?.isactive === false)
async function restore() {
const label = machine.value?.assetnumber || machine.value?.name || 'this machine'
try {
await machinesApi.update(machine.value?.machine?.machineid, { isactive: true })
toast.success(`Restored ${label}`)
await loadMachine()
} catch (error) {
console.error('Error restoring machine:', error)
toast.error(apiError(error, 'Failed to restore machine'))
}
}
async function confirmRetire() {
const label = machine.value?.assetnumber || machine.value?.name || 'this machine'
if (!confirm(`Retire ${label}?\n\nIt will no longer appear in ShopDB. `
@@ -340,6 +366,10 @@ async function confirmRetire() {
</script>
<style scoped>
.retired-banner {
border-left: 3px solid var(--warning);
margin-bottom: 1rem;
}
.mono {
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
}

View File

@@ -416,6 +416,7 @@ import CustomFieldsInputs from '@/components/CustomFieldsInputs.vue'
import PrinterAssignmentPicker from '@/components/PrinterAssignmentPicker.vue'
import { currentTheme } from '@/stores/theme'
import { useIdentifierFlags } from '@/composables/identifierSettings'
import { loadAssetTypeIds, assetTypeId } from '@/composables/assetTypeIds'
import { apiError } from '@/utils/apiError'
import { planControllingPcChange } from './controllingPc'
@@ -426,8 +427,11 @@ const router = useRouter()
const isEdit = computed(() => !!route.params.id)
// Seeded asset-type id for machines (see /api/assets/types).
const MACHINE_ASSETTYPEID = 1
// Resolved by NAME at load, never hardcoded: the id comes from seed
// order, so the constant that used to live here pointed at a different
// asset type on a differently-seeded site - the form then rendered THAT
// type's custom fields and dropped whatever was typed into them.
const MACHINE_ASSETTYPEID = computed(() => assetTypeId('machine'))
const customFieldsRef = ref(null)
const printerPickerRef = ref(null)
const currentAssetId = ref(null)
@@ -571,15 +575,16 @@ watch(() => form.value.controllervendorid, (newVal, oldVal) => {
})
onMounted(async () => {
await loadAssetTypeIds()
try {
// Load reference data in parallel
const [typesRes, statusRes, vendorRes, locRes, allModels, buRes, pcsRes, relTypesRes] = await Promise.all([
machinesApi.types.list(),
assetsApi.statuses.list(),
vendorsApi.list({ perpage: 500 }),
vendorsApi.listAll(),
locationsApi.listAll(), // backend caps perpage at 100; page through all
modelsApi.listAll({ category: 'Equipment' }), // backend caps perpage at 100; page through all
businessunitsApi.list({ perpage: 500 }),
businessunitsApi.listAll(),
computersApi.listAll(), // backend caps perpage at 100; page through all
assetsApi.types.list() // Used for relationship types, will fix below
])
@@ -763,7 +768,13 @@ async function saveMachine() {
maintenanceintervaldays: form.value.maintenanceintervaldays || null,
notes: form.value.notes || null,
mapx: form.value.mapx,
mapy: form.value.mapy
mapy: form.value.mapy,
// ADR-017: a position without its level cannot be drawn on the right
// floor plan. The picker sets all three and this payload sent two, so a
// picked pin saved a levelid of null and then rendered nowhere - the
// form even showed "Position: x, y" back, because it kept the value it
// had never sent.
levelid: form.value.levelid
}
let savedMachine

View File

@@ -95,7 +95,7 @@ onMounted(loadData)
async function loadData() {
loading.value = true
try {
const response = await machinesApi.types.list({ perpage: 200, active: false })
const response = await machinesApi.types.list({ perpage: 100, active: false })
items.value = response.data.data || []
} catch (err) {
console.error('Error loading machine types:', err)

View File

@@ -15,6 +15,14 @@
placeholder="Search machines..."
@input="debouncedSearch"
/>
<!-- Retire is a soft delete and the confirm dialog promises the record
"can be restored". The API kept that promise all along (?active=false
lists them, PUT isactive restores) but nothing in the UI could reach
it, so a retired machine was simply gone. -->
<label class="checkbox-label">
<input type="checkbox" v-model="showRetired" @change="loadMachines" />
Show retired
</label>
</div>
<div class="card">
@@ -41,6 +49,7 @@
@click="item.machine?.machineid && $router.push(`/machines/${item.machine.machineid}`)">
<td>
{{ item.assetnumber }}<template v-if="item.dualpathpartner"> / {{ item.dualpathpartner.assetnumber }}</template>
<span v-if="item.isactive === false" class="badge badge-secondary">retired</span>
</td>
<td>{{ item.name || '-' }}</td>
<td class="mono">{{ item.serialnumber || '-' }}</td>
@@ -122,6 +131,7 @@ function applySuggestion(word) {
// search page documents. Load directly.
loadMachines()
}
const showRetired = ref(false)
const totalPages = ref(1)
const perPage = ref(20)
@@ -139,6 +149,8 @@ async function loadMachines() {
perpage: perPage.value
}
if (search.value) params.search = search.value
// active=false means "include retired", matching the API.
if (showRetired.value) params.active = 'false'
const response = await machinesApi.list(params)
machines.value = response.data.data || []