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>
This commit is contained in:
99
frontend/src/views/print/assetLabel.js
Normal file
99
frontend/src/views/print/assetLabel.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// 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 || ''
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user