From e6315643778274de919665fa293037ca0526304f Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 26 Jun 2026 08:35:32 -0400 Subject: [PATCH] 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 --- frontend/src/api/index.js | 74 ++- frontend/src/assets/style.css | 9 + frontend/src/router/routes/printers.js | 7 + frontend/src/views/machines/MachineDetail.vue | 10 + frontend/src/views/machines/MachineForm.vue | 40 +- frontend/src/views/machines/MachinesList.vue | 2 +- .../src/views/network/NetworkDevicesList.vue | 2 +- frontend/src/views/pcs/PCDetail.vue | 6 +- frontend/src/views/pcs/PCForm.vue | 28 +- frontend/src/views/pcs/PCsList.vue | 19 +- frontend/src/views/printers/PrinterDetail.vue | 79 +++- frontend/src/views/printers/PrinterForm.vue | 131 +++--- frontend/src/views/printers/PrintersList.vue | 30 +- .../src/views/settings/BusinessUnitsList.vue | 2 +- frontend/src/views/settings/LocationsList.vue | 2 +- .../src/views/settings/MachineTypesList.vue | 2 +- .../src/views/settings/ModelSuppliesList.vue | 437 ++++++++++++++++++ frontend/src/views/settings/PCTypesList.vue | 2 +- frontend/src/views/settings/SettingsIndex.vue | 8 +- frontend/src/views/settings/StatusesList.vue | 16 +- frontend/src/views/settings/UsersList.vue | 2 +- frontend/src/views/settings/VLANsList.vue | 2 +- 22 files changed, 786 insertions(+), 124 deletions(-) create mode 100644 frontend/src/views/settings/ModelSuppliesList.vue diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index e884c03..5dad473 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -264,9 +264,26 @@ export const printersApi = { get(id) { return api.get(`/printers/${id}`) }, + // create/update write asset core + printer extension in one call (the + // printers plugin owns both). Use these instead of the legacy machinesApi. + create(data) { + return api.post('/printers', data) + }, + update(id, data) { + return api.put(`/printers/${id}`, data) + }, updateExtension(id, data) { return api.put(`/printers/${id}/printerdata`, data) }, + // printer sub-types (Laser, Inkjet, Label, Card, Wide Format, ...) + types: { + list(params = {}) { + return api.get('/printers/types', { params }) + }, + create(data) { + return api.post('/printers/types', data) + } + }, updateCommunication(id, data) { return api.put(`/printers/${id}/communication`, data) }, @@ -279,6 +296,12 @@ export const printersApi = { lowSupplies() { return api.get('/printers/lowsupplies') }, + refreshSupplies() { + return api.post('/printers/supplies/refresh') + }, + lookup({ ip, fqdn } = {}) { + return api.get('/printers/lookup', { params: { ip, fqdn } }) + }, dashboardSummary() { return api.get('/printers/dashboard/summary') }, @@ -303,6 +326,27 @@ export const printersApi = { create(data) { return api.post('/printers/supplytypes', data) } + }, + // model -> toner/drum/waste part-number management + modelSupplies: { + meta() { + return api.get('/printers/supplies/meta') + }, + listModels(params = {}) { + return api.get('/printers/models', { params }) + }, + list(modelnumberid) { + return api.get(`/printers/models/${modelnumberid}/supplies`) + }, + create(modelnumberid, data) { + return api.post(`/printers/models/${modelnumberid}/supplies`, data) + }, + update(modelsupplyid, data) { + return api.put(`/printers/supplies/${modelsupplyid}`, data) + }, + delete(modelsupplyid) { + return api.delete(`/printers/supplies/${modelsupplyid}`) + } } } @@ -321,6 +365,23 @@ export const modelsApi = { list(params = {}) { return api.get('/models', { params }) }, + // Backend caps perpage at 100, so page through every model. Returns the + // full array directly (not an axios response). Use in forms whose model + // dropdown must include the editing record's model regardless of page. + async listAll() { + const first = await api.get('/models', { params: { perpage: 100, page: 1 } }) + let items = first.data.data || [] + const totalpages = first.data.meta?.pagination?.totalpages || 1 + if (totalpages > 1) { + const rest = await Promise.all( + Array.from({ length: totalpages - 1 }, (_, i) => + api.get('/models', { params: { perpage: 100, page: i + 2 } }) + ) + ) + rest.forEach(r => { items = items.concat(r.data.data || []) }) + } + return items + }, get(id) { return api.get(`/models/${id}`) }, @@ -528,8 +589,17 @@ export const assetsApi = { } }, statuses: { - list() { - return api.get('/assets/statuses') + list(params = {}) { + return api.get('/assets/statuses', { params }) + }, + create(data) { + return api.post('/assets/statuses', data) + }, + update(id, data) { + return api.put(`/assets/statuses/${id}`, data) + }, + delete(id) { + return api.delete(`/assets/statuses/${id}`) } } } diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css index 148299e..681aeb3 100644 --- a/frontend/src/assets/style.css +++ b/frontend/src/assets/style.css @@ -372,6 +372,15 @@ th, td { border-top: 1px solid var(--border); } +/* Cap a long free-text column (e.g. Description) so it truncates with an + ellipsis instead of widening the row and pushing the Actions column out of + view. Pair with a title attribute to show the full text on hover. */ +td.cell-truncate { + max-width: 32rem; + overflow: hidden; + text-overflow: ellipsis; +} + th { font-weight: 600; font-size: 11px; diff --git a/frontend/src/router/routes/printers.js b/frontend/src/router/routes/printers.js index 0e50cfc..2dd83d8 100644 --- a/frontend/src/router/routes/printers.js +++ b/frontend/src/router/routes/printers.js @@ -23,5 +23,12 @@ export default [ name: 'printer-edit', component: () => import('../../views/printers/PrinterForm.vue'), meta: { requiresAuth: true } + }, + // printer-specific settings + { + path: 'settings/modelsupplies', + name: 'model-supplies', + component: () => import('../../views/settings/ModelSuppliesList.vue'), + meta: { requiresAuth: true } } ] diff --git a/frontend/src/views/machines/MachineDetail.vue b/frontend/src/views/machines/MachineDetail.vue index 97cebef..e1c5585 100644 --- a/frontend/src/views/machines/MachineDetail.vue +++ b/frontend/src/views/machines/MachineDetail.vue @@ -68,6 +68,14 @@ Name {{ equipment.name }} +
+ Gauge Lab Reference + {{ equipment.gaugelabreference }} +
+
+ Maintenance Reference + {{ equipment.maintenancereference }} +
Serial Number {{ equipment.serialnumber }} @@ -229,8 +237,10 @@ import { ref, computed, onMounted } from 'vue' import { useRoute } from 'vue-router' import { equipmentApi, assetsApi } from '../../api' import LocationMapTooltip from '../../components/LocationMapTooltip.vue' +import { useIdentifierFlags } from '../../composables/identifierSettings' const route = useRoute() +const identifierflags = useIdentifierFlags() const loading = ref(true) const equipment = ref(null) diff --git a/frontend/src/views/machines/MachineForm.vue b/frontend/src/views/machines/MachineForm.vue index 5b332c7..b0ced1d 100644 --- a/frontend/src/views/machines/MachineForm.vue +++ b/frontend/src/views/machines/MachineForm.vue @@ -30,6 +30,31 @@ type="text" class="form-control" /> + Layperson-friendly label +
+ + +
+
+ + + Authoritative gauge lab asset reference (if tracked) +
+ +
+ + + Maintenance system asset reference (if tracked)
@@ -331,6 +356,9 @@ import { equipmentApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, co 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() @@ -346,6 +374,8 @@ const tempMapPosition = ref(null) const form = ref({ assetnumber: '', name: '', + gaugelabreference: '', + maintenancereference: '', serialnumber: '', statusid: 1, equipmenttypeid: '', @@ -410,12 +440,12 @@ watch(() => form.value.controllervendorid, (newVal, oldVal) => { onMounted(async () => { try { // Load reference data in parallel - const [typesRes, statusRes, vendorRes, locRes, modelsRes, buRes, pcsRes, relTypesRes] = await Promise.all([ + const [typesRes, statusRes, vendorRes, locRes, allModels, buRes, pcsRes, relTypesRes] = await Promise.all([ equipmentApi.types.list(), assetsApi.statuses.list(), vendorsApi.list({ perpage: 500 }), locationsApi.list({ perpage: 500 }), - modelsApi.list({ perpage: 1000 }), + modelsApi.listAll(), // backend caps perpage at 100; page through all businessunitsApi.list({ perpage: 500 }), computersApi.list({ perpage: 500 }), assetsApi.types.list() // Used for relationship types, will fix below @@ -425,7 +455,7 @@ onMounted(async () => { statuses.value = statusRes.data.data || [] vendors.value = vendorRes.data.data || [] locations.value = locRes.data.data || [] - models.value = modelsRes.data.data || [] + models.value = allModels businessunits.value = buRes.data.data || [] pcs.value = pcsRes.data.data || [] @@ -456,6 +486,8 @@ onMounted(async () => { form.value = { assetnumber: data.assetnumber || '', name: data.name || '', + gaugelabreference: data.gaugelabreference || '', + maintenancereference: data.maintenancereference || '', serialnumber: data.serialnumber || '', statusid: data.statusid || 1, equipmenttypeid: data.equipment?.equipmenttypeid || '', @@ -526,6 +558,8 @@ async function saveEquipment() { 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 || 1, equipmenttypeid: form.value.equipmenttypeid || null, diff --git a/frontend/src/views/machines/MachinesList.vue b/frontend/src/views/machines/MachinesList.vue index bf04767..3bf93a2 100644 --- a/frontend/src/views/machines/MachinesList.vue +++ b/frontend/src/views/machines/MachinesList.vue @@ -24,7 +24,7 @@ - + diff --git a/frontend/src/views/network/NetworkDevicesList.vue b/frontend/src/views/network/NetworkDevicesList.vue index 1fb111d..53dbd15 100644 --- a/frontend/src/views/network/NetworkDevicesList.vue +++ b/frontend/src/views/network/NetworkDevicesList.vue @@ -54,7 +54,7 @@
Asset #Machine # Name Serial Number Type
- + diff --git a/frontend/src/views/pcs/PCDetail.vue b/frontend/src/views/pcs/PCDetail.vue index 3152ff1..b6aca38 100644 --- a/frontend/src/views/pcs/PCDetail.vue +++ b/frontend/src/views/pcs/PCDetail.vue @@ -16,7 +16,7 @@

{{ computer.assetnumber }}

- {{ computer.computer.hostname }} + {{ computer.computer.hostname }}
Computer @@ -57,7 +57,7 @@ Name {{ computer.name }}
-
+
Hostname {{ computer.computer.hostname }}
@@ -207,8 +207,10 @@ import { ref, onMounted, computed } from 'vue' import { useRoute } from 'vue-router' import { computersApi, applicationsApi, assetsApi } from '../../api' import LocationMapTooltip from '../../components/LocationMapTooltip.vue' +import { useIdentifierFlags } from '../../composables/identifierSettings' const route = useRoute() +const identifierflags = useIdentifierFlags() const loading = ref(true) const computer = ref(null) diff --git a/frontend/src/views/pcs/PCForm.vue b/frontend/src/views/pcs/PCForm.vue index 42f8568..d8292d9 100644 --- a/frontend/src/views/pcs/PCForm.vue +++ b/frontend/src/views/pcs/PCForm.vue @@ -32,7 +32,7 @@
-
+
import { ref, onMounted, computed } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { machinesApi, machinetypesApi, statusesApi, vendorsApi, locationsApi, modelsApi, operatingsystemsApi } from '../../api' +import { machinesApi, machinetypesApi, assetsApi, vendorsApi, locationsApi, modelsApi, operatingsystemsApi } 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,7 +320,9 @@ const filteredModels = computed(() => { if (form.value.vendorid && m.vendorid !== form.value.vendorid) { return false } - if (form.value.machinetypeid && m.machinetypeid !== form.value.machinetypeid) { + // only exclude models that have a type set and it differs; most models + // have no machinetypeid, so a strict check hides the PC's own model + if (form.value.machinetypeid && m.machinetypeid && m.machinetypeid !== form.value.machinetypeid) { return false } return true @@ -327,19 +332,20 @@ const filteredModels = computed(() => { onMounted(async () => { try { // Load reference data - const [ptRes, statusRes, vendorRes, modelsRes, locRes, osRes] = await Promise.all([ - machinetypesApi.list({ category: 'PC' }), - statusesApi.list(), - vendorsApi.list(), - modelsApi.list(), - locationsApi.list(), - operatingsystemsApi.list() + // perpage 100 so dropdowns aren't truncated to the default 20-row page + const [ptRes, statusRes, vendorRes, allModels, locRes, osRes] = await Promise.all([ + machinetypesApi.list({ category: 'PC', perpage: 100 }), + assetsApi.statuses.list(), + vendorsApi.list({ perpage: 100 }), + modelsApi.listAll(), // backend caps perpage at 100; page through all + locationsApi.list({ perpage: 100 }), + operatingsystemsApi.list({ perpage: 100 }) ]) pcTypes.value = ptRes.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 || [] operatingsystems.value = osRes.data.data || [] diff --git a/frontend/src/views/pcs/PCsList.vue b/frontend/src/views/pcs/PCsList.vue index 1c401d0..090759f 100644 --- a/frontend/src/views/pcs/PCsList.vue +++ b/frontend/src/views/pcs/PCsList.vue @@ -24,7 +24,7 @@
Asset #Asset Tag Hostname Serial Number Type
- + @@ -153,13 +153,15 @@ function getStatusClass(status) { font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; } +/* keep the cell as a table-cell; flex on a - +
Asset #Asset Tag Hostname Serial Number Type strips table-cell layout and + offsets the row. lay tags out inline instead. */ .features { - display: flex; - gap: 0.375rem; + white-space: nowrap; } .feature-tag { display: inline-block; + margin-right: 0.375rem; padding: 0.3rem 0.625rem; font-size: 0.875rem; border-radius: 5px; @@ -167,6 +169,17 @@ function getStatusClass(status) { color: var(--text-light); } +.feature-tag:last-child { + margin-right: 0; +} + +/* the global .actions rule is inline-flex, which also breaks table-cell + alignment when applied straight on a ; pin it back to a cell here. */ +td.actions { + display: table-cell; + vertical-align: middle; +} + .feature-tag.active { background: #e3f2fd; color: #1976d2; diff --git a/frontend/src/views/printers/PrinterDetail.vue b/frontend/src/views/printers/PrinterDetail.vue index 15a9789..da13633 100644 --- a/frontend/src/views/printers/PrinterDetail.vue +++ b/frontend/src/views/printers/PrinterDetail.vue @@ -18,7 +18,7 @@
-

{{ printer.name || printer.assetnumber }}

+

{{ displayTitle }}

Printer @@ -63,7 +63,7 @@ Windows Name {{ printer.printer.windowsname }}
-
+
Hostname / FQDN {{ printer.printer.hostname }}
@@ -172,23 +172,30 @@
-
+
- {{ supply.supplyname }} - - {{ supply.currentlevel !== null ? `${supply.currentlevel}%` : 'N/A' }} + {{ supply.name }} + + {{ supply.level !== null ? `${supply.level}%` : 'N/A' }}
- {{ supply.supplytypename }} - Part: {{ supply.partnumber }} + {{ formatSupplyType(supply.supplytype) }} + + {{ part.marketingname || part.partnumber }} +
@@ -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; +} diff --git a/frontend/src/views/printers/PrinterForm.vue b/frontend/src/views/printers/PrinterForm.vue index abf5c05..a22ee39 100644 --- a/frontend/src/views/printers/PrinterForm.vue +++ b/frontend/src/views/printers/PrinterForm.vue @@ -37,7 +37,7 @@
-
+
@@ -127,8 +127,9 @@ id="modelnumberid" v-model="form.modelnumberid" class="form-control" + :disabled="!form.vendorid" > - +
@@ -266,10 +267,13 @@ + + diff --git a/frontend/src/views/settings/PCTypesList.vue b/frontend/src/views/settings/PCTypesList.vue index d60ff3d..b2311d5 100644 --- a/frontend/src/views/settings/PCTypesList.vue +++ b/frontend/src/views/settings/PCTypesList.vue @@ -21,7 +21,7 @@
{{ pt.pctype }}{{ pt.description || '-' }}{{ pt.description || '-' }} diff --git a/frontend/src/views/settings/SettingsIndex.vue b/frontend/src/views/settings/SettingsIndex.vue index 5bb3241..c816beb 100644 --- a/frontend/src/views/settings/SettingsIndex.vue +++ b/frontend/src/views/settings/SettingsIndex.vue @@ -27,6 +27,12 @@

Manage equipment models by vendor

+ +
+

Model Toners and Supplies

+

Map toner, drum, and waste part numbers to printer models

+
+

Machine Types

@@ -85,7 +91,7 @@