Add tablet camera scanning for employee badge QR on kiosk.
Deploy Ladill Frontdesk / deploy (push) Successful in 36s

Uses html5-qrcode with rear camera preview on Scan badge, keeps external scanner fallback, and still requires PIN after a successful scan.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-28 21:40:54 +00:00
co-authored by Cursor
parent cdd675d247
commit 7a9871f43f
4 changed files with 143 additions and 6 deletions
+117 -1
View File
@@ -9,6 +9,10 @@ export function registerKioskFlow(Alpine) {
stepOutReasons: config.stepOutReasons || [],
staffIdentifyMode: 'code',
staffForm: { employee_code: '', qr_token: '', pin: '' },
staffQrScanner: null,
staffQrScanning: false,
staffQrScanned: false,
staffQrError: '',
staffEmployee: null,
staffMessage: '',
staffBranchWarning: '',
@@ -175,7 +179,10 @@ export function registerKioskFlow(Alpine) {
}
},
goBack() {
async goBack() {
if (this.step === 'staff_identify') {
await this.stopStaffQrScanner(true);
}
if (this.step === 'type') {
this.step = this.employeeKioskEnabled ? 'choose' : 'welcome';
this.typeIndex = 0;
@@ -204,6 +211,7 @@ export function registerKioskFlow(Alpine) {
},
reset() {
this.stopStaffQrScanner(true);
this.stopCamera();
this.step = 'welcome';
this.formStep = 1;
@@ -215,6 +223,8 @@ export function registerKioskFlow(Alpine) {
this.staffBranchWarning = '';
this.staffIdentifyMode = 'code';
this.staffForm = { employee_code: '', qr_token: '', pin: '' };
this.staffQrScanned = false;
this.staffQrError = '';
this.staffStepOut = { step_out_reason: 'lunch', destination: '', notes: '', expected_return_at: '' };
this.form = {
full_name: '', company: '', phone: '', email: '', host_id: '',
@@ -239,13 +249,118 @@ export function registerKioskFlow(Alpine) {
},
startStaffFlow() {
this.stopStaffQrScanner(true);
this.step = 'staff_identify';
this.errorMessage = '';
this.staffIdentifyMode = 'code';
this.staffForm = { employee_code: '', qr_token: '', pin: '' };
this.staffQrScanned = false;
this.staffQrError = '';
this.resetTimer();
},
async setStaffIdentifyMode(mode) {
if (this.staffIdentifyMode === mode) {
return;
}
await this.stopStaffQrScanner(true);
this.staffIdentifyMode = mode;
this.staffQrError = '';
if (mode === 'badge') {
await this.$nextTick();
await this.startStaffQrScanner();
}
},
parseEmployeeQr(text) {
const trimmed = String(text || '').trim();
if (trimmed === '') {
return null;
}
const urlMatch = trimmed.match(/\/eq\/([A-Za-z0-9]+)/);
if (urlMatch) {
return urlMatch[1];
}
if (/^[A-Za-z0-9]{16,64}$/.test(trimmed)) {
return trimmed;
}
return null;
},
async startStaffQrScanner() {
if (this.staffQrScanning || this.staffQrScanned || this.staffIdentifyMode !== 'badge') {
return;
}
this.staffQrError = '';
try {
const { Html5Qrcode } = await import('html5-qrcode');
await this.$nextTick();
if (! document.getElementById('staff-qr-reader')) {
return;
}
if (this.staffQrScanner) {
await this.stopStaffQrScanner(false);
}
this.staffQrScanner = new Html5Qrcode('staff-qr-reader');
await this.staffQrScanner.start(
{ facingMode: 'environment' },
{ fps: 10, qrbox: { width: 260, height: 260 } },
(decodedText) => this.handleStaffBadgeScan(decodedText),
() => {},
);
this.staffQrScanning = true;
} catch (e) {
this.staffQrError = 'Camera unavailable. Use an external scanner or switch to employee code.';
console.error('Staff QR scanner failed', e);
}
},
async handleStaffBadgeScan(decodedText) {
if (! this.parseEmployeeQr(decodedText)) {
return;
}
this.staffForm.qr_token = decodedText.trim();
this.staffQrScanned = true;
this.errorMessage = '';
await this.stopStaffQrScanner(false);
this.resetTimer();
},
async rescanStaffBadge() {
this.staffQrScanned = false;
this.staffForm.qr_token = '';
this.staffQrError = '';
await this.$nextTick();
await this.startStaffQrScanner();
},
async stopStaffQrScanner(clearScanned = false) {
if (this.staffQrScanner) {
try {
if (this.staffQrScanning) {
await this.staffQrScanner.stop();
}
await this.staffQrScanner.clear();
} catch (e) {
// Scanner may already be stopped when leaving the step.
}
this.staffQrScanner = null;
this.staffQrScanning = false;
}
if (clearScanned) {
this.staffQrScanned = false;
this.staffForm.qr_token = '';
this.staffQrError = '';
}
},
staffCredentials() {
const body = { pin: this.staffForm.pin };
if (this.staffIdentifyMode === 'badge' && this.staffForm.qr_token.trim()) {
@@ -298,6 +413,7 @@ export function registerKioskFlow(Alpine) {
return;
}
try {
await this.stopStaffQrScanner(false);
const data = await this.staffPost('identify');
if (!data) return;
this.staffEmployee = data.employee;