export function registerKioskFlow(Alpine) { Alpine.data('kioskFlow', () => { const config = window.__KIOSK__ || {}; return { step: 'welcome', formStep: 1, typeIndex: 0, typeTouchStart: null, loading: false, errorMessage: '', result: null, timer: null, visitorTypes: config.visitorTypes || [], typeConfigs: config.typeConfigs || {}, resetSeconds: config.resetSeconds || 120, checkInUrl: config.checkInUrl || '', signing: { active: false, field: null }, canvases: {}, cameraActive: false, cameraStream: null, form: { full_name: '', company: '', phone: '', email: '', host_id: '', visitor_type: 'visitor', purpose: '', photo_data: '', policies_accepted: false, type_fields: {}, }, hasTypeFields() { return (this.typeConfigs[this.form.visitor_type]?.fields || []).length > 0; }, totalFormSteps() { return this.hasTypeFields() ? 4 : 3; }, finishStep() { return this.totalFormSteps(); }, isLastFormStep() { return this.formStep >= this.finishStep(); }, currentStepKind() { if (this.formStep === 1) return 'identity'; if (this.formStep === 2) return 'visit'; if (this.hasTypeFields() && this.formStep === 3) return 'typeFields'; return 'finish'; }, currentFormStepLabel() { const labels = { identity: 'About you', visit: 'Your visit', typeFields: 'Additional details', finish: 'Photo & finish', }; return labels[this.currentStepKind()] || ''; }, selectType(type) { this.form.visitor_type = type; this.form.type_fields = {}; this.formStep = 1; this.errorMessage = ''; this.step = 'details'; this.resetTimer(); }, nextType() { if (this.typeIndex < this.visitorTypes.length - 1) { this.typeIndex++; this.resetTimer(); } }, prevType() { if (this.typeIndex > 0) { this.typeIndex--; this.resetTimer(); } }, onTypeTouchStart(e) { this.typeTouchStart = e.touches[0].clientX; }, onTypeTouchEnd(e) { if (this.typeTouchStart === null) return; const delta = e.changedTouches[0].clientX - this.typeTouchStart; if (delta > 60) this.prevType(); else if (delta < -60) this.nextType(); this.typeTouchStart = null; }, showError(message) { this.errorMessage = message; }, validateCurrentStep() { this.errorMessage = ''; const kind = this.currentStepKind(); if (kind === 'identity') { if (!String(this.form.full_name || '').trim()) { this.showError('Please enter your full name.'); return false; } } if (kind === 'typeFields') { const fields = this.typeConfigs[this.form.visitor_type]?.fields || []; for (const field of fields) { if (!field.required) continue; const val = this.form.type_fields[field.name]; if (field.type === 'checkbox' && !val) { this.showError(`Please confirm: ${field.label}`); return false; } if (field.type !== 'checkbox' && (!val || String(val).trim() === '')) { this.showError(`Please complete: ${field.label}`); return false; } } } if (kind === 'finish' && !this.form.policies_accepted) { this.showError('Please accept the visitor policies.'); return false; } return true; }, handleContinue() { if (this.isLastFormStep()) { this.submitCheckIn(); return; } this.nextFormStep(); }, nextFormStep() { if (!this.validateCurrentStep()) return; if (this.formStep < this.totalFormSteps()) { this.formStep++; this.resetTimer(); } }, prevFormStep() { this.errorMessage = ''; if (this.formStep > 1) { this.formStep--; this.resetTimer(); } else { this.step = 'type'; this.resetTimer(); } }, goBack() { if (this.step === 'type') { this.step = 'welcome'; this.typeIndex = 0; } else if (this.step === 'details') { this.prevFormStep(); } this.resetTimer(); }, resetTimer() { clearTimeout(this.timer); this.timer = setTimeout(() => this.reset(), this.resetSeconds * 1000); }, startTimer() { this.resetTimer(); }, reset() { this.stopCamera(); this.step = 'welcome'; this.formStep = 1; this.typeIndex = 0; this.errorMessage = ''; this.result = null; this.form = { full_name: '', company: '', phone: '', email: '', host_id: '', visitor_type: 'visitor', purpose: '', photo_data: '', policies_accepted: false, type_fields: {}, }; this.resetTimer(); }, async startCamera() { try { this.cameraStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } }); this.$refs.camera.srcObject = this.cameraStream; this.cameraActive = true; } catch (e) { this.showError('Camera unavailable on this device.'); } }, capturePhoto() { const video = this.$refs.camera; if (!video?.videoWidth) return; const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0); this.form.photo_data = canvas.toDataURL('image/jpeg', 0.85); this.stopCamera(); }, clearPhoto() { this.form.photo_data = ''; }, stopCamera() { this.cameraStream?.getTracks().forEach(track => track.stop()); this.cameraStream = null; this.cameraActive = false; }, startSign(e, field) { this.signing = { active: true, field }; this.canvases[field] = e.target; const ctx = e.target.getContext('2d'); ctx.strokeStyle = '#0f172a'; ctx.lineWidth = 2; ctx.beginPath(); const p = this.pointer(e); ctx.moveTo(p.x, p.y); }, drawSign(e, field) { if (!this.signing.active || this.signing.field !== field) return; const ctx = this.canvases[field].getContext('2d'); const p = this.pointer(e); ctx.lineTo(p.x, p.y); ctx.stroke(); }, endSign(field) { if (this.signing.field === field) { this.form.type_fields[field] = this.canvases[field].toDataURL('image/png'); } this.signing = { active: false, field: null }; }, clearSign(field) { const canvas = this.canvases[field]; if (!canvas) return; canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); this.form.type_fields[field] = ''; }, pointer(e) { const canvas = e.target; const rect = canvas.getBoundingClientRect(); const touch = e.touches?.[0]; const clientX = touch ? touch.clientX : e.clientX; const clientY = touch ? touch.clientY : e.clientY; return { x: clientX - rect.left, y: clientY - rect.top }; }, async submitCheckIn() { if (!this.validateCurrentStep()) return; this.loading = true; this.errorMessage = ''; try { const payload = { ...this.form, photo_data: this.form.photo_data || undefined, policies_accepted: this.form.policies_accepted ? 1 : 0, }; const res = await fetch(this.checkInUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json', }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error('Check-in failed'); const data = await res.json(); this.result = data.visit; this.step = 'done'; this.resetTimer(); } catch (e) { this.showError('Check-in failed. Please ask reception for help.'); } finally { this.loading = false; } }, }; }); }