{"id":272,"date":"2026-02-25T09:24:05","date_gmt":"2026-02-25T09:24:05","guid":{"rendered":"https:\/\/macmonir.net\/test\/?page_id=272"},"modified":"2026-02-25T09:39:26","modified_gmt":"2026-02-25T09:39:26","slug":"scanner","status":"publish","type":"page","link":"https:\/\/macmonir.net\/test\/scanner\/","title":{"rendered":"scanner"},"content":{"rendered":"        <!-- PWA Meta Headers -->\n        <link rel=\"manifest\" href=\"https:\/\/macmonir.net\/test\/?les_action=manifest\">\n                    <meta http-equiv=\"Permissions-Policy\" content=\"camera=(self)\">\n                <meta name=\"theme-color\" content=\"#6a40a5\">\n        <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n        <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">\n        <meta name=\"apple-mobile-web-app-title\" content=\"LES Scanner\">\n\n        <div class=\"les-frontend-scanner-wrap\">\n            \n            \n            <!-- Offline Control Bar -->\n            <div class=\"les-offline-bar\" style=\"background:#fff; border:1px solid #ccd0d4; border-radius:8px; padding:12px 16px; margin-bottom:20px; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; box-shadow:0 1px 3px rgba(0,0,0,0.05);\">\n                <div style=\"display:flex; align-items:center; gap:10px; flex-wrap:wrap;\">\n                    <span class=\"dashicons dashicons-cloud\" style=\"font-size:20px; color:#6a40a5; margin-right:2px;\"><\/span>\n                    <strong style=\"color:#1d2327; font-size:13px;\">Offline Event Data:<\/strong>\n                    \n                    <select id=\"les_offline_event_select\" style=\"font-size:12px; border-radius:4px; padding:4px 8px; border:1px solid #8c8f94;\">\n                        <option value=\"0\">-- All Active Events --<\/option>\n                                                    <option value=\"225\">Gala Dinner Admin Demo<\/option>\n                                                    <option value=\"186\">HAVASI Symphonic Arena Show 2026<\/option>\n                                                    <option value=\"220\">Large theatre demo<\/option>\n                                                    <option value=\"364\">Nyimas Gandasari<\/option>\n                                                    <option value=\"188\">Small theatre<\/option>\n                                                    <option value=\"223\">Trade show admin demo<\/option>\n                                                    <option value=\"339\">WaitList Event<\/option>\n                                                    <option value=\"207\">yusman event<\/option>\n                                            <\/select>\n                    \n                    <button type=\"button\" class=\"button button-secondary\" id=\"les_prefetch_btn\" style=\"display:inline-flex; align-items:center; gap:4px; font-size:12px; border-color:#6a40a5; color:#6a40a5;\">\n                        <span class=\"dashicons dashicons-download\" style=\"font-size:14px; width:14px; height:14px; line-height:14px;\"><\/span>\n                        Download \/ Refresh Offline Guest List                    <\/button>\n                    <span id=\"les_last_download_info\" style=\"font-size:11px; color:#666;\"><\/span>\n                <\/div>\n\n                <div style=\"display:flex; align-items:center; gap:10px;\">\n                    <div id=\"les_connection_status\" class=\"les-status-badge online\" style=\"font-size:11px; font-weight:700; padding:4px 10px; border-radius:12px; background:#e6f7f0; color:#1a9e5f; display:inline-flex; align-items:center; gap:5px;\">\n                        <span class=\"status-dot\" style=\"display:inline-block; width:8px; height:8px; border-radius:50%; background:#1a9e5f;\"><\/span>\n                        <span class=\"status-text\">Online Mode<\/span>\n                    <\/div>\n\n                    <div id=\"les_checkin_count\" style=\"font-size:11px; font-weight:700; padding:4px 10px; border-radius:12px; background:#eef4ff; color:#2271b1; display:inline-flex; align-items:center; gap:5px;\">\n                        <span class=\"dashicons dashicons-yes-alt\" style=\"font-size:14px; width:14px; height:14px;\"><\/span>\n                        <span>Checked in: <span id=\"les_checked_in_count_value\">0<\/span><\/span>\n                    <\/div>\n\n                    <button type=\"button\" class=\"button button-primary\" id=\"les_sync_now_btn\" style=\"display:none; align-items:center; gap:4px; font-size:12px; background:#107c41; border-color:#107c41;\">\n                        <span class=\"dashicons dashicons-update\" style=\"font-size:14px; width:14px; height:14px; line-height:14px;\"><\/span>\n                        Sync Pending Check-ins (<span id=\"les_pending_count\">0<\/span>)\n                    <\/button>\n                <\/div>\n            <\/div>\n            \n            <div id=\"les-manual-entry-container\" style=\"margin-bottom: 20px; padding: 15px; background: #fff; border: 1px solid #ddd; border-radius: 5px;\">\n                <h3 style=\"margin-top:0; margin-bottom:10px;\">Manual Ticket Entry<\/h3>\n                <div class=\"manual-entry-form\" style=\"display:flex; gap: 10px;\">\n                    <input type=\"text\" id=\"manual_ticket_code_input\" placeholder=\"e.g., LESO-123-45678\" style=\"flex-grow:1; max-width:300px;\">\n                    <button class=\"button button-primary\" id=\"verify_manual_code_btn\">Verify<\/button>\n                <\/div>\n            <\/div>\n\n            <div id=\"les-scanner-container\" style=\"margin-bottom: 20px;\">\n                <div id=\"les-qr-reader\" style=\"width: 100%; max-width: 500px; margin: 0 auto;\"><\/div>\n            <\/div>\n            \n            <div id=\"les-scan-result-container\">\n                <div id=\"les-scan-result\" class=\"notice notice-info\" style=\"padding: 12px; border-left: 4px solid #72aee6; background: #fff; border-radius:4px;\">\n                    <p>Waiting for scan or manual entry...<\/p>\n                <\/div>\n            <\/div>\n        <\/div>\n\n        <script src=\"https:\/\/macmonir.net\/test\/wp-content\/plugins\/live-event-seating\/public\/js\/vendor\/html5-qrcode.min.js\" type=\"text\/javascript\"><\/script>\n\n        <script>\n            const les_checkin_i18n = {\"pleaseEnterCode\":\"Please enter a ticket code.\",\"invalidFormat\":\"Invalid Master Ticket Code format.\",\"verifying\":\"Verifying ticket...\",\"serverError\":\"Error connecting to server.\",\"scanNext\":\"Scan Next Ticket\",\"invalidQR\":\"Invalid QR Code.\",\"scanAgain\":\"Scan Again\",\"waiting\":\"Waiting for scan or manual entry...\",\"downloadingData\":\"Downloading offline guest list...\",\"downloadSuccess\":\"Offline guest list saved successfully!\",\"syncing\":\"Syncing offline check-ins...\",\"syncSuccess\":\"All offline check-ins synced to server!\",\"onlineMode\":\"Online Mode\",\"offlineMode\":\"Offline Mode (Local DB)\",\"alreadyCheckedIn\":\"Ticket Already Used!\",\"ticketNotFound\":\"Invalid Ticket! Code not found in database.\"};\n            \n            const les_scanner_config = {\n                ajax_url: 'https:\/\/macmonir.net\/test\/wp-admin\/admin-ajax.php',\n                nonce: '24b61bb32d',\n                sw_url: 'https:\/\/macmonir.net\/test\/?les_action=service_worker'\n            };\n\n            jQuery(document).ready(function($) {\n                \n                \/\/ \u2500\u2500 PWA SERVICE WORKER REGISTRATION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                if ('serviceWorker' in navigator) {\n                    navigator.serviceWorker.register(les_scanner_config.sw_url, { scope: '\/' })\n                        .then(function(reg) {\n                            console.log('LES PWA Service Worker active:', reg.scope);\n                        })\n                        .catch(function(err) {\n                            console.warn('LES PWA SW Register Error:', err);\n                        });\n                }\n\n                \/\/ \u2500\u2500 INDEXEDDB OFFLINE STORAGE ENGINE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                let db = null;\n                const DB_NAME = 'les_offline_scanner_db';\n                const DB_VERSION = 1;\n\n                function initIndexedDB(callback) {\n                    if (!window.indexedDB) {\n                        console.warn('IndexedDB not supported by this browser.');\n                        if (callback) callback(false);\n                        return;\n                    }\n\n                    const request = window.indexedDB.open(DB_NAME, DB_VERSION);\n\n                    request.onupgradeneeded = function(e) {\n                        const dbInstance = e.target.result;\n                        if (!dbInstance.objectStoreNames.contains('tickets')) {\n                            const ticketStore = dbInstance.createObjectStore('tickets', { keyPath: 'master_ticket_code' });\n                            ticketStore.createIndex('order_id', 'order_id', { unique: false });\n                            ticketStore.createIndex('event_id', 'event_id', { unique: false });\n                        }\n                        if (!dbInstance.objectStoreNames.contains('pending_sync')) {\n                            dbInstance.createObjectStore('pending_sync', { keyPath: 'id', autoIncrement: true });\n                        }\n                        if (!dbInstance.objectStoreNames.contains('meta')) {\n                            dbInstance.createObjectStore('meta', { keyPath: 'key' });\n                        }\n                    };\n\n                    request.onsuccess = function(e) {\n                        db = e.target.result;\n                        updateStatusUI();\n                        if (callback) callback(true);\n                    };\n\n                    request.onerror = function(e) {\n                        console.error('IndexedDB init error:', e);\n                        if (callback) callback(false);\n                    };\n                }\n\n                function updateStatusUI() {\n                    const isOnline = navigator.onLine;\n                    const $status  = $('#les_connection_status');\n                    \n                    if (isOnline) {\n                        $status.css({'background': '#e6f7f0', 'color': '#1a9e5f'})\n                               .find('.status-dot').css('background', '#1a9e5f');\n                        $status.find('.status-text').text(les_checkin_i18n.onlineMode);\n                    } else {\n                        $status.css({'background': '#fef3ea', 'color': '#e07b39'})\n                               .find('.status-dot').css('background', '#e07b39');\n                        $status.find('.status-text').text(les_checkin_i18n.offlineMode);\n                    }\n\n                    updateCheckedInCount();\n\n                    countPendingSyncItems(function(count) {\n                        $('#les_pending_count').text(count);\n                        if (count > 0) {\n                            $('#les_sync_now_btn').css('display', 'inline-flex');\n                            if (isOnline) {\n                                syncPendingCheckins();\n                            }\n                        } else {\n                            $('#les_sync_now_btn').hide();\n                        }\n                    });\n\n                    getMeta('last_download_info', function(info) {\n                        if (info) {\n                            $('#les_last_download_info').text('(' + info.value + ')');\n                        }\n                    });\n                }\n\n                function getMeta(key, cb) {\n                    if (!db) return cb(null);\n                    try {\n                        const tx = db.transaction(['meta'], 'readonly');\n                        const store = tx.objectStore('meta');\n                        const req = store.get(key);\n                        req.onsuccess = function() { cb(req.result); };\n                        req.onerror = function() { cb(null); };\n                    } catch(e) { cb(null); }\n                }\n\n                function setMeta(key, value) {\n                    if (!db) return;\n                    try {\n                        const tx = db.transaction(['meta'], 'readwrite');\n                        tx.objectStore('meta').put({ key: key, value: value });\n                    } catch(e){}\n                }\n\n                function countPendingSyncItems(cb) {\n                    if (!db) return cb(0);\n                    try {\n                        const tx = db.transaction(['pending_sync'], 'readonly');\n                        const store = tx.objectStore('pending_sync');\n                        const req = store.count();\n                        req.onsuccess = function() { cb(req.result || 0); };\n                        req.onerror = function() { cb(0); };\n                    } catch(e) { cb(0); }\n                }\n\n                function updateCheckedInCount() {\n                    if (!db) return;\n\n                    try {\n                        const tx = db.transaction(['tickets'], 'readonly');\n                        const req = tx.objectStore('tickets').getAll();\n                        req.onsuccess = function() {\n                            const selectedEvent = String($('#les_offline_event_select').val() || '0');\n                            const checkedInOrders = new Set();\n                            (req.result || []).forEach(function(ticket) {\n                                if (ticket.check_in_status !== 1) return;\n                                if (selectedEvent !== '0' && String(ticket.event_id) !== selectedEvent) return;\n                                checkedInOrders.add(String(ticket.master_ticket_code || ticket.order_id));\n                            });\n                            $('#les_checked_in_count_value').text(checkedInOrders.size);\n                        };\n                    } catch(e) {}\n                }\n\n                \/\/ \u2500\u2500 PREFETCH GUEST LIST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                $('#les_prefetch_btn').on('click', function(e) {\n                    e.preventDefault();\n                    var $btn = $(this);\n                    var eventId = $('#les_offline_event_select').val();\n\n                    $btn.prop('disabled', true).text(les_checkin_i18n.downloadingData);\n\n                    $.post(les_scanner_config.ajax_url, {\n                        action: 'les_get_offline_guest_list',\n                        nonce: les_scanner_config.nonce,\n                        event_id: eventId\n                    }, function(res) {\n                        $btn.prop('disabled', false).html('<span class=\"dashicons dashicons-download\" style=\"font-size:14px; width:14px; height:14px; line-height:14px;\"><\/span> Download \/ Refresh Offline Guest List');\n                        if (res.success && res.data.tickets) {\n                            saveTicketsToIndexedDB(res.data.tickets, function(count) {\n                                const nowStr = res.data.tickets.length + ' tickets saved @ ' + new Date().toLocaleTimeString();\n                                setMeta('last_download_info', nowStr);\n                                $('#les_last_download_info').text('(' + nowStr + ')');\n                                updateCheckedInCount();\n                                showResult(les_checkin_i18n.downloadSuccess + ' (' + count + ' tickets saved offline)', 'success');\n                            });\n                        } else {\n                            showResult('Error: ' + (res.data ? res.data.message : 'Failed to fetch tickets.'), 'error');\n                        }\n                    }).fail(function() {\n                        $btn.prop('disabled', false).html('<span class=\"dashicons dashicons-download\" style=\"font-size:14px; width:14px; height:14px; line-height:14px;\"><\/span> Download \/ Refresh Offline Guest List');\n                        showResult(les_checkin_i18n.serverError, 'error');\n                    });\n                });\n\n                $('#les_offline_event_select').on('change', updateCheckedInCount);\n\n                function saveTicketsToIndexedDB(tickets, cb) {\n                    if (!db) return cb(0);\n                    const tx = db.transaction(['tickets'], 'readwrite');\n                    const store = tx.objectStore('tickets');\n                    let count = 0;\n                    tickets.forEach(function(t) {\n                        store.put(t);\n                        count++;\n                    });\n                    tx.oncomplete = function() { cb(count); };\n                }\n\n                \/\/ \u2500\u2500 SYNC PENDING CHECKINS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                $('#les_sync_now_btn').on('click', function(e) {\n                    e.preventDefault();\n                    syncPendingCheckins();\n                });\n\n                function syncPendingCheckins() {\n                    if (!db || !navigator.onLine) return;\n\n                    const tx = db.transaction(['pending_sync'], 'readonly');\n                    const store = tx.objectStore('pending_sync');\n                    const req = store.getAll();\n\n                    req.onsuccess = function() {\n                        const items = req.result;\n                        if (!items || items.length === 0) {\n                            updateStatusUI();\n                            return;\n                        }\n\n                        $('#les_sync_now_btn').prop('disabled', true).text(les_checkin_i18n.syncing);\n\n                        $.post(les_scanner_config.ajax_url, {\n                            action: 'les_batch_sync_offline_checkins',\n                            nonce: les_scanner_config.nonce,\n                            checkins: JSON.stringify(items)\n                        }, function(res) {\n                            $('#les_sync_now_btn').prop('disabled', false);\n                            if (res.success) {\n                                const clearTx = db.transaction(['pending_sync'], 'readwrite');\n                                clearTx.objectStore('pending_sync').clear();\n                                clearTx.oncomplete = function() {\n                                    updateStatusUI();\n                                    appendSyncNotice(res.data.message, 'success');\n                                };\n                            } else {\n                                updateStatusUI();\n                                appendSyncNotice('Sync Warning: ' + (res.data ? res.data.message : 'Check-ins queued.'), 'warning');\n                            }\n                        }).fail(function() {\n                            $('#les_sync_now_btn').prop('disabled', false);\n                            updateStatusUI();\n                        });\n                    };\n                }\n\n                \/\/ \u2500\u2500 TICKET VERIFICATION ENGINE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n                function verifyTicketCode(rawCode) {\n                    if (!rawCode) {\n                        showResult(les_checkin_i18n.pleaseEnterCode, 'warning');\n                        return;\n                    }\n\n                    const code = rawCode.trim();\n                    const numMatch = code.match(\/\\d+\/);\n                    const orderId = numMatch ? numMatch[0] : 0;\n\n                    clearCompletedResult();\n                    showResult(les_checkin_i18n.verifying, 'info');\n                    $('#verify_manual_code_btn').prop('disabled', true);\n\n                    \/\/ Try local IndexedDB verification first\n                    tryOfflineVerification(code, orderId, function(handledOffline) {\n                        if (handledOffline) {\n                            $('#verify_manual_code_btn').prop('disabled', false);\n                            $('#manual_ticket_code_input').val('');\n                            $('#les-scan-result').append('<p style=\"margin-top:10px;\"><button class=\"button button-primary\" id=\"restart-scanner-btn\">' + les_checkin_i18n.scanNext + '<\/button><\/p>');\n                            return;\n                        }\n\n                        \/\/ Fallback: If offline and ticket wasn't found in downloaded local database\n                        if (!navigator.onLine) {\n                            showResult(les_checkin_i18n.ticketNotFound + ' (Ticket code not found in downloaded offline guest list. Please refresh guest list when online.)', 'error');\n                            $('#verify_manual_code_btn').prop('disabled', false);\n                            $('#manual_ticket_code_input').val('');\n                            $('#les-scan-result').append('<p style=\"margin-top:10px;\"><button class=\"button button-primary\" id=\"restart-scanner-btn\">' + les_checkin_i18n.scanNext + '<\/button><\/p>');\n                            return;\n                        }\n\n                        \/\/ Fallback to online AJAX if online\n                        $.ajax({\n                            url: les_scanner_config.ajax_url,\n                            type: 'POST',\n                            data: {\n                                action: 'les_verify_master_ticket',\n                                nonce: les_scanner_config.nonce,\n                                master_ticket_code: code,\n                                order_id: orderId\n                            },\n                            success: function(response) {\n                                if (response.success) {\n                                    showResult(response.data.message, 'success');\n                                } else {\n                                    showResult(response.data.message, 'error');\n                                }\n                            },\n                            error: function() {\n                                showResult(les_checkin_i18n.serverError, 'error');\n                            },\n                            complete: function() {\n                                $('#verify_manual_code_btn').prop('disabled', false);\n                                $('#manual_ticket_code_input').val('');\n                                $('#les-scan-result').append('<p style=\"margin-top:10px;\"><button class=\"button button-primary\" id=\"restart-scanner-btn\">' + les_checkin_i18n.scanNext + '<\/button><\/p>');\n                            }\n                        });\n                    });\n                }\n\n                function tryOfflineVerification(code, orderId, cb) {\n                    if (!db) return cb(false);\n\n                    try {\n                        const tx = db.transaction(['tickets'], 'readwrite');\n                        const store = tx.objectStore('tickets');\n                        const req = store.getAll();\n\n                        req.onsuccess = function() {\n                            const allTickets = req.result || [];\n                            if (allTickets.length === 0) return cb(false);\n\n                            const cleanCode    = code.trim().toUpperCase();\n                            const cleanOrderId = String(orderId || '').trim();\n\n                            \/\/ 1. Match by exact master_ticket_code\n                            let ticket = allTickets.find(t => t.master_ticket_code && t.master_ticket_code.toUpperCase() === cleanCode);\n\n                            \/\/ 2. Match by order_id\n                            if (!ticket && cleanOrderId) {\n                                ticket = allTickets.find(t => String(t.order_id) === cleanOrderId);\n                            }\n\n                            \/\/ 3. Match by code substring \/ prefix\n                            if (!ticket && cleanCode) {\n                                ticket = allTickets.find(t => t.master_ticket_code && t.master_ticket_code.toUpperCase().includes(cleanCode));\n                            }\n\n                            if (!ticket) {\n                                return cb(false);\n                            }\n\n                            if (ticket.check_in_status === 1) {\n                                const checkinTimeStr = ticket.check_in_time ? ' at ' + ticket.check_in_time : '';\n                                showResult(buildOfflineTicketMessage(ticket, true), 'error');\n                                return cb(true);\n                            }\n\n                            \/\/ Mark checked-in locally in IndexedDB\n                            const nowStr = new Date().toISOString().replace('T', ' ').substring(0, 19);\n                            ticket.check_in_status = 1;\n                            ticket.check_in_time   = nowStr;\n                            store.put(ticket);\n\n                            \/\/ Queue for background sync\n                            const pendingTx = db.transaction(['pending_sync'], 'readwrite');\n                            pendingTx.objectStore('pending_sync').add({\n                                order_id: ticket.order_id,\n                                master_ticket_code: ticket.master_ticket_code,\n                                check_in_time: nowStr\n                            });\n\n                            pendingTx.oncomplete = function() {\n                                updateStatusUI();\n                                showResult(buildOfflineTicketMessage(ticket, false), 'success');\n                                cb(true);\n                            };\n                        };\n\n                        req.onerror = function() { cb(false); };\n                    } catch(e) {\n                        cb(false);\n                    }\n                }\n\n                function buildOfflineTicketMessage(ticket, alreadyCheckedIn) {\n                    const guestCount = parseInt(ticket.quantity, 10) || 1;\n                    const guestName = ticket.guest_name || 'Guest name not assigned';\n                    const schedule = [ticket.event_date, ticket.event_time].filter(Boolean).join(' \u00b7 ');\n                    const eventDetails = [ticket.event_title, schedule, ticket.venue_name].filter(Boolean).join(' \u00b7 ');\n                    const contact = [ticket.customer_email, ticket.customer_phone].filter(Boolean).join('<br>');\n                    const heading = alreadyCheckedIn ? les_checkin_i18n.alreadyCheckedIn : '\u2713 Success! Check-in Complete (Offline).';\n                    const timeLabel = alreadyCheckedIn ? 'Originally checked in:' : 'Checked in offline:';\n                    const time = ticket.check_in_time ? '<br><strong>' + timeLabel + '<\/strong> ' + escapeHtml(ticket.check_in_time) : '';\n                    return '<strong>' + heading + '<\/strong><br>' +\n                        '<strong>Customer:<\/strong> ' + escapeHtml(ticket.customer_name) + '<br>' +\n                        '<strong>Order:<\/strong> #' + escapeHtml(ticket.order_id) + '<br>' +\n                        '<strong>Guests:<\/strong> ' + guestCount + '<br>' +\n                        '<strong>Event:<\/strong> ' + escapeHtml(eventDetails) + '<br>' +\n                        '<strong>Seat \/ guest:<\/strong> ' + escapeHtml(ticket.seat_label || ticket.seat_id) + ' \u2014 ' + escapeHtml(guestName) + '<br>' +\n                        (contact ? '<strong>Contact:<\/strong> ' + contact : '') + time;\n                }\n\n                function escapeHtml(str) {\n                    return $('<div>').text(str || '').html();\n                }\n\n                function showResult(msg, type) {\n                    const $el = $('#les-scan-result');\n\n                    \/\/ Keep the completed ticket summary visible until staff starts another scan.\n                    if ($el.data('les-result-locked')) {\n                        return;\n                    }\n\n                    $el.removeClass('notice-info notice-error notice-success notice-warning');\n                    \n                    let color = '#72aee6'; \/\/ info\n                    if(type === 'error') color = '#d63638';\n                    if(type === 'success') color = '#46b450';\n                    if(type === 'warning') color = '#f0b849';\n\n                    $el.css('border-left-color', color);\n                    $el.html('<p>' + msg + '<\/p>');\n                    $el.data('les-result-locked', type === 'success');\n                }\n\n                function appendSyncNotice(msg, type) {\n                    const $el = $('#les-scan-result');\n                    const noticeClass = type === 'warning' ? 'les-sync-warning' : 'les-sync-success';\n                    $el.find('.' + noticeClass).remove();\n                    $el.append('<p class=\"' + noticeClass + '\" style=\"margin:10px 0 0; font-size:12px; opacity:.8;\">' + escapeHtml(msg) + '<\/p>');\n                }\n\n                function clearCompletedResult() {\n                    $('#les-scan-result').removeData('les-result-locked');\n                }\n\n                let html5QrcodeScanner = null;\n\n                function startScanner() {\n                    if (html5QrcodeScanner) { \n                        try { html5QrcodeScanner.clear(); } catch(e){}\n                    }\n                    \n                    clearCompletedResult();\n                    showResult(les_checkin_i18n.waiting, 'info');\n                    \n                    html5QrcodeScanner = new Html5QrcodeScanner(\"les-qr-reader\", { fps: 10, qrbox: 250 });\n                    html5QrcodeScanner.render(onScanSuccess, (err) => {\n                        if (String(err).toLowerCase().indexOf('notallowederror') !== -1 || String(err).toLowerCase().indexOf('permission denied') !== -1) {\n                            showResult('Camera access is blocked for this page. In Android Chrome, open the lock icon, choose Permissions, set Camera to Allow, then reload.', 'error');\n                        }\n                    });\n                }\n\n                function onScanSuccess(decodedText, decodedResult) {\n                    try { html5QrcodeScanner.clear(); } catch(e){}\n                    try {\n                        const ticketData = JSON.parse(decodedText);\n                        if (!ticketData.master_ticket_code) throw new Error();\n                        verifyTicketCode(ticketData.master_ticket_code);\n                    } catch(e) {\n                        if (decodedText && decodedText.indexOf('LESO-') === 0) {\n                            verifyTicketCode(decodedText.trim());\n                        } else {\n                            showResult(les_checkin_i18n.invalidQR, 'error');\n                            $('#les-scan-result').append('<p><button class=\"button button-primary\" id=\"restart-scanner-btn\">' + les_checkin_i18n.scanAgain + '<\/button><\/p>');\n                        }\n                    }\n                }\n\n                \/\/ Event Listeners\n                window.addEventListener('online', updateStatusUI);\n                window.addEventListener('offline', updateStatusUI);\n\n                initIndexedDB(function() {\n                    startScanner();\n                });\n\n                $(document).on('click', '#restart-scanner-btn', function() {\n                    startScanner();\n                });\n                \n                $('#verify_manual_code_btn').on('click', function() {\n                    const manualCode = $('#manual_ticket_code_input').val().trim();\n                    if(html5QrcodeScanner) {\n                        try { html5QrcodeScanner.clear(); } catch(e){}\n                    }\n                    verifyTicketCode(manualCode);\n                });\n\n                $('#manual_ticket_code_input').on('keypress', function(e) {\n                    if (e.which === 13) $('#verify_manual_code_btn').click();\n                });\n            });\n        <\/script>\n        \n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-272","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.3 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>scanner - My Plugins Demo<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/macmonir.net\/test\/scanner\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"scanner - My Plugins Demo\" \/>\n<meta property=\"og:url\" content=\"https:\/\/macmonir.net\/test\/scanner\/\" \/>\n<meta property=\"og:site_name\" content=\"My Plugins Demo\" \/>\n<meta property=\"article:modified_time\" content=\"2026-02-25T09:39:26+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/macmonir.net\\\/test\\\/scanner\\\/\",\"url\":\"https:\\\/\\\/macmonir.net\\\/test\\\/scanner\\\/\",\"name\":\"scanner - My Plugins Demo\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/macmonir.net\\\/test\\\/#website\"},\"datePublished\":\"2026-02-25T09:24:05+00:00\",\"dateModified\":\"2026-02-25T09:39:26+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/macmonir.net\\\/test\\\/scanner\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/macmonir.net\\\/test\\\/scanner\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/macmonir.net\\\/test\\\/scanner\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/macmonir.net\\\/test\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"scanner\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/macmonir.net\\\/test\\\/#website\",\"url\":\"https:\\\/\\\/macmonir.net\\\/test\\\/\",\"name\":\"My Blog\",\"description\":\"My Plugins Demo\",\"publisher\":{\"@id\":\"https:\\\/\\\/macmonir.net\\\/test\\\/#\\\/schema\\\/person\\\/ecff2bb70513f60141e7763107eb9ceb\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/macmonir.net\\\/test\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/macmonir.net\\\/test\\\/#\\\/schema\\\/person\\\/ecff2bb70513f60141e7763107eb9ceb\",\"name\":\"ozasg\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/abbe28d5cea5474d548af07c9445010cc691868aa22fa07b13d5ab610c89455b?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/abbe28d5cea5474d548af07c9445010cc691868aa22fa07b13d5ab610c89455b?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/abbe28d5cea5474d548af07c9445010cc691868aa22fa07b13d5ab610c89455b?s=96&d=mm&r=g\",\"caption\":\"ozasg\"},\"logo\":{\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/abbe28d5cea5474d548af07c9445010cc691868aa22fa07b13d5ab610c89455b?s=96&d=mm&r=g\"},\"sameAs\":[\"http:\\\/\\\/macmonir.net\\\/test\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"scanner - My Plugins Demo","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/macmonir.net\/test\/scanner\/","og_locale":"en_US","og_type":"article","og_title":"scanner - My Plugins Demo","og_url":"https:\/\/macmonir.net\/test\/scanner\/","og_site_name":"My Plugins Demo","article_modified_time":"2026-02-25T09:39:26+00:00","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/macmonir.net\/test\/scanner\/","url":"https:\/\/macmonir.net\/test\/scanner\/","name":"scanner - My Plugins Demo","isPartOf":{"@id":"https:\/\/macmonir.net\/test\/#website"},"datePublished":"2026-02-25T09:24:05+00:00","dateModified":"2026-02-25T09:39:26+00:00","breadcrumb":{"@id":"https:\/\/macmonir.net\/test\/scanner\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/macmonir.net\/test\/scanner\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/macmonir.net\/test\/scanner\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/macmonir.net\/test\/"},{"@type":"ListItem","position":2,"name":"scanner"}]},{"@type":"WebSite","@id":"https:\/\/macmonir.net\/test\/#website","url":"https:\/\/macmonir.net\/test\/","name":"My Blog","description":"My Plugins Demo","publisher":{"@id":"https:\/\/macmonir.net\/test\/#\/schema\/person\/ecff2bb70513f60141e7763107eb9ceb"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/macmonir.net\/test\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/macmonir.net\/test\/#\/schema\/person\/ecff2bb70513f60141e7763107eb9ceb","name":"ozasg","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/abbe28d5cea5474d548af07c9445010cc691868aa22fa07b13d5ab610c89455b?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/abbe28d5cea5474d548af07c9445010cc691868aa22fa07b13d5ab610c89455b?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/abbe28d5cea5474d548af07c9445010cc691868aa22fa07b13d5ab610c89455b?s=96&d=mm&r=g","caption":"ozasg"},"logo":{"@id":"https:\/\/secure.gravatar.com\/avatar\/abbe28d5cea5474d548af07c9445010cc691868aa22fa07b13d5ab610c89455b?s=96&d=mm&r=g"},"sameAs":["http:\/\/macmonir.net\/test"]}]}},"_links":{"self":[{"href":"https:\/\/macmonir.net\/test\/wp-json\/wp\/v2\/pages\/272","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/macmonir.net\/test\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/macmonir.net\/test\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/macmonir.net\/test\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/macmonir.net\/test\/wp-json\/wp\/v2\/comments?post=272"}],"version-history":[{"count":3,"href":"https:\/\/macmonir.net\/test\/wp-json\/wp\/v2\/pages\/272\/revisions"}],"predecessor-version":[{"id":275,"href":"https:\/\/macmonir.net\/test\/wp-json\/wp\/v2\/pages\/272\/revisions\/275"}],"wp:attachment":[{"href":"https:\/\/macmonir.net\/test\/wp-json\/wp\/v2\/media?parent=272"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}