—'; } $html = ''; foreach ( $seats as $s ) { $label = seatt_seat_label( $s->row_name, $s->seat_label ); $html .= '' . esc_html( $label ) . ''; } return $html; } public function render_page() { $events = get_posts( array( 'post_type' => 'seatt_event', 'numberposts' => -1 ) ); // 0 = "nothing picked yet" — matches Chart Builder's "select an // event first" screen, so this page doesn't silently default to // whichever event happens to be most recently created. $event_id = isset( $_GET['event_id'] ) ? intval( $_GET['event_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only: chooses what to display self::$chip_event = max( 0, (int) $event_id ); if ( ! $event_id ) { ?>

! empty( $b->checked_in_at ) ) ); ?>

post_title : '' ); ?>

[seatt_checkin_portal]


[seatt_confirmation_portal]

seats, 'seat_label' ); // Same fix as the Bookings page's identical search box — // attendee names (per-seat) weren't included here either, // so searching for one silently never matched anything. $attendee_names = wp_list_pluck( $b->seats, 'attendee_name' ); $holder_names = array_map( function ( $h ) { return $h->name . ' ' . $h->email; }, (array) ( $b->holders ?? array() ) ); $search_blob = strtolower( $b->buyer_name . ' ' . implode( ' ', $seat_labels ) . ' ' . implode( ' ', $attendee_names ) . ' ' . implode( ' ', $holder_names ) ); ?>
seat_chips( $b->seats ) ); ?> buyer_name ); ?> array( 'Paid', 'esm-badge-paid' ), 'pending_payment' => array( 'Pending', 'esm-badge-pending' ), 'cancelled' => array( 'Cancelled', 'esm-badge-cancelled' ) ); $info = $status_map[ $b->status ] ?? array( ucfirst( $b->status ), 'esm-badge-pending' ); ?> checked_in_at ) : ?> ✓ checked_in_at ) ); ?> checked_in_by ) : ?> checked_in_by ) ) ); ?> — checked_in_at && 'cancelled' !== $b->status ) : ?>
✓ Just now\'; btn.remove(); }).catch(function () { alert(\'Could not check in — please try again.\'); btn.disabled = false; }); }); })(); ' ); ?>
\' + \'
✕
\' + \'
\' + message + \'
\' + \'\'; statusEl.textContent = ' . ( wp_json_encode( __( 'Point the guest\'s QR code at the camera…', 'seathive' ) ) ) . '; scheduleResultClear(); } function startCamera() { if (clearResultTimer) clearTimeout(clearResultTimer); resultEl.innerHTML = \'\'; statusEl.textContent = ' . ( wp_json_encode( __( 'Point the camera at the guest\'s QR code…', 'seathive' ) ) ) . '; navigator.mediaDevices.getUserMedia({ video: { facingMode: \'environment\' } }) .then(function (s) { stream = s; video.srcObject = s; video.play(); scanning = true; requestAnimationFrame(scanLoop); }) .catch(function () { statusEl.textContent = ' . ( wp_json_encode( __( 'Could not access the camera — check your browser\'s camera permission for this site.', 'seathive' ) ) ) . '; }); } function stopCamera() { scanning = false; if (clearResultTimer) clearTimeout(clearResultTimer); if (stream) { stream.getTracks().forEach(function (t) { t.stop(); }); stream = null; } } function scanLoop() { if (!scanning) return; if (video.readyState === video.HAVE_ENOUGH_DATA && window.jsQR) { canvas.width = video.videoWidth; canvas.height = video.videoHeight; var ctx = canvas.getContext(\'2d\'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); var code = jsQR(imageData.data, imageData.width, imageData.height); if (code && code.data) { handleScannedUrl(code.data); } } requestAnimationFrame(scanLoop); } function handleScannedUrl(text) { var url, id, token, tid = \'\'; try { url = new URL(text); } catch (e) { url = null; } if (url) { id = (url.searchParams.get(\'seatt_checkin_id\') || url.searchParams.get(\'esm_checkin_id\')); token = (url.searchParams.get(\'seatt_checkin_token\') || url.searchParams.get(\'esm_checkin_token\')); tid = (url.searchParams.get(\'seatt_t\') || url.searchParams.get(\'esm_t\')) || \'\'; // one ticket\'s own QR } // Not one of our booking QR codes at all — debounce on the // raw scanned text so this doesn\'t buzz on every single // video frame while the same wrong code sits in view. if (!url || !id || !token) { var invalidKey = \'invalid:\' + text; var nowInvalid = Date.now(); if (invalidKey === lastScannedKey && nowInvalid - lastScannedAt < 3000) return; lastScannedKey = invalidKey; lastScannedAt = nowInvalid; showInvalidQr(' . ( wp_json_encode( __( 'Not a valid booking QR code', 'seathive' ) ) ) . '); return; } var key = id + \':\' + token + \':\' + tid; var now = Date.now(); if (key === lastScannedKey && now - lastScannedAt < 4000) return; // debounce repeat frames of the same code lastScannedKey = key; lastScannedAt = now; statusEl.textContent = ' . ( wp_json_encode( __( 'Checking in…', 'seathive' ) ) ) . '; ESMOffline.fetch(esmRestUrl + \'/bookings/\' + id + \'/checkin\', { method: \'POST\', headers: { \'Content-Type\': \'application/json\', \'X-WP-Nonce\': esmNonce }, body: JSON.stringify({ token: token, ticket_id: tid ? Number(tid) : 0 }), }).then(function (res) { return res.json().then(function (data) { return { ok: res.ok, data: data }; }); }) .then(function (r) { if (r.ok) { var seatDetails = r.data.seat_details || []; var seatsHtml = seatDetails.map(function (s) { return \'
\' + \'\' + s.label + \'\' + \'\' + s.category + \'\' + \'\' + (s.attendee_name || \'—\') + \'\' + \'
\'; }).join(\'\'); if (r.data.already_checked_in) { // Re-scanning a ticket that was already checked // in earlier — a shorter double-buzz and an // amber card, clearly different from a fresh // check-in, so staff don\'t mistake it for one. if (navigator.vibrate) navigator.vibrate([60, 70, 60]); playErrorSound(); resultEl.innerHTML = \'
\' + \'
↻
\' + \'
\' + (r.data.buyer_name || \'Guest\') + \'
\' + (seatsHtml ? \'
\' + seatsHtml + \'
\' : \'\') + \'
Already checked in
\' + \'
\'; } else { if (navigator.vibrate) navigator.vibrate(90); playSuccessSound(); resultEl.innerHTML = \'
\' + \'
✓
\' + \'
\' + (r.data.buyer_name || \'Guest\') + \'
\' + (seatsHtml ? \'
\' + seatsHtml + \'
\' : \'\') + \'
Checked in
\' + \'
\'; var row = document.querySelector(\'.esm-bookings-table tr[data-booking-id="\' + id + \'"]\'); if (row) { var cell = row.querySelector(\'.esm-arrival-cell\'); if (cell) cell.innerHTML = \'✓ Just now\'; var btn = row.querySelector(\'.esm-mark-arrived-btn\'); if (btn) btn.remove(); } } statusEl.textContent = ' . ( wp_json_encode( __( 'Ready for the next guest…', 'seathive' ) ) ) . '; scheduleResultClear(); } else { showInvalidQr(r.data.message || ' . ( wp_json_encode( __( 'Invalid QR code.', 'seathive' ) ) ) . '); } }).catch(function () { showInvalidQr(' . ( wp_json_encode( __( 'Network error — please try again.', 'seathive' ) ) ) . '); }); } openBtn.addEventListener(\'click\', function () { ensureAudioContext(); // unlock audio here, inside a real click — not later, inside an async callback modal.style.display = \'flex\'; startCamera(); }); closeBtn.addEventListener(\'click\', function () { modal.style.display = \'none\'; stopCamera(); }); })(); ', 'seatt-door' ); ?>