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

@@ -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>