—';
}
$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 ) ) );
?>
[seatt_checkin_portal]
[seatt_confirmation_portal]
✓ 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'
);
?>