Frontend: asset identifiers, statuses, printer types, UI fixes

- Wire gauge/maintenance refs + identifier toggles into equipment pages.
- Repoint status dropdowns and Settings>Statuses to asset statuses.
- Printer type column/filter; require vendor before model; printer-model picker.
- Per-type identifier labels; printer detail title fallback.
- Fixes: truncate long description cells, opaque supply modal, readable
  dark-mode autofill, drum/waste default colorless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 08:35:32 -04:00
parent 0436e8b0af
commit e631564377
22 changed files with 786 additions and 124 deletions

View File

@@ -37,7 +37,7 @@
</div>
<div class="form-row">
<div class="form-group">
<div class="form-group" v-if="identifierflags.fqdn">
<label for="hostname">Hostname (FQDN)</label>
<input
id="hostname"
@@ -73,11 +73,11 @@
>
<option value="">Select type...</option>
<option
v-for="mt in printerTypes"
:key="mt.machinetypeid"
:value="mt.machinetypeid"
v-for="pt in printerTypes"
:key="pt.printertypeid"
:value="pt.printertypeid"
>
{{ mt.machinetype }}
{{ pt.printertype }}
</option>
</select>
</div>
@@ -127,8 +127,9 @@
id="modelnumberid"
v-model="form.modelnumberid"
class="form-control"
:disabled="!form.vendorid"
>
<option value="">Select model...</option>
<option value="">{{ form.vendorid ? 'Select model...' : 'Select a vendor first' }}</option>
<option
v-for="m in filteredModels"
:key="m.modelnumberid"
@@ -137,8 +138,8 @@
{{ m.modelnumber }}
</option>
</select>
<small v-if="!form.vendorid && !form.machinetypeid" class="form-hint">
Select vendor or printer type to filter models
<small v-if="!form.vendorid" class="form-hint">
Select a vendor first to choose a model
</small>
</div>
</div>
@@ -266,10 +267,13 @@
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { machinesApi, machinetypesApi, statusesApi, vendorsApi, locationsApi, printersApi, modelsApi } from '../../api'
import { assetsApi, vendorsApi, locationsApi, printersApi, modelsApi } from '../../api'
import ShopFloorMap from '../../components/ShopFloorMap.vue'
import Modal from '../../components/Modal.vue'
import { currentTheme } from '../../stores/theme'
import { useIdentifierFlags } from '../../composables/identifierSettings'
const identifierflags = useIdentifierFlags()
const route = useRoute()
const router = useRouter()
@@ -317,8 +321,10 @@ const filteredModels = computed(() => {
if (form.value.vendorid && m.vendorid !== form.value.vendorid) {
return false
}
// Filter by printer type if selected
if (form.value.machinetypeid && m.machinetypeid !== form.value.machinetypeid) {
// Filter by printer type if selected, but only exclude models that have a
// type set and it differs. Most models have no machinetypeid, so excluding
// null-typed models would hide the printer's own model from the dropdown.
if (form.value.machinetypeid && m.machinetypeid && m.machinetypeid !== form.value.machinetypeid) {
return false
}
return true
@@ -422,47 +428,53 @@ function onWindowsNameInput() {
onMounted(async () => {
try {
// Load reference data
const [mtRes, statusRes, vendorRes, modelsRes, locRes] = await Promise.all([
machinetypesApi.list({ category: 'Printer' }),
statusesApi.list(),
vendorsApi.list(),
modelsApi.list(),
locationsApi.list()
// Load reference data (models paged in full via listAll, see api/index.js)
// perpage 100 so dropdowns aren't truncated to the default 20-row page
// (e.g. 44 vendors; the editing record's vendor can be past row 20)
const [mtRes, statusRes, vendorRes, allModels, locRes] = await Promise.all([
printersApi.types.list({ perpage: 100 }),
assetsApi.statuses.list(),
vendorsApi.list({ perpage: 100 }),
modelsApi.listAll(),
locationsApi.list({ perpage: 100 })
])
printerTypes.value = mtRes.data.data || []
statuses.value = statusRes.data.data || []
vendors.value = vendorRes.data.data || []
models.value = modelsRes.data.data || []
models.value = allModels
locations.value = locRes.data.data || []
// Load printer if editing
if (isEdit.value) {
const response = await printersApi.get(route.params.id)
const printer = response.data.data
// asset-based shape: printer extension fields live under printer.printer
const ext = printer.printer || {}
// Get IP from communications
const primaryComm = printer.communications?.find(c => c.isprimary) || printer.communications?.[0]
form.value = {
machinenumber: printer.machinenumber || '',
alias: printer.alias || '',
hostname: printer.hostname || '',
// "Windows Name" is the printer's business identifier (assetnumber).
// Prefer the explicit windowsname, then fall back to assetnumber.
machinenumber: ext.windowsname || printer.assetnumber || '',
alias: printer.name && printer.name.toUpperCase() !== 'NONE' ? printer.name : '',
hostname: ext.hostname || '',
serialnumber: printer.serialnumber || '',
machinetypeid: printer.machinetype?.machinetypeid || '',
statusid: printer.status?.statusid || '',
vendorid: printer.vendor?.vendorid || '',
modelnumberid: printer.model?.modelnumberid || '',
locationid: printer.location?.locationid || '',
machinetypeid: ext.printertypeid || '',
statusid: printer.statusid || '',
vendorid: ext.vendorid || '',
modelnumberid: ext.modelnumberid || '',
locationid: printer.locationid || '',
notes: printer.notes || '',
mapx: printer.mapx ?? null,
mapy: printer.mapy ?? null,
// Printer-specific
ipaddress: primaryComm?.ipaddress || '',
csfname: printer.printerdata?.sharename || '',
installpath: printer.printerdata?.installpath || '',
pin: printer.printerdata?.pin || ''
csfname: ext.sharename || '',
installpath: ext.installpath || '',
pin: ext.pin || ''
}
// Don't auto-generate for existing printers
@@ -500,48 +512,36 @@ async function savePrinter() {
saving.value = true
try {
const machineData = {
machinenumber: form.value.machinenumber,
alias: form.value.alias,
hostname: form.value.hostname,
serialnumber: form.value.serialnumber,
machinetypeid: form.value.machinetypeid || null,
// One payload for the printers plugin, which owns asset core + extension +
// primary communication. The "Windows Name" field is the business
// identifier, written to both assetnumber and the extension windowsname.
const payload = {
assetnumber: form.value.machinenumber,
windowsname: form.value.machinenumber || null,
hostname: form.value.hostname || null,
serialnumber: form.value.serialnumber || null,
printertypeid: form.value.machinetypeid || null,
statusid: form.value.statusid || null,
vendorid: form.value.vendorid || null,
modelnumberid: form.value.modelnumberid || null,
locationid: form.value.locationid || null,
notes: form.value.notes,
sharename: form.value.csfname || null,
iscsf: !!form.value.csfname,
installpath: form.value.installpath || null,
pin: form.value.pin || null,
ipaddress: form.value.ipaddress || null,
mapx: form.value.mapx,
mapy: form.value.mapy
}
const printerData = {
windowsname: form.value.machinenumber, // Windows name is the machinenumber
sharename: form.value.csfname,
installpath: form.value.installpath,
pin: form.value.pin,
iscsf: !!form.value.csfname // Auto-set based on whether CSF name is filled
// only set the display name when an alias is given, so we don't clobber it
if (form.value.alias) {
payload.name = form.value.alias
}
// Handle IP address - need to update/create communication record
const communicationData = form.value.ipaddress ? {
ipaddress: form.value.ipaddress,
isprimary: true
} : null
if (isEdit.value) {
await machinesApi.update(route.params.id, machineData)
await printersApi.updateExtension(route.params.id, printerData)
if (communicationData) {
await printersApi.updateCommunication(route.params.id, communicationData)
}
await printersApi.update(route.params.id, payload)
} else {
const response = await machinesApi.create(machineData)
const newId = response.data.data.machineid
await printersApi.updateExtension(newId, printerData)
if (communicationData) {
await printersApi.updateCommunication(newId, communicationData)
}
await printersApi.create(payload)
}
router.push('/printers')
@@ -592,12 +592,11 @@ async function savePrinter() {
color: var(--text-light, #666);
}
/* auto-fill cue: a subtle translucent blue tint + accent border that reads
correctly over both light and dark backgrounds. Text color stays themed
(no solid light fill that turns into an unreadable white box in dark mode). */
.auto-generated {
background-color: #f0f7ff;
background-color: rgba(33, 150, 243, 0.12);
border-color: #90caf9;
}
.auto-generated:focus {
background-color: white;
}
</style>