Add self-service kiosk for queue ticket issuance.
Deploy Ladill Queue / deploy (push) Successful in 36s

Customers can pick a service, optionally enter details, and receive a ticket on a branded touchscreen flow with admin-configurable queues and auto-reset.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-29 23:25:51 +00:00
co-authored by Cursor
parent d982e28191
commit 492eec9cda
9 changed files with 683 additions and 53 deletions
+71 -3
View File
@@ -46,18 +46,43 @@ export function registerKioskFlow(Alpine) {
ticket: null,
error: null,
loading: false,
resetCountdown: null,
resetTimer: null,
countdownTimer: null,
issueUrl: config.issueUrl,
csrf: config.csrf,
queues: config.queues ?? [],
collectName: config.collectName ?? false,
collectPhone: config.collectPhone ?? false,
resetSeconds: config.resetSeconds ?? 15,
welcomeMessage: config.welcomeMessage ?? 'Tap a service below to get your ticket',
init() {
if (this.queues.length === 1) {
this.queueId = this.queues[0].id;
}
},
selectQueue(id) {
this.queueId = id;
this.error = null;
if (! this.collectName && ! this.collectPhone) {
this.issue();
return;
}
this.step = 'details';
},
async issue() {
if (! this.queueId) return;
if (! this.queueId || this.loading) {
return;
}
this.loading = true;
this.error = null;
try {
const res = await fetch(this.issueUrl, {
method: 'POST',
@@ -73,9 +98,14 @@ export function registerKioskFlow(Alpine) {
}),
});
const data = await res.json();
if (! res.ok) throw new Error(data.message || 'Could not issue ticket');
if (! res.ok) {
throw new Error(data.message || 'Could not issue ticket');
}
this.ticket = data.data;
this.step = 'ticket';
this.scheduleReset();
} catch (e) {
this.error = e.message || 'Something went wrong';
} finally {
@@ -83,13 +113,51 @@ export function registerKioskFlow(Alpine) {
}
},
scheduleReset() {
this.clearResetTimers();
this.resetCountdown = this.resetSeconds;
this.countdownTimer = setInterval(() => {
this.resetCountdown -= 1;
if (this.resetCountdown <= 0) {
this.clearResetTimers();
}
}, 1000);
this.resetTimer = setTimeout(() => this.reset(), this.resetSeconds * 1000);
},
clearResetTimers() {
if (this.resetTimer) {
clearTimeout(this.resetTimer);
this.resetTimer = null;
}
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
this.resetCountdown = null;
},
reset() {
this.clearResetTimers();
this.step = 'select';
this.queueId = null;
this.queueId = this.queues.length === 1 ? this.queues[0].id : null;
this.customerName = '';
this.customerPhone = '';
this.ticket = null;
this.error = null;
this.loading = false;
},
formatWait(seconds) {
if (! seconds) {
return null;
}
const minutes = Math.ceil(seconds / 60);
return minutes <= 1 ? 'About 1 minute' : `About ${minutes} minutes`;
},
}));