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:
@@ -18,7 +18,7 @@
|
||||
<div class="hero-card">
|
||||
<div class="hero-content">
|
||||
<div class="hero-title">
|
||||
<h1>{{ printer.name || printer.assetnumber }}</h1>
|
||||
<h1>{{ displayTitle }}</h1>
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span class="badge badge-lg badge-printer">Printer</span>
|
||||
@@ -63,7 +63,7 @@
|
||||
<span class="info-label">Windows Name</span>
|
||||
<span class="info-value mono">{{ printer.printer.windowsname }}</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="printer.printer?.hostname">
|
||||
<div class="info-row" v-if="identifierflags.fqdn && printer.printer?.hostname">
|
||||
<span class="info-label">Hostname / FQDN</span>
|
||||
<span class="info-value mono">{{ printer.printer.hostname }}</span>
|
||||
</div>
|
||||
@@ -172,23 +172,30 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="supplies-grid">
|
||||
<div v-for="supply in supplies" :key="supply.supplyid" class="supply-item">
|
||||
<div v-for="supply in supplies" :key="supply.itemid || supply.name" class="supply-item">
|
||||
<div class="supply-header">
|
||||
<span class="supply-name">{{ supply.supplyname }}</span>
|
||||
<span class="supply-level" :class="getSupplyLevelClass(supply.currentlevel)">
|
||||
{{ supply.currentlevel !== null ? `${supply.currentlevel}%` : 'N/A' }}
|
||||
<span class="supply-name">{{ supply.name }}</span>
|
||||
<span class="supply-level" :class="supply.status">
|
||||
{{ supply.level !== null ? `${supply.level}%` : 'N/A' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="supply-bar">
|
||||
<div
|
||||
class="supply-bar-fill"
|
||||
:class="getSupplyLevelClass(supply.currentlevel)"
|
||||
:style="{ width: `${supply.currentlevel || 0}%` }"
|
||||
:class="supply.status"
|
||||
:style="{ width: `${supply.level || 0}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="supply-meta">
|
||||
<span>{{ supply.supplytypename }}</span>
|
||||
<span v-if="supply.partnumber">Part: {{ supply.partnumber }}</span>
|
||||
<span>{{ formatSupplyType(supply.supplytype) }}<template v-if="supply.iswaste"> ({{ supply.remaining }}% remaining)</template></span>
|
||||
<span v-if="supply.partnumbers && supply.partnumbers.length" class="supply-parts">
|
||||
<span
|
||||
v-for="part in supply.partnumbers"
|
||||
:key="part.partnumber"
|
||||
class="supply-part"
|
||||
:data-tip="`Part ${part.partnumber}` + (part.pageyield ? ` - ${part.pageyield} pages` : '')"
|
||||
>{{ part.marketingname || part.partnumber }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,14 +245,26 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { printersApi } from '../../api'
|
||||
import LocationMapTooltip from '../../components/LocationMapTooltip.vue'
|
||||
import { useIdentifierFlags } from '../../composables/identifierSettings'
|
||||
|
||||
const route = useRoute()
|
||||
const identifierflags = useIdentifierFlags()
|
||||
|
||||
const loading = ref(true)
|
||||
const printer = ref(null)
|
||||
const supplies = ref([])
|
||||
const drivers = ref([])
|
||||
|
||||
// Best display identifier for a printer. name is often the literal "NONE",
|
||||
// so fall back to the Windows name, then hostname, then asset number.
|
||||
const displayTitle = computed(() => {
|
||||
const p = printer.value
|
||||
if (!p) return ''
|
||||
const name = (p.name || '').trim()
|
||||
if (name && name.toUpperCase() !== 'NONE') return name
|
||||
return p.printer?.windowsname || p.printer?.hostname || p.assetnumber
|
||||
})
|
||||
|
||||
// Get IP address from communications
|
||||
const ipAddress = computed(() => {
|
||||
if (!printer.value?.communications) return null
|
||||
@@ -262,7 +281,8 @@ onMounted(async () => {
|
||||
])
|
||||
|
||||
printer.value = printerRes.data.data
|
||||
supplies.value = suppliesRes.data.data || []
|
||||
// supplies endpoint returns {ipaddress, pingstatus, supplies:[...]}
|
||||
supplies.value = suppliesRes.data.data?.supplies || []
|
||||
drivers.value = driversRes.data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error loading printer:', error)
|
||||
@@ -280,11 +300,9 @@ function getStatusClass(status) {
|
||||
return 'badge-info'
|
||||
}
|
||||
|
||||
function getSupplyLevelClass(level) {
|
||||
if (level === null || level === undefined) return ''
|
||||
if (level <= 10) return 'critical'
|
||||
if (level <= 25) return 'low'
|
||||
return 'ok'
|
||||
function formatSupplyType(supplytype) {
|
||||
if (!supplytype) return ''
|
||||
return supplytype.charAt(0).toUpperCase() + supplytype.slice(1)
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
@@ -360,4 +378,33 @@ function formatDate(dateStr) {
|
||||
color: var(--text-light);
|
||||
margin-top: 0.625rem;
|
||||
}
|
||||
|
||||
.supply-parts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.supply-part {
|
||||
cursor: help;
|
||||
border-bottom: 1px dotted var(--text-light);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* instant CSS tooltip, no native title delay */
|
||||
.supply-part:hover::after {
|
||||
content: attr(data-tip);
|
||||
position: absolute;
|
||||
bottom: 125%;
|
||||
right: 0;
|
||||
white-space: nowrap;
|
||||
background: var(--text);
|
||||
color: var(--bg-card);
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
placeholder="Search printers..."
|
||||
@input="debouncedSearch"
|
||||
/>
|
||||
<select v-model="typeFilter" class="form-control" @change="onFilterChange">
|
||||
<option value="">All types</option>
|
||||
<option v-for="pt in printerTypes" :key="pt.printertypeid" :value="pt.printertypeid">
|
||||
{{ pt.printertype }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -27,9 +33,10 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset #</th>
|
||||
<th>Asset Tag</th>
|
||||
<th>Name</th>
|
||||
<th>Business Unit</th>
|
||||
<th>Type</th>
|
||||
<th>Model</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
@@ -38,8 +45,9 @@
|
||||
<tbody>
|
||||
<tr v-for="printer in printers" :key="printer.printer?.printerid || printer.assetid">
|
||||
<td>{{ printer.assetnumber }}</td>
|
||||
<td>{{ printer.name || '-' }}</td>
|
||||
<td>{{ printer.name && printer.name.toUpperCase() !== 'NONE' ? printer.name : (printer.printer?.hostname || '-') }}</td>
|
||||
<td>{{ printer.businessunitname || '-' }}</td>
|
||||
<td>{{ printer.printer?.printertypename || '-' }}</td>
|
||||
<td>{{ printer.printer?.modelname || '-' }}</td>
|
||||
<td>
|
||||
<span class="badge" :class="getStatusClass(printer.statusname)">
|
||||
@@ -56,7 +64,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printers.length === 0">
|
||||
<td colspan="6" style="text-align: center; color: var(--text-light);">
|
||||
<td colspan="7" style="text-align: center; color: var(--text-light);">
|
||||
No printers found
|
||||
</td>
|
||||
</tr>
|
||||
@@ -83,6 +91,8 @@ import { printersApi } from '../../api'
|
||||
import PaginationBar from '../../components/PaginationBar.vue'
|
||||
|
||||
const printers = ref([])
|
||||
const printerTypes = ref([])
|
||||
const typeFilter = ref('')
|
||||
const loading = ref(true)
|
||||
const search = ref('')
|
||||
const page = ref(1)
|
||||
@@ -91,7 +101,13 @@ const perPage = ref(20)
|
||||
|
||||
let searchTimeout = null
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await printersApi.types.list({ perpage: 100 })
|
||||
printerTypes.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('Error loading printer types:', error)
|
||||
}
|
||||
loadPrinters()
|
||||
})
|
||||
|
||||
@@ -103,6 +119,7 @@ async function loadPrinters() {
|
||||
perpage: perPage.value
|
||||
}
|
||||
if (search.value) params.search = search.value
|
||||
if (typeFilter.value) params.typeid = typeFilter.value
|
||||
|
||||
const response = await printersApi.list(params)
|
||||
printers.value = response.data.data || []
|
||||
@@ -122,6 +139,11 @@ function debouncedSearch() {
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function onFilterChange() {
|
||||
page.value = 1
|
||||
loadPrinters()
|
||||
}
|
||||
|
||||
function goToPage(p) {
|
||||
page.value = p
|
||||
loadPrinters()
|
||||
|
||||
Reference in New Issue
Block a user