Eight fields across three asset types were shown on the detail page and accepted by the API, but had no input on the form. A wrong value therefore needed database access to correct, which is not a thing anybody should need for a device category or a maintenance date. The shape is the same in each case: a column was added, the detail card was written, the API allowlist was updated, and the form was missed. Nothing was broken - the values arrive by import or collector - so nothing complained. PCs gain azureobjectid and azuredevicecategory. azuredeviceid was already there, which is what made the omission easy to miss: the page showed two ids and the form offered one. The two are not interchangeable and the pairing is counterintuitive - the DEVICE id opens Intune, the OBJECT id opens Entra - so each input says where it goes. Machines gain the three maintenance fields. These matter beyond the immediate fix: they are the columns a measuring instrument's calibration dates map onto when metrology moves to machines, and until now there was no way to enter a calibration date on a machine at all. Printers gain iscolor, isduplex and isnetwork as a capability group. isnetwork defaults to true in the form to match the API's create default; a new printer claiming otherwise would be wrong for nearly all of them. Checked before making any of these editable that nothing writes them automatically - a hand edit silently reverted by the next collector poll would be worse than no input at all. The printers collector reports queues and supplies, not capabilities, and nothing writes machine maintenance dates, which is exactly why they are all empty. The two genuinely collector-owned computer fields, lastreporteddate and lastboottime, are deliberately still not editable. Scope: product
772 lines
26 KiB
Vue
772 lines
26 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>{{ isEdit ? 'Edit Printer' : 'New Printer' }}</h2>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
|
|
<form v-else @submit.prevent="savePrinter">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="machinenumber">Windows Name *</label>
|
|
<input
|
|
id="machinenumber"
|
|
v-model="form.machinenumber"
|
|
type="text"
|
|
class="form-control"
|
|
required
|
|
@input="onWindowsNameInput"
|
|
:class="{ 'auto-generated': !manualWindowsName && form.machinenumber }"
|
|
/>
|
|
<small class="form-hint">Auto-generated from CSF Name, Alias, Vendor & Model</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="alias">Alias / Location</label>
|
|
<input
|
|
id="alias"
|
|
v-model="form.alias"
|
|
type="text"
|
|
class="form-control"
|
|
placeholder="e.g., SpoolsInspection"
|
|
/>
|
|
<small class="form-hint">Used in Windows name generation</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group" v-if="isEnabled('fqdn', 'printer')">
|
|
<label for="hostname">Hostname (FQDN)</label>
|
|
<input
|
|
id="hostname"
|
|
v-model="form.hostname"
|
|
type="text"
|
|
class="form-control"
|
|
@input="onHostnameInput"
|
|
:class="{ 'auto-generated': !manualHostname && form.hostname }"
|
|
/>
|
|
<small class="form-hint">Auto-generated from IP address</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="serialnumber">Serial Number</label>
|
|
<input
|
|
id="serialnumber"
|
|
v-model="form.serialnumber"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row" v-if="isEnabled('gaugelabreference', 'printer') || isEnabled('maintenancereference', 'printer')">
|
|
<div class="form-group" v-if="isEnabled('gaugelabreference', 'printer')">
|
|
<label for="gaugelabreference">Gauge Lab Reference</label>
|
|
<input
|
|
id="gaugelabreference"
|
|
v-model="form.gaugelabreference"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group" v-if="isEnabled('maintenancereference', 'printer')">
|
|
<label for="maintenancereference">Maintenance Reference</label>
|
|
<input
|
|
id="maintenancereference"
|
|
v-model="form.maintenancereference"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="machinetypeid">Printer Type *</label>
|
|
<select
|
|
id="machinetypeid"
|
|
v-model="form.machinetypeid"
|
|
class="form-control"
|
|
required
|
|
@change="form.modelnumberid = ''"
|
|
>
|
|
<option value="">Select type...</option>
|
|
<option
|
|
v-for="pt in printerTypes"
|
|
:key="pt.printertypeid"
|
|
:value="pt.printertypeid"
|
|
>
|
|
{{ pt.printertype }}
|
|
</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>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="vendorid">Vendor</label>
|
|
<select
|
|
id="vendorid"
|
|
v-model="form.vendorid"
|
|
class="form-control"
|
|
@change="form.modelnumberid = ''"
|
|
>
|
|
<option value="">Select vendor...</option>
|
|
<option
|
|
v-for="v in filteredVendors"
|
|
:key="v.vendorid"
|
|
:value="v.vendorid"
|
|
>
|
|
{{ v.vendor }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="modelnumberid">Model *</label>
|
|
<select
|
|
id="modelnumberid"
|
|
v-model="form.modelnumberid"
|
|
class="form-control"
|
|
:disabled="!form.vendorid"
|
|
required
|
|
>
|
|
<option value="">{{ form.vendorid ? 'Select model...' : 'Select a vendor first' }}</option>
|
|
<option
|
|
v-for="m in filteredModels"
|
|
:key="m.modelnumberid"
|
|
:value="m.modelnumberid"
|
|
>
|
|
{{ m.modelnumber }}
|
|
</option>
|
|
</select>
|
|
<small v-if="!form.vendorid" class="form-hint">
|
|
Select a vendor first to choose a model
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
</div>
|
|
|
|
<!-- Printer-specific fields -->
|
|
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">Printer Settings</h4>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="ipaddress">IP Address</label>
|
|
<input
|
|
id="ipaddress"
|
|
v-model="form.ipaddress"
|
|
type="text"
|
|
class="form-control"
|
|
placeholder="e.g., 192.168.1.100"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="csfname">CSF Name</label>
|
|
<input
|
|
id="csfname"
|
|
v-model="form.csfname"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="pin">PIN</label>
|
|
<input
|
|
id="pin"
|
|
v-model="form.pin"
|
|
type="text"
|
|
class="form-control"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Capabilities. On the detail page since they were added but never
|
|
editable, so a wrong one needed database access to correct.
|
|
Nothing writes these automatically - the collector reports queues
|
|
and supplies, not capabilities - so a person is the only source. -->
|
|
<div class="form-group">
|
|
<label>Capabilities</label>
|
|
<div class="capability-list">
|
|
<label class="capability-item">
|
|
<input type="checkbox" v-model="form.iscolor" />
|
|
<span>Colour</span>
|
|
</label>
|
|
<label class="capability-item">
|
|
<input type="checkbox" v-model="form.isduplex" />
|
|
<span>Duplex</span>
|
|
</label>
|
|
<label class="capability-item">
|
|
<input type="checkbox" v-model="form.isnetwork" />
|
|
<span>Network</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="installpath">Driver Install Path</label>
|
|
<input
|
|
id="installpath"
|
|
v-model="form.installpath"
|
|
type="text"
|
|
class="form-control"
|
|
placeholder="Leave empty for universal driver"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="notes">Notes</label>
|
|
<textarea
|
|
id="notes"
|
|
v-model="form.notes"
|
|
class="form-control"
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
|
|
<!-- Map Location Picker -->
|
|
<div class="form-group">
|
|
<label>Map Location</label>
|
|
<div class="map-location-control">
|
|
<div v-if="form.mapx !== null && form.mapy !== null" class="current-position">
|
|
Position: {{ form.mapx }}, {{ form.mapy }}
|
|
<span v-if="form.levelid" class="position-level">on {{ levelName(form.levelid) }}</span>
|
|
<span v-else class="position-level position-level-missing">level not set</span>
|
|
<button type="button" class="btn btn-sm btn-secondary" @click="clearMapPosition">Clear</button>
|
|
</div>
|
|
<button type="button" class="btn btn-secondary" @click="openMapPicker">
|
|
Set Location on Map
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Map Picker Modal -->
|
|
<Modal v-model="showMapPicker" title="Select Location on Map" size="fullscreen">
|
|
<div class="map-modal-content">
|
|
<div v-if="levelOptions().length > 1" class="map-level-picker">
|
|
<label>Level</label>
|
|
<select v-model.number="pickerLevelId" class="form-control">
|
|
<option v-for="option in levelOptions()" :key="option.levelid"
|
|
:value="option.levelid">{{ option.label }}</option>
|
|
</select>
|
|
<span class="input-hint">
|
|
The position is pixels on this drawing, so pick the level first.
|
|
</span>
|
|
</div>
|
|
<ShopFloorMap
|
|
:pickerMode="true"
|
|
:levelid="pickerLevelId"
|
|
:initialPosition="form.mapx !== null ? { left: form.mapx, top: form.mapy } : null"
|
|
:theme="currentTheme"
|
|
@positionPicked="handlePositionPicked"
|
|
/>
|
|
</div>
|
|
<template #footer>
|
|
<button class="btn btn-secondary" @click="showMapPicker = false">Cancel</button>
|
|
<button class="btn btn-primary" @click="confirmMapPosition">Confirm Location</button>
|
|
</template>
|
|
</Modal>
|
|
|
|
<!-- Site-defined custom fields for printers -->
|
|
<CustomFieldsInputs ref="customFieldsRef" :assettypeid="PRINTER_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 Printer' }}
|
|
</button>
|
|
<router-link to="/printers" class="btn btn-secondary">Cancel</router-link>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { assetsApi, vendorsApi, locationsApi, printersApi, modelsApi } from '@/api'
|
|
import ShopFloorMap from '@/components/ShopFloorMap.vue'
|
|
import { loadMapConfig, levelOptions, levelName, state as mapConfig }
|
|
from '@/composables/mapConfig'
|
|
import Modal from '@/components/Modal.vue'
|
|
import CustomFieldsInputs from '@/components/CustomFieldsInputs.vue'
|
|
import { currentTheme } from '@/stores/theme'
|
|
import { useIdentifierFlags } from '@/composables/identifierSettings'
|
|
import { apiError } from '@/utils/apiError'
|
|
import { getPrinterHostnameTemplate } from '@/utils/siteSettings'
|
|
|
|
const { isEnabled } = useIdentifierFlags()
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const isEdit = computed(() => !!route.params.id)
|
|
|
|
// Seeded asset-type id for printers (see /api/assets/types).
|
|
const PRINTER_ASSETTYPEID = 4
|
|
const customFieldsRef = ref(null)
|
|
const currentAssetId = ref(null)
|
|
const manualHostname = ref(false)
|
|
const manualWindowsName = ref(false)
|
|
|
|
const loading = ref(true)
|
|
const saving = ref(false)
|
|
const error = ref('')
|
|
const showMapPicker = ref(false)
|
|
// Which drawing the picker shows, and therefore which level the coordinates it
|
|
// returns belong to (ADR-017). Opens on the position's existing level so editing
|
|
// a marker does not silently move it to the default one.
|
|
const pickerLevelId = ref(null)
|
|
const tempMapPosition = ref(null)
|
|
|
|
const form = ref({
|
|
machinenumber: '',
|
|
alias: '',
|
|
hostname: '',
|
|
serialnumber: '',
|
|
gaugelabreference: '',
|
|
maintenancereference: '',
|
|
machinetypeid: '',
|
|
statusid: '',
|
|
vendorid: '',
|
|
modelnumberid: '',
|
|
locationid: '',
|
|
notes: '',
|
|
mapx: null,
|
|
mapy: null,
|
|
levelid: null,
|
|
// Printer-specific
|
|
ipaddress: '',
|
|
csfname: '',
|
|
installpath: '',
|
|
pin: '',
|
|
iscolor: false,
|
|
isduplex: false,
|
|
// true to match the create default in the API; most printers here are
|
|
// networked and a new record that claimed otherwise would be wrong.
|
|
isnetwork: true
|
|
})
|
|
|
|
const printerTypes = ref([])
|
|
const statuses = ref([])
|
|
const vendors = ref([])
|
|
const models = ref([])
|
|
const locations = ref([])
|
|
|
|
// Ensure a saved model id is present in models.value even when the filtered
|
|
// list (category Printer) excluded it because it has no modeltype. Fetches the
|
|
// single row by id and appends it; a no-op when already present or blank.
|
|
async function ensureModelLoaded(modelId) {
|
|
if (!modelId) return
|
|
if (models.value.some(m => m.modelnumberid === modelId)) return
|
|
try {
|
|
const response = await modelsApi.get(modelId)
|
|
const model = response.data.data
|
|
if (model) models.value = [...models.value, model]
|
|
} catch (e) {
|
|
// Leave it absent; the select shows blank but we did not invent a value.
|
|
}
|
|
}
|
|
|
|
// Filter models by selected vendor and printer type
|
|
const filteredModels = computed(() => {
|
|
const savedId = form.value.modelnumberid
|
|
return models.value.filter(m => {
|
|
// Always keep the record's own saved model, so editing a printer never
|
|
// renders a blank select that clears the field on save.
|
|
if (m.modelnumberid === savedId) return true
|
|
// Filter by vendor if selected
|
|
if (form.value.vendorid && m.vendorid !== form.value.vendorid) {
|
|
return false
|
|
}
|
|
// 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
|
|
})
|
|
})
|
|
|
|
// Only offer vendors that actually have Printer-category models. models is
|
|
// already scoped to category Printer at load, so its vendorids are the printer
|
|
// vendor set. Keep the record's own vendor even if it falls outside that set,
|
|
// so editing an existing printer never silently drops its saved vendor.
|
|
const filteredVendors = computed(() => {
|
|
const printerVendorIds = new Set(models.value.map(m => m.vendorid))
|
|
return vendors.value.filter(
|
|
v => printerVendorIds.has(v.vendorid) || v.vendorid === form.value.vendorid
|
|
)
|
|
})
|
|
|
|
// Get short description from model number for naming
|
|
function getModelShortDesc(modelNumber) {
|
|
if (!modelNumber) return ''
|
|
const mn = modelNumber.toLowerCase()
|
|
|
|
if (mn.includes('colorlaserjet')) return 'ColorLaserJet'
|
|
if (mn.includes('laserjetpro') || mn.includes('laserjet pro')) return 'LaserJetPro'
|
|
if (mn.includes('laserjet')) return 'LaserJet'
|
|
if (mn.includes('altalink')) return 'Altalink'
|
|
if (mn.includes('versalink')) return 'Versalink'
|
|
if (mn.includes('designjet')) return 'DesignJet'
|
|
if (mn.includes('dtc')) return 'DTC'
|
|
if (mn.includes('officejet')) return 'OfficeJet'
|
|
if (mn.includes('pagewide')) return 'PageWide'
|
|
|
|
// Fallback: get letters before first digit
|
|
const match = modelNumber.match(/^([A-Za-z]+)/)
|
|
return match ? match[1] : modelNumber.substring(0, 5)
|
|
}
|
|
|
|
// Auto-generate hostname from IP address using the site template ({ip} =
|
|
// dash-separated IP).
|
|
async function generateHostname(ip) {
|
|
if (!ip) return ''
|
|
const ipDashed = ip.replace(/\./g, '-')
|
|
const template = await getPrinterHostnameTemplate()
|
|
return template.replace('{ip}', ipDashed)
|
|
}
|
|
|
|
// Auto-generate Windows name (machinenumber)
|
|
function generateWindowsName() {
|
|
const parts = []
|
|
|
|
// 1. CSF Name (if set and not "NONE")
|
|
const csfName = form.value.csfname?.trim()
|
|
if (csfName && csfName.toUpperCase() !== 'NONE') {
|
|
parts.push(csfName.replace(/\s+/g, ''))
|
|
}
|
|
|
|
// 2. Location (from alias, removing spaces and "Machine")
|
|
const alias = form.value.alias?.trim()
|
|
if (alias) {
|
|
const location = alias.replace(/\s+/g, '').replace(/Machine/gi, '')
|
|
// Skip if same as CSF name
|
|
if (location.toLowerCase() !== csfName?.toLowerCase()) {
|
|
parts.push(location)
|
|
}
|
|
}
|
|
|
|
// 3. Vendor + Model short description
|
|
const selectedModel = models.value.find(m => m.modelnumberid === form.value.modelnumberid)
|
|
const selectedVendor = vendors.value.find(v => v.vendorid === form.value.vendorid)
|
|
|
|
let vendorModel = ''
|
|
if (selectedVendor) {
|
|
vendorModel = selectedVendor.vendor.replace(/\s+/g, '')
|
|
}
|
|
if (selectedModel) {
|
|
vendorModel += getModelShortDesc(selectedModel.modelnumber)
|
|
}
|
|
if (vendorModel) {
|
|
parts.push(vendorModel)
|
|
}
|
|
|
|
return parts.join('-')
|
|
}
|
|
|
|
// Watch IP address and auto-generate hostname
|
|
watch(() => form.value.ipaddress, async (newIp) => {
|
|
if (!manualHostname.value && newIp) {
|
|
form.value.hostname = await generateHostname(newIp)
|
|
}
|
|
})
|
|
|
|
// Watch fields that affect Windows name generation
|
|
watch(
|
|
() => [form.value.csfname, form.value.alias, form.value.vendorid, form.value.modelnumberid],
|
|
() => {
|
|
if (!manualWindowsName.value && !isEdit.value) {
|
|
const generated = generateWindowsName()
|
|
if (generated) {
|
|
form.value.machinenumber = generated
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
// Track manual edits to hostname
|
|
function onHostnameInput() {
|
|
manualHostname.value = true
|
|
}
|
|
|
|
// Track manual edits to Windows name
|
|
function onWindowsNameInput() {
|
|
manualWindowsName.value = true
|
|
}
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
// 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({ category: 'Printer' }),
|
|
locationsApi.listAll() // backend caps perpage at 100; page through all
|
|
])
|
|
|
|
printerTypes.value = mtRes.data.data || []
|
|
statuses.value = statusRes.data.data || []
|
|
vendors.value = vendorRes.data.data || []
|
|
models.value = allModels
|
|
locations.value = locRes || []
|
|
|
|
// Load printer if editing
|
|
if (isEdit.value) {
|
|
const response = await printersApi.get(route.params.id)
|
|
const printer = response.data.data
|
|
currentAssetId.value = printer.assetid || null
|
|
// 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 = {
|
|
// "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 || '',
|
|
gaugelabreference: printer.gaugelabreference || '',
|
|
maintenancereference: printer.maintenancereference || '',
|
|
machinetypeid: ext.printertypeid || '',
|
|
statusid: printer.statusid || '',
|
|
vendorid: ext.vendorid || '',
|
|
modelnumberid: ext.modelnumberid || '',
|
|
locationid: printer.locationid || '',
|
|
notes: printer.notes || '',
|
|
iscolor: !!printer.printer?.iscolor,
|
|
isduplex: !!printer.printer?.isduplex,
|
|
isnetwork: printer.printer?.isnetwork !== false,
|
|
mapx: printer.mapx ?? null,
|
|
mapy: printer.mapy ?? null,
|
|
levelid: printer.levelid ?? null,
|
|
// Printer-specific
|
|
ipaddress: primaryComm?.ipaddress || '',
|
|
csfname: ext.sharename || '',
|
|
installpath: ext.installpath || '',
|
|
pin: ext.pin || ''
|
|
}
|
|
|
|
// The model list is loaded filtered to category Printer, which the backend
|
|
// inner-joins against ModelType - so an untyped model is absent. If this
|
|
// printer's saved model is one of those rows, fetch it by id and merge it
|
|
// in, or its dropdown would render blank and saving would clear the field.
|
|
await ensureModelLoaded(form.value.modelnumberid)
|
|
|
|
// Don't auto-generate for existing printers
|
|
manualWindowsName.value = true
|
|
manualHostname.value = true
|
|
}
|
|
} catch (err) {
|
|
console.error('Error loading data:', err)
|
|
error.value = 'Failed to load data'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
})
|
|
|
|
function handlePositionPicked(position) {
|
|
tempMapPosition.value = position
|
|
}
|
|
|
|
function openMapPicker() {
|
|
loadMapConfig().then(() => {
|
|
pickerLevelId.value = form.value.levelid || mapConfig.defaultlevelid
|
|
showMapPicker.value = true
|
|
})
|
|
}
|
|
|
|
function confirmMapPosition() {
|
|
if (tempMapPosition.value) {
|
|
form.value.mapx = tempMapPosition.value.left
|
|
form.value.mapy = tempMapPosition.value.top
|
|
// Never one without the other: coordinates saved with no level render as
|
|
// "level unknown", and coordinates saved against the wrong level render
|
|
// convincingly in the wrong place.
|
|
form.value.levelid = pickerLevelId.value
|
|
}
|
|
showMapPicker.value = false
|
|
}
|
|
|
|
function clearMapPosition() {
|
|
form.value.mapx = null
|
|
form.value.mapy = null
|
|
form.value.levelid = null
|
|
tempMapPosition.value = null
|
|
}
|
|
|
|
async function savePrinter() {
|
|
error.value = ''
|
|
|
|
// Model is required. The native `required` on the select is skipped while it
|
|
// is disabled (no vendor picked), so guard here too and point at the vendor.
|
|
if (!form.value.modelnumberid) {
|
|
error.value = form.value.vendorid
|
|
? 'Model is required.'
|
|
: 'Select a vendor, then a model (model is required).'
|
|
return
|
|
}
|
|
|
|
saving.value = true
|
|
|
|
try {
|
|
// 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,
|
|
gaugelabreference: form.value.gaugelabreference || null,
|
|
maintenancereference: form.value.maintenancereference || 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,
|
|
sharename: form.value.csfname || null,
|
|
iscsf: !!form.value.csfname,
|
|
installpath: form.value.installpath || null,
|
|
pin: form.value.pin || null,
|
|
ipaddress: form.value.ipaddress || null,
|
|
iscolor: !!form.value.iscolor,
|
|
isduplex: !!form.value.isduplex,
|
|
isnetwork: !!form.value.isnetwork,
|
|
mapx: form.value.mapx,
|
|
mapy: form.value.mapy
|
|
}
|
|
// 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
|
|
}
|
|
|
|
let assetId = currentAssetId.value
|
|
if (isEdit.value) {
|
|
const response = await printersApi.update(route.params.id, payload)
|
|
assetId = assetId || response.data?.data?.assetid || response.data?.data?.asset?.assetid
|
|
} else {
|
|
const response = await printersApi.create(payload)
|
|
assetId = response.data?.data?.assetid || response.data?.data?.asset?.assetid
|
|
}
|
|
|
|
if (assetId && customFieldsRef.value) {
|
|
try {
|
|
await customFieldsRef.value.save(assetId)
|
|
} catch (cfErr) {
|
|
console.error('Error saving custom fields:', cfErr)
|
|
}
|
|
}
|
|
|
|
router.push('/printers')
|
|
} catch (err) {
|
|
console.error('Error saving printer:', err)
|
|
error.value = apiError(err, 'Failed to save printer')
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.capability-list { display: flex; flex-wrap: wrap; gap: 1rem; }
|
|
.capability-item { display: inline-flex; align-items: center; gap: 0.4rem; font-weight: normal; }
|
|
|
|
.map-location-control {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.current-position {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 1rem;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
color: var(--text);
|
|
}
|
|
|
|
.map-modal-content {
|
|
height: calc(90vh - 140px);
|
|
}
|
|
|
|
.map-modal-content :deep(.shopfloor-map) {
|
|
height: 100%;
|
|
}
|
|
|
|
.map-modal-content :deep(.map-container) {
|
|
height: calc(100% - 50px);
|
|
}
|
|
|
|
.form-hint {
|
|
display: block;
|
|
margin-top: 0.25rem;
|
|
font-size: 0.8rem;
|
|
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: rgba(33, 150, 243, 0.12);
|
|
border-color: #90caf9;
|
|
}
|
|
</style>
|