Files
shopdb-flask/frontend/src/views/print/assetLabel.js
cproudlock a846587f39
All checks were successful
CI / backend (push) Successful in 1m23s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s
Add email sending (service + 3 flows) and a general asset label generator
Email: a stdlib SMTP mailer (settings-first config, graceful no-op when
unconfigured), a test-email endpoint wired to the Email settings page,
forced first-login password change (users.mustchangepassword, migration
7d23, /change-password flow), new-user welcome mail, and on-demand
report/alert delivery (POST /api/reports/email + Email Report buttons)
with an external-cron-with-a-scoped-PAT path documented for automation.
All tests patch smtplib - no network.

Labels: a shared /print/asset-label/<type>/<id> view any asset detail
page opens - card or plain style, QR or barcode, configurable encoding.
Per-type qr_target_* templates plus label_default_style/codetype/encodes
settings on the Printing page. Measuring-tool labels default to encoding
their inspection-operation code (derived from the location name, e.g.
0615), so every tool in an area shares the area code - verified by
decoding the rendered QR. Machine labels default to the machine number;
blank-serial handled gracefully.

808 tests pass; both features verified live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 11:58:30 -04:00

100 lines
4.0 KiB
JavaScript

// Shared wiring for the asset label pages (single AssetLabel.vue and batch
// AssetLabelBatch.vue): per-type api/route config plus the "what does the code
// encode" resolution so both surfaces behave identically.
import {
machinesApi, computersApi, printersApi, networkApi, measuringtoolsApi,
} from '../../api'
import { fillUrlTemplate } from '@/utils/qrTarget'
import { getSetting, getSiteBaseUrl } from '@/utils/siteSettings'
// Per asset-type: which api loads it, its detail/list routes, the extension key
// nested in the merged payload, and its qr_target_<type> settings key.
export const TYPE_CONFIG = {
machine: {
api: machinesApi, extkey: 'machine', targetKey: 'qr_target_machine',
label: 'Machine', detailPath: id => `/machines/${id}`, listPath: '/machines',
},
computer: {
api: computersApi, extkey: 'computer', targetKey: 'qr_target_computer',
label: 'Computer', detailPath: id => `/pcs/${id}`, listPath: '/pcs',
},
printer: {
api: printersApi, extkey: 'printer', targetKey: 'qr_target_printer',
label: 'Printer', detailPath: id => `/printers/${id}`, listPath: '/printers',
},
network_device: {
api: networkApi, extkey: 'network_device', targetKey: 'qr_target_network_device',
label: 'Network Device', detailPath: id => `/network/${id}`, listPath: '/network',
},
measuring_tool: {
api: measuringtoolsApi, extkey: 'measuringtool', targetKey: 'qr_target_measuring_tool',
label: 'Measuring Tool', detailPath: id => `/measuringtools/${id}`, listPath: '/measuringtools',
},
}
// Hardcoded fallback default per type when the site setting is unset. Machines
// encode their machine number, measuring tools their inspection location.
const DEFAULT_ENCODES = { machine: 'assetnumber', measuring_tool: 'location' }
export function hasLocationType(assettype) {
return assettype === 'measuring_tool'
}
// The default encode mode for a type: the label_default_encodes_<type> site
// setting when valid, else the hardcoded fallback.
export async function resolveDefaultEncodes(assettype) {
const valid = ['assetpage', 'assetnumber', 'serialnumber', 'custom']
if (hasLocationType(assettype)) valid.push('location')
const seeded = await getSetting(`label_default_encodes_${assettype}`, '')
return valid.includes(seeded) ? seeded : (DEFAULT_ENCODES[assettype] || 'assetpage')
}
// 'location' degrades to the asset page when the asset has no location code.
export function effectiveEncodes(encodes, asset) {
if (encodes === 'location' && !asset?.locationcode) return 'assetpage'
return encodes
}
function tokensFor(asset) {
return {
assetid: asset.assetid || '',
assetnumber: asset.assetnumber || '',
serialnumber: asset.serialnumber || '',
name: asset.name || '',
pluginid: asset.pluginid || '',
locationcode: asset.locationcode || '',
locationname: asset.locationname || '',
}
}
// The string a label's code encodes for one asset. Empty when the chosen field
// has no value (e.g. serial number on an asset with none).
export async function resolveCodeText(assettype, asset, encodes) {
const config = TYPE_CONFIG[assettype]
if (!config || !asset) return ''
const detailId = asset.pluginid
const assetPageUrl = `${await getSiteBaseUrl()}${config.detailPath(detailId)}`
switch (effectiveEncodes(encodes, asset)) {
case 'assetnumber': return asset.assetnumber || ''
case 'serialnumber': return asset.serialnumber || ''
case 'location': return asset.locationcode || ''
case 'custom': {
const template = (await getSetting(config.targetKey, '')).trim()
return template ? fillUrlTemplate(template, tokensFor(asset)) : assetPageUrl
}
case 'assetpage':
default: return assetPageUrl
}
}
// Human caption printed under the code.
export function captionFor(asset, encodes) {
if (!asset) return ''
switch (effectiveEncodes(encodes, asset)) {
case 'location': return asset.locationcode || ''
case 'serialnumber': return asset.serialnumber || ''
case 'assetnumber':
default: return asset.assetnumber || ''
}
}