|
-
+
+
+
+ {{ printer.printername || 'Unknown' }}
+
+
+
{{ printer.printername || 'Unknown' }}
|
- {{ printer.assetnumber }} |
- {{ printer.location || '-' }} |
- {{ printer.ipaddress }} |
+
+
+ {{ fqdnFor(printer) }}
+ {{ printer.ipaddress || '-' }}
+ |
{{ supply.level }}%
+
+ {{ part.partnumber }}
+ -
+
|
@@ -102,13 +132,14 @@
import { ref, computed, onMounted } from 'vue'
import { printersApi } from '@/api'
import EmailReportButton from '@/components/EmailReportButton.vue'
+import LocationMapTooltip from '@/components/LocationMapTooltip.vue'
+import { getPrinterHostnameTemplate } from '@/utils/siteSettings'
const emailColumns = [
{ key: 'printer', label: 'Printer' },
- { key: 'assetnumber', label: 'Asset #' },
- { key: 'location', label: 'Location' },
- { key: 'ipaddress', label: 'IP Address' },
+ { key: 'hostname', label: 'Hostname' },
{ key: 'supply', label: 'Supply' },
+ { key: 'partnumber', label: 'Part Number' },
{ key: 'level', label: 'Level' },
{ key: 'status', label: 'Status' },
]
@@ -125,6 +156,47 @@ const filterOptions = [
{ label: 'Low', value: 'low' }
]
+// Printers have no stored FQDN; the site builds one from the IP via the
+// printer_hostname_template setting, the same way PrinterForm does.
+const hostnameTemplate = ref('')
+
+function fqdnFor(printer) {
+ if (!printer.ipaddress || !hostnameTemplate.value) return ''
+ return hostnameTemplate.value.replace('{ip}', printer.ipaddress.replace(/\./g, '-'))
+}
+
+// A model can list several capacity tiers for one color, so the chip shows the
+// part number and the tooltip says which one it is.
+function partTooltip(part) {
+ const bits = [part.marketingname, part.capacitytier]
+ if (part.pageyield) bits.push(part.pageyield + ' pages')
+ return bits.filter(Boolean).join(' - ')
+}
+
+// One flat row per part number so an ordering list is copy-pasteable; supplies
+// with no part on file still get a row.
+function reportRows() {
+ const rows = []
+ for (const printer of filteredPrinters.value) {
+ for (const supply of printer.supplies || []) {
+ const parts = supply.partnumbers && supply.partnumbers.length
+ ? supply.partnumbers.map(p => p.partnumber)
+ : ['']
+ for (const partnumber of parts) {
+ rows.push({
+ printer: printer.printername || '',
+ hostname: fqdnFor(printer) || printer.ipaddress || '',
+ supply: supply.name || '',
+ partnumber: partnumber,
+ level: supply.level,
+ status: supply.status || '',
+ })
+ }
+ }
+ }
+ return rows
+}
+
const filteredPrinters = computed(() => {
if (filter.value === 'all') return printers.value
return printers.value.filter(p =>
@@ -132,36 +204,17 @@ const filteredPrinters = computed(() => {
)
})
-// One row per supply, honoring the active filter, for the emailed table.
-const emailRows = computed(() => {
- const rows = []
- for (const printer of filteredPrinters.value) {
- for (const supply of printer.supplies || []) {
- rows.push({
- printer: printer.printername || '',
- assetnumber: printer.assetnumber || '',
- location: printer.location || '',
- ipaddress: printer.ipaddress || '',
- supply: supply.name || '',
- level: supply.level + '%',
- status: supply.status || '',
- })
- }
- }
- return rows
-})
+// Emailed table, honoring the active filter.
+const emailRows = computed(() =>
+ reportRows().map(row => ({ ...row, level: row.level + '%' }))
+)
function exportCSV() {
- // one row per supply, honoring the active filter
const quote = value => `"${String(value ?? '').replace(/"/g, '""')}"`
- const rows = [['printer', 'assetnumber', 'location', 'ipaddress', 'supply', 'level', 'status']]
- for (const printer of filteredPrinters.value) {
- for (const supply of printer.supplies || []) {
- rows.push([
- printer.printername || '', printer.assetnumber || '', printer.location || '',
- printer.ipaddress || '', supply.name || '', supply.level, supply.status || ''
- ])
- }
+ const header = ['printer', 'hostname', 'supply', 'partnumber', 'level', 'status']
+ const rows = [header]
+ for (const row of reportRows()) {
+ rows.push(header.map(key => row[key]))
}
const csv = rows.map(row => row.map(quote).join(',')).join('\n')
const link = document.createElement('a')
@@ -173,6 +226,7 @@ function exportCSV() {
onMounted(async () => {
try {
+ hostnameTemplate.value = await getPrinterHostnameTemplate()
const response = await printersApi.lowSupplies()
const data = response.data.data
printers.value = data.printers || []
@@ -218,32 +272,31 @@ onMounted(async () => {
color: var(--danger);
}
+/* One grid for the whole cell, with each supply row contributing its cells
+ directly (display: contents). Cartridge names were being ellipsised inside a
+ fixed 120px column, which hid the very thing being reordered; a max-content
+ column sizes to the longest name instead, and the columns still line up
+ across the rows of one printer. */
.supplies-cell {
- min-width: 280px;
+ min-width: 460px;
+ display: grid;
+ grid-template-columns: max-content minmax(80px, 1fr) auto max-content;
+ column-gap: 0.5rem;
+ row-gap: 0.35rem;
+ align-items: center;
}
.supply-row {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- margin-bottom: 0.35rem;
-}
-
-.supply-row:last-child {
- margin-bottom: 0;
+ display: contents;
}
.supply-name {
- flex: 0 0 120px;
font-size: 0.85rem;
color: var(--text-light);
- overflow: hidden;
- text-overflow: ellipsis;
white-space: nowrap;
}
.supply-bar-track {
- flex: 1;
height: 8px;
background: var(--border);
border-radius: 4px;
@@ -269,12 +322,33 @@ onMounted(async () => {
}
.supply-level {
- flex: 0 0 40px;
+ min-width: 40px;
text-align: right;
font-size: 0.85rem;
font-weight: 600;
}
+.supply-parts {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+}
+
+.part-chip {
+ font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+ font-size: 0.75rem;
+ padding: 0.05rem 0.35rem;
+ border: 1px solid var(--border);
+ border-radius: 3px;
+ color: var(--text);
+ white-space: nowrap;
+}
+
+.part-none {
+ font-size: 0.85rem;
+ color: var(--text-light);
+}
+
.supply-text-ok {
color: var(--success);
}