Every report offers to email itself, and a long drive name stays in its card
Some checks failed
CI / naming (push) Successful in 1m58s
CI / frontend (push) Successful in 1m49s
CI / migrations-mysql (push) Failing after 9m0s
CI / backend (push) Has been cancelled

POST /api/reports/email has always taken any {columns, rows} from any caller,
but the Email report button existed on the toner and warranty pages only - not
because those reports are special, but because they happen to be their own
routed pages and the reports page had no button at all. It is now beside Export
CSV for every report the page can build a table from, which is all of them:
plugin reports through their declared columns, core reports through the flat
list they return, and the asset inventory summary by flattening its three
tables into one with the grouping named in a column. Presentation-only fields -
a status colour, a link id - are left out; a column of hex codes is worse than
no column. The shapes live in a module with tests rather than in the view.

The button is now hidden without reports.export on all three pages. Offering
somebody a button that can only answer 403 is worse than not offering it.

Separately, on the USB kiosk a long drive name pushed Take or Return out
through the side of its card, leaving a drive nobody could press. The name is a
flex item and so defaulted to min-width:auto, keeping its full intrinsic width;
it now yields, wraps mid-word for the serial-style names that carry no spaces,
and clamps its description to two lines so one long name cannot make every card
in the row tall. The locker line is never clipped - it is how somebody finds
the drive.
This commit is contained in:
cproudlock
2026-08-27 10:55:33 -04:00
parent 605dd6cb1b
commit de3ca70592
7 changed files with 233 additions and 12 deletions

View File

@@ -43,6 +43,9 @@
<h2>{{ currentReport.name }}</h2>
<div class="report-actions">
<button class="btn btn-secondary" @click="exportCSV">Export CSV</button>
<EmailReportButton v-if="emailtable && canemail"
:subject="currentReport.name"
:columns="emailtable.columns" :rows="emailtable.rows" />
<button class="btn btn-secondary" @click="clearReport">Back to Reports</button>
</div>
</div>
@@ -266,9 +269,18 @@ import { ref, computed, onMounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import api, { reportsApi, businessunitsApi, assetsApi, locationsApi, applicationsApi } from '@/api'
import { apiError } from '@/utils/apiError'
import EmailReportButton from '@/components/EmailReportButton.vue'
import { emailTableFrom, titleCase } from '@/utils/reportTable'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
// Emailing a report is the reports.export permission, the same one the endpoint
// enforces. Offering the button to somebody who will only get a 403 is worse
// than not offering it.
const canemail = computed(() => authStore.hasPermission('reports.export'))
const reports = ref([])
const search = ref('')
@@ -321,6 +333,12 @@ const genericColumns = computed(() => {
return data.rows?.length ? Object.keys(data.rows[0]) : []
})
// The table to email, built from whatever the open report returned. Emailing
// was available on two pages and nowhere else, purely because those two were
// built as their own routed pages - the endpoint has always taken any
// {columns, rows}. Lives in its own module so the shapes can be tested.
const emailtable = computed(() => emailTableFrom(reportData.value))
const activeFilterFields = computed(() =>
currentReport.value ? (reportFilterFields[currentReport.value.id] || []) : []
)
@@ -391,12 +409,6 @@ async function loadReports() {
}
}
function titleCase(text) {
return String(text || '')
.split(/[\s_-]+/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
// Filter by name/description/category, then group by category in the fixed order.
const groupedReports = computed(() => {