Plugin reports render; USB check-in/out audit report; kiosk drive lists
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user