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:
@@ -4,6 +4,8 @@
|
||||
<h1>Toner Report</h1>
|
||||
<div class="header-actions">
|
||||
<button v-if="!loading && !error" class="btn btn-secondary" @click="exportCSV">Export CSV</button>
|
||||
<EmailReportButton v-if="!loading && !error" subject="Toner / Supply Report"
|
||||
:columns="emailColumns" :rows="emailRows" />
|
||||
<router-link to="/reports" class="btn btn-secondary">Back to Reports</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,6 +101,17 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { printersApi } from '@/api'
|
||||
import EmailReportButton from '../../components/EmailReportButton.vue'
|
||||
|
||||
const emailColumns = [
|
||||
{ key: 'printer', label: 'Printer' },
|
||||
{ key: 'assetnumber', label: 'Asset #' },
|
||||
{ key: 'location', label: 'Location' },
|
||||
{ key: 'ipaddress', label: 'IP Address' },
|
||||
{ key: 'supply', label: 'Supply' },
|
||||
{ key: 'level', label: 'Level' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
]
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
@@ -119,6 +132,25 @@ 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
|
||||
})
|
||||
|
||||
function exportCSV() {
|
||||
// one row per supply, honoring the active filter
|
||||
const quote = value => `"${String(value ?? '').replace(/"/g, '""')}"`
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<h1>Warranty Report</h1>
|
||||
<div class="header-actions">
|
||||
<button v-if="!loading" class="btn btn-secondary" @click="exportCSV">Export CSV</button>
|
||||
<EmailReportButton v-if="!loading" subject="Warranty Report"
|
||||
:columns="emailColumns" :rows="emailRows" />
|
||||
<router-link to="/reports" class="btn btn-secondary">Back to Reports</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,13 +55,39 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { warrantyApi } from '../../api'
|
||||
import EmailReportButton from '../../components/EmailReportButton.vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const counts = ref({})
|
||||
const buckets = ref({})
|
||||
|
||||
const emailColumns = [
|
||||
{ key: 'bucket', label: 'Status' },
|
||||
{ key: 'vendor', label: 'Vendor' },
|
||||
{ key: 'servicelevel', label: 'Service Level' },
|
||||
{ key: 'enddate', label: 'Ends' },
|
||||
{ key: 'assets', label: 'Covers' },
|
||||
]
|
||||
|
||||
// Flatten the buckets into one row per warranty for the emailed table.
|
||||
const emailRows = computed(() => {
|
||||
const rows = []
|
||||
for (const b of bucketOrder) {
|
||||
for (const w of buckets.value[b.key] || []) {
|
||||
rows.push({
|
||||
bucket: b.label,
|
||||
vendor: w.vendor || '',
|
||||
servicelevel: w.servicelevel || '',
|
||||
enddate: w.enddate || '',
|
||||
assets: (w.assets || []).map(a => a.assetnumber).join(', '),
|
||||
})
|
||||
}
|
||||
}
|
||||
return rows
|
||||
})
|
||||
|
||||
const bucketOrder = [
|
||||
{ key: 'expired', label: 'Expired', color: '#F44336' },
|
||||
{ key: 'expiring', label: 'Expiring Soon', color: '#FF9800' },
|
||||
|
||||
Reference in New Issue
Block a user