Add queue kiosk and display devices for Care Queue management.
Deploy Ladill Care / deploy (push) Successful in 39s

Register walk-up kiosks and waiting-room display devices under Devices, with public kiosk ticket issue and linked TV boards surfaced in sidebar and Queue shortcuts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-20 10:59:02 +00:00
co-authored by Cursor
parent b2cebe2908
commit a3839da869
18 changed files with 1103 additions and 60 deletions
+2
View File
@@ -1,11 +1,13 @@
import Alpine from 'alpinejs';
import { registerLadillClipboard } from './ladill-clipboard';
import { registerCareDisplay } from './care-display';
import { registerCareKiosk } from './care-kiosk';
import collapse from '@alpinejs/collapse';
Alpine.plugin(collapse);
registerLadillClipboard(Alpine);
registerCareDisplay(Alpine);
registerCareKiosk(Alpine);
// In-app notification bell + dropdown.
Alpine.data('notificationDropdown', (config = {}) => ({
+162
View File
@@ -0,0 +1,162 @@
export function registerCareKiosk(Alpine) {
Alpine.data('careKioskFlow', (config = {}) => ({
step: 'welcome',
queueId: null,
customerName: '',
customerPhone: '',
ticket: null,
error: null,
loading: false,
idleTimer: null,
countdownInterval: null,
resetCountdown: 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 ?? null,
startTimer() {
this.resetIdleTimer();
},
resetIdleTimer() {
clearTimeout(this.idleTimer);
clearInterval(this.countdownInterval);
this.resetCountdown = this.resetSeconds;
this.countdownInterval = setInterval(() => {
this.resetCountdown -= 1;
if (this.resetCountdown <= 0) {
clearInterval(this.countdownInterval);
this.countdownInterval = null;
}
}, 1000);
this.idleTimer = setTimeout(() => this.reset(), this.resetSeconds * 1000);
},
clearIdleTimer() {
clearTimeout(this.idleTimer);
clearInterval(this.countdownInterval);
this.idleTimer = null;
this.countdownInterval = null;
this.resetCountdown = null;
},
beginKiosk() {
this.resetIdleTimer();
if (this.queues.length === 0) {
this.step = 'select';
return;
}
if (this.queues.length === 1) {
this.queueId = this.queues[0].id;
if (! this.collectName && ! this.collectPhone) {
this.issue();
return;
}
}
this.step = 'select';
},
goBack() {
this.resetIdleTimer();
this.error = null;
if (this.step === 'details') {
this.step = 'select';
return;
}
if (this.step === 'select') {
this.step = 'welcome';
this.queueId = null;
}
},
selectQueue(id) {
this.queueId = id;
this.error = null;
this.resetIdleTimer();
if (! this.collectName && ! this.collectPhone) {
this.issue();
return;
}
this.step = 'details';
},
async issue() {
if (! this.queueId || this.loading) {
return;
}
this.loading = true;
this.error = null;
this.resetIdleTimer();
try {
const res = await fetch(this.issueUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': this.csrf,
},
body: JSON.stringify({
queue_id: this.queueId,
customer_name: this.customerName || null,
customer_phone: this.customerPhone || null,
}),
});
const data = await res.json();
if (! res.ok) {
throw new Error(data.message || 'Could not issue ticket');
}
this.ticket = data.data;
this.step = 'ticket';
this.resetIdleTimer();
} catch (e) {
this.error = e.message || 'Something went wrong';
} finally {
this.loading = false;
}
},
reset() {
this.clearIdleTimer();
this.step = 'welcome';
this.queueId = null;
this.customerName = '';
this.customerPhone = '';
this.ticket = null;
this.error = null;
this.loading = false;
this.startTimer();
},
formatWait(seconds) {
if (! seconds) {
return null;
}
const minutes = Math.ceil(seconds / 60);
return minutes <= 1 ? 'About 1 minute' : `About ${minutes} minutes`;
},
}));
}