Plugin reports render; USB check-in/out audit report; kiosk drive lists

This commit is contained in:
2026-08-27 10:20:59 -04:00
parent 376298dc02
commit 60c340b07f
7 changed files with 609 additions and 59 deletions

View File

@@ -42,8 +42,9 @@
<section v-else-if="step === 'pick'" class="kiosk-step kiosk-wide">
<p class="kiosk-greeting">{{ session.name }}</p>
<div v-if="session.yours.length" class="drive-group">
<div v-if="session.yours.length" class="drive-group drive-group-yours">
<h2>Your drives</h2>
<div class="drive-list">
<button v-for="drive in session.yours" :key="drive.device_id"
class="drive-row" :disabled="busy"
@click="act(drive, 'checkin')">
@@ -53,6 +54,7 @@
</span>
<span class="drive-action return">Return</span>
</button>
</div>
</div>
<div class="drive-group">
@@ -60,6 +62,7 @@
<p v-if="!session.available.length" class="kiosk-hint">
No drives are free right now.
</p>
<div v-else class="drive-list">
<button v-for="drive in session.available" :key="drive.device_id"
class="drive-row" :disabled="busy"
@click="act(drive, 'checkout')">
@@ -70,6 +73,7 @@
</span>
<span class="drive-action take">Take</span>
</button>
</div>
</div>
<!-- Other people's drives are behind a tap, not on the main screen: a
@@ -87,6 +91,7 @@
<p class="kiosk-hint">
Returning one puts it back and records that YOU handed it in.
</p>
<div class="drive-list">
<button v-for="drive in others" :key="drive.device_id"
class="drive-row" :disabled="busy"
@click="act(drive, 'checkin', true)">
@@ -96,6 +101,7 @@
</span>
<span class="drive-action return">Return</span>
</button>
</div>
<button class="btn btn-secondary act-button" @click="step = 'pick'">
Back
</button>
@@ -149,6 +155,14 @@ function focusEntry (event) {
nextTick(() => entryInput.value?.focus())
}
// Drive ids sort as TEXT everywhere else, which puts -10 before -3 and reads as
// a jumble on a wall panel. Compare the numeric runs numerically; falls back to
// plain text for ids that carry no digits.
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
function byDeviceId (a, b) {
return collator.compare(a.device_id || '', b.device_id || '')
}
function messageFrom (err, fallback) {
return err.response?.data?.data?.error?.message || fallback
}
@@ -160,7 +174,10 @@ async function startSession (value) {
busy.value = true
try {
const response = await api.post('/usb/kiosk/session', { badge: scanned })
session.value = response.data.data
const data = response.data.data
data.yours = (data.yours || []).sort(byDeviceId)
data.available = (data.available || []).sort(byDeviceId)
session.value = data
badge.value = scanned
manualBadge.value = ''
step.value = 'pick'
@@ -177,7 +194,7 @@ async function loadOthers () {
busy.value = true
try {
const response = await api.post('/usb/kiosk/others', { badge: badge.value })
others.value = response.data.data || []
others.value = (response.data.data || []).sort(byDeviceId)
step.value = 'others'
} catch (othersError) {
error.value = messageFrom(othersError, 'Could not load those drives')
@@ -211,7 +228,10 @@ async function act (drive, action, forsomeoneelse = false) {
async function refresh () {
try {
const response = await api.post('/usb/kiosk/session', { badge: badge.value })
session.value = response.data.data
const data = response.data.data
data.yours = (data.yours || []).sort(byDeviceId)
data.available = (data.available || []).sort(byDeviceId)
session.value = data
step.value = 'pick'
} catch (refreshError) {
reset()
@@ -232,34 +252,73 @@ function reset () {
</script>
<style scoped>
/* Touch-sized throughout: a wall panel, often operated with gloves on. */
/* Touch-sized throughout: a wall panel, often operated with gloves on.
Laid out to match the parts kiosk beside it - two kiosks on the same floor
that sit differently on their screens read as two different products. */
.usb-kiosk {
min-height: 100vh;
padding: 1.5rem;
padding: 2rem;
background: var(--bg);
color: var(--text);
display: flex;
flex-direction: column;
gap: 1.25rem;
align-items: center;
/* Centered as one block on a wall-mounted display rather than hugging the
top. `safe` so a short screen - or a long drive list - scrolls from the top
instead of clipping the header off the edge, which plain `center` does with
no way to scroll back up. */
justify-content: center;
justify-content: safe center;
gap: 1.5rem;
}
.kiosk-header {
width: 100%;
max-width: 40rem;
display: flex;
justify-content: center;
align-items: center;
justify-content: space-between;
gap: 1rem;
/* Title centered over the panel; Done is pulled out of the flow so it keeps
its top-right corner instead of pushing the title off-center on the steps
that show it. */
position: relative;
}
.kiosk-header .btn {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.kiosk-header h1 { margin: 0; font-size: 2rem; }
.kiosk-step {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
max-width: 34rem;
gap: 1.25rem;
max-width: 40rem;
width: 100%;
margin: 0 auto;
}
.kiosk-wide { max-width: 46rem; align-items: stretch; }
/* The picker holds lists, so its rows stretch - but it keeps the same width as
every other step so the panel does not jump when the step changes.
It is also height-bounded: the lists absorb whatever space is left over and
scroll inside themselves, so the PAGE never scrolls and the heading saying
whose drives these are stays on screen. A fixed max-height cannot do this -
it either clips with room to spare or overflows once both groups are
present, which is exactly what it did. */
.kiosk-wide {
align-items: stretch;
max-height: calc(100vh - 9rem);
min-height: 0;
}
.kiosk-wide .drive-group {
display: flex;
flex-direction: column;
min-height: 0;
}
/* Your own drives size to their content - usually one or two - and the
available list absorbs what is left. Sharing the space evenly clipped a
single row of "Your drives" in half while the other list had room. */
.kiosk-wide .drive-group-yours { flex: 0 0 auto; }
.kiosk-wide .drive-group-yours .drive-list { max-height: 30vh; }
.kiosk-prompt { font-size: 1.5rem; text-align: center; margin: 0; }
.kiosk-greeting { font-size: 1.5rem; margin: 0; text-align: center; }
.kiosk-hint { color: var(--textmuted); text-align: center; margin: 0; }
@@ -276,19 +335,30 @@ function reset () {
.drive-group { width: 100%; }
.drive-group h2 { font-size: 1.2rem; margin: 0 0 0.5rem; }
/* Two columns once there is room, so a cabinet of a dozen drives is one glance
rather than a scroll. The list itself scrolls when it has to - the PAGE must
not, or the person loses the heading telling them whose drives these are. */
.drive-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
gap: 0.6rem;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding-right: 0.2rem;
}
.drive-row {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.2rem;
margin-bottom: 0.6rem;
padding: 0.9rem 1rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface, transparent);
border-radius: 0.75rem;
background: var(--bg-card);
color: var(--text);
font-size: 1.2rem;
font-size: 1.05rem;
text-align: left;
cursor: pointer;
}
@@ -307,21 +377,39 @@ function reset () {
.others-button { width: 100%; padding: 0.9rem; margin-top: 0.5rem; }
.entry-panel {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.9rem;
gap: 1rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 1rem;
padding: 1.5rem 2rem;
}
.entry-input {
width: 100%;
padding: 0.8rem 1rem;
font-size: 1.6rem;
width: 16.9rem;
box-sizing: border-box;
font-size: 2.4rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
text-align: center;
padding: 0.5rem 1rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface, transparent);
border-radius: 0.75rem;
background: var(--bg);
color: var(--text);
outline: none;
}
.entry-input:focus { border-color: var(--primary); }
.entry-input::placeholder {
color: var(--text-light);
font-weight: 400;
font-size: 1.4rem;
}
.act-button {
font-size: 1.4rem;
padding: 0.85rem 0;
width: 16.9rem;
border-radius: 0.75rem;
}
.act-button { width: 100%; padding: 1rem; font-size: 1.25rem; }
</style>