Add tablet camera scanning for employee badge QR on kiosk.
Deploy Ladill Frontdesk / deploy (push) Successful in 36s
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:
+117
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user