<?php

/**

 * Canteen QR Scanner — Queue 2 eliminator

 * Mobile-first, full-viewport, no EduTrack sidebar.

 * Auth: canteen_staff_authenticated PIN session OR valid EduTrack role.

 */ 

if (session_status() === PHP_SESSION_NONE) session_start();

require_once '../config.php';

require_once '../includes/canteen_helpers.php';



canteen_require_active();



// Auth gate

$is_pin_session  = !empty($_SESSION['canteen_staff_authenticated']);

$is_role_session = !empty($_SESSION['user_id']);



if (!$is_pin_session && !$is_role_session) {

    header('Location: scanner_login.php');

    exit;

}



// PIN sessions expire after 12 hours

if ($is_pin_session && isset($_SESSION['canteen_staff_auth_time'])) {

    if (time() - $_SESSION['canteen_staff_auth_time'] > 43200) {

        unset($_SESSION['canteen_staff_authenticated'], $_SESSION['canteen_staff_auth_time']);

        header('Location: scanner_login.php?expired=1');

        exit;

    }

}



$conn = getDBConnection();



// Load today's active/scheduled sessions for the selector

$today    = date('Y-m-d');

$sessions = [];

$res = $conn->prepare(

    "SELECT id, session_name, start_time, end_time, total_enrolled,

            (SELECT COUNT(*) FROM canteen_entitlements WHERE session_id = canteen_sessions.id AND status = 'confirmed') AS confirmed_count

     FROM canteen_sessions

     WHERE session_date = ? AND school_id = 1 AND status IN ('scheduled','active')

     ORDER BY start_time ASC"

);

$res->bind_param('s', $today);

$res->execute();

$result = $res->get_result();

while ($row = $result->fetch_assoc()) {

    $sessions[] = $row;

}

$res->close();



$active_session = $sessions[0] ?? null;



$settings    = canteen_get_settings();

$fp_enabled  = !empty($settings['fingerprint_enabled']);

$face_enabled= !empty($settings['facial_recognition_enabled']);

$school_name = defined('SCHOOL_NAME') ? SCHOOL_NAME : 'School Canteen';

$conn->close();

?>

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

    <meta name="theme-color" content="#1a1a2e">

    <title>Canteen Scanner</title>

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css">

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">

    <style>

        *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }



        :root {

            --green:  #4CAF50;

            --amber:  #FF9800;

            --red:    #F44336;

            --dark:   #1a1a2e;

            --header: 70px;

        }



        html, body {

            height: 100dvh;

            overflow: hidden;

            background: #000;

            font-family: 'Segoe UI', sans-serif;

            color: #fff;

        }



        /* ── Header ─────────────────────────────────────────────────────────── */

        #scannerHeader {

            position: fixed; top: 0; left: 0; right: 0;

            height: var(--header);

            background: var(--dark);

            display: flex; align-items: center;

            padding: 0 1rem;

            gap: 0.75rem;

            z-index: 100;

            border-bottom: 1px solid rgba(255,255,255,.1);

        }

        #sessionSelector {

            flex: 1; background: rgba(255,255,255,.1);

            border: 1px solid rgba(255,255,255,.2);

            color: #fff; border-radius: 8px;

            padding: 0.4rem 0.6rem; font-size: 0.85rem;

        }

        #sessionSelector option { background: #1a1a2e; }

        #statsBar {

            font-size: 0.8rem; white-space: nowrap;

            background: rgba(255,255,255,.08);

            padding: 4px 10px; border-radius: 20px;

        }

        #statsBar .count { font-size: 1rem; font-weight: 700; }

        #offlineBadge {

            background: var(--amber); color: #000;

            font-size: 0.7rem; font-weight: 700;

            padding: 3px 8px; border-radius: 20px;

            display: none;

        }



        /* ── Camera ─────────────────────────────────────────────────────────── */

        #cameraWrap {

            position: fixed;

            top: var(--header); left: 0; right: 0;

            height: calc(60dvh - var(--header));

            background: #111;

            overflow: hidden;

        }

        #scanVideo {

            width: 100%; height: 100%;

            object-fit: cover;

        }

        #scanCanvas { display: none; }



        /* Corner brackets */

        #brackets {

            position: absolute; inset: 0;

            pointer-events: none;

        }

        .bracket {

            position: absolute; width: 40px; height: 40px;

            border-color: #4CAF50; border-style: solid;

        }

        .bracket.tl { top: 20%; left: 20%; border-width: 3px 0 0 3px; }

        .bracket.tr { top: 20%; right: 20%; border-width: 3px 3px 0 0; }

        .bracket.bl { bottom: 20%; left: 20%; border-width: 0 0 3px 3px; }

        .bracket.br { bottom: 20%; right: 20%; border-width: 0 3px 3px 0; }

        .scan-line {

            position: absolute; left: 20%; right: 20%;

            height: 2px; background: rgba(76,175,80,.7);

            animation: scanline 2s linear infinite;

        }

        @keyframes scanline {

            0%   { top: 20%; }

            100% { top: 80%; }

        }



        /* Camera status */

        #cameraStatus {

            position: absolute; inset: 0;

            display: flex; flex-direction: column;

            align-items: center; justify-content: center;

            background: rgba(0,0,0,.7);

            font-size: 0.9rem; gap: 0.5rem;

            text-align: center; padding: 1rem;

        }

        #cameraStatus i { font-size: 2rem; color: var(--amber); }



        /* ── Manual entry ────────────────────────────────────────────────────── */

        #bottomPanel {

            position: fixed;

            top: 60dvh; left: 0; right: 0;

            bottom: 0;

            background: #0d1117;

            padding: 0.75rem 1rem 1rem;

            overflow-y: auto;

            display: flex; flex-direction: column; gap: 0.5rem;

        }

        #manualForm {

            display: flex; gap: 0.5rem;

        }

        #manualInput {

            flex: 1; background: rgba(255,255,255,.08);

            border: 1px solid rgba(255,255,255,.2);

            color: #fff; border-radius: 8px;

            padding: 0.5rem 0.75rem; font-size: 1rem;

        }

        #manualInput::placeholder { color: rgba(255,255,255,.4); }

        #manualInput:focus { outline: none; border-color: var(--green); }

        #manualConfirmBtn {

            background: var(--green); border: none;

            color: #fff; border-radius: 8px;

            padding: 0.5rem 1rem; font-weight: 600;

        }



        #recentList {

            flex: 1; overflow-y: auto;

        }

        .recent-item {

            display: flex; align-items: center; gap: 0.6rem;

            background: rgba(255,255,255,.05);

            border-radius: 8px; padding: 0.5rem 0.75rem;

            margin-bottom: 0.4rem; font-size: 0.85rem;

        }

        .recent-item .badge-dot {

            width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;

        }

        .dot-success  { background: var(--green); }

        .dot-duplicate{ background: var(--amber); }

        .dot-invalid  { background: var(--red); }

        .dot-override { background: #2196F3; }



        /* ── Result overlay ──────────────────────────────────────────────────── */

        #resultOverlay {

            position: fixed; inset: 0; z-index: 200;

            display: none; flex-direction: column;

            align-items: center; justify-content: center;

            padding: 2rem; text-align: center;

        }

        #resultOverlay.success  { background: rgba(30,120,40,.97); }

        #resultOverlay.duplicate{ background: rgba(180,100,0,.97); }

        #resultOverlay.error    { background: rgba(180,30,30,.97); }

        #resultOverlay.not_enrolled { background: rgba(180,30,30,.97); }



        #resultOverlay .ov-icon { font-size: 4rem; margin-bottom: 0.75rem; }

        #resultOverlay .ov-name { font-size: 1.8rem; font-weight: 700; }

        #resultOverlay .ov-class{ font-size: 1.1rem; opacity: .85; }

        #resultOverlay .ov-msg  { font-size: 1rem; margin-top: 0.5rem; opacity: .9; }

        #resultPhoto {

            width: 100px; height: 100px; border-radius: 50%;

            object-fit: cover; border: 3px solid rgba(255,255,255,.5);

            margin-bottom: 0.75rem; display: none;

        }

        #resultOverlay .dismiss-btn {

            margin-top: 1.5rem; background: rgba(255,255,255,.2);

            border: 1px solid rgba(255,255,255,.4);

            color: #fff; border-radius: 20px;

            padding: 0.5rem 1.5rem; font-size: 0.9rem;

            cursor: pointer;

        }

        #manualOverrideBtn {

            margin-top: 0.75rem; background: rgba(255,255,255,.1);

            border: 1px solid rgba(255,255,255,.3);

            color: #fff; border-radius: 20px;

            padding: 0.4rem 1.2rem; font-size: 0.85rem;

            cursor: pointer; display: none;

        }



        /* ── Manual override modal ───────────────────────────────────────────── */

        .modal-content { background: #1a1a2e; color: #fff; }

        .modal-header { border-color: rgba(255,255,255,.1); }

        .modal-footer { border-color: rgba(255,255,255,.1); }

        .form-control, .form-select {

            background: rgba(255,255,255,.1);

            border-color: rgba(255,255,255,.2);

            color: #fff;

        }

        .form-control:focus, .form-select:focus {

            background: rgba(255,255,255,.15);

            color: #fff; border-color: var(--green);

            box-shadow: none;

        }

        .form-label { font-size: 0.85rem; opacity: .75; }



        /* ── Biometric buttons ───────────────────────────────────────────────── */

        #biometricRow {

            display: flex; gap: 0.5rem; margin-top: 0.5rem;

        }

        .bio-btn {

            flex: 1; background: rgba(255,255,255,.08);

            border: 1px solid rgba(255,255,255,.2);

            color: #fff; border-radius: 8px;

            padding: 0.5rem 0.5rem; font-size: 0.8rem;

            cursor: pointer; text-align: center;

            display: flex; flex-direction: column; align-items: center; gap: 4px;

        }

        .bio-btn:active { background: rgba(255,255,255,.18); }

        .bio-btn.scanning-fp { border-color: #2196F3; background: rgba(33,150,243,.2); }

        .bio-btn.scanning-face { border-color: #9C27B0; background: rgba(156,39,176,.2); }

        #faceWrap {

            position: fixed;

            top: var(--header); left: 0; right: 0;

            height: calc(60dvh - var(--header));

            z-index: 50; display: none;

        }

        #faceVideo { width: 100%; height: 100%; object-fit: cover; }

        #faceCanvas { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }

        #faceStatusBanner {

            position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);

            background: rgba(0,0,0,.75); color: #fff; border-radius: 20px;

            padding: 4px 14px; font-size: .8rem; white-space: nowrap;

        }

        #faceLowLight {

            position: absolute; top: 8px; left: 50%; transform: translateX(-50%);

            background: rgba(255,152,0,.9); color: #000;

            padding: 3px 12px; border-radius: 20px; font-size: .75rem;

            display: none;

        }

        #faceConfirmOverlay {

            position: fixed; inset: 0; z-index: 210;

            background: rgba(100,0,140,.95);

            display: none; flex-direction: column;

            align-items: center; justify-content: center;

            text-align: center; padding: 2rem;

        }

        #faceConfirmOverlay img { width: 100px; height: 100px; border-radius: 50%; object-fit: cover; border: 3px solid rgba(255,255,255,.5); margin-bottom: 1rem; }

        #faceConfirmOverlay .conf-name { font-size: 1.6rem; font-weight: 700; }

        #faceConfirmOverlay .conf-class { font-size: 1rem; opacity: .85; margin-bottom: 1.5rem; }

        #faceConfirmBtns { display: flex; gap: 1rem; }

        #faceConfirmBtns button { padding: .6rem 1.6rem; border-radius: 20px; font-weight: 600; font-size: .95rem; border: 2px solid; cursor: pointer; }

        #btnFaceYes  { background: #4CAF50; border-color: #4CAF50; color: #fff; }

        #btnFaceNo   { background: transparent; border-color: rgba(255,255,255,.5); color: #fff; }



        /* ── No sessions state ───────────────────────────────────────────────── */

        /* ── html5-qrcode video override ── */

        #qr-reader { position: absolute; inset: 0; width: 100%; height: 100%; }

        #qr-reader video {

            width: 100% !important; height: 100% !important;

            object-fit: cover !important; display: block !important;

        }



        /* ── No sessions state ───────────────────────────────────────────────── */

        #noSessions {

            position: fixed; inset: 0;

            background: var(--dark);

            display: flex; flex-direction: column;

            align-items: center; justify-content: center;

            text-align: center; padding: 2rem; gap: 1rem;

        }

        #noSessions i { font-size: 3rem; color: var(--amber); }

    </style>

</head>

<body>



<?php if (empty($sessions)): ?>

<!-- ── No sessions state ──────────────────────────────────────────────────── -->

<div id="noSessions">

    <i class="fas fa-calendar-times"></i>

    <h4>No Active Sessions Today</h4>

    <p class="text-white-50">No canteen sessions are scheduled or active for <?= date('l, d M Y') ?>.</p>

    <?php if ($is_role_session): ?>

    <a href="setup.php" class="btn btn-outline-light mt-2">

        <i class="fas fa-plus me-2"></i>Create a Session

    </a>

    <?php endif; ?>

</div>



<?php else: ?>



<!-- ── Header ─────────────────────────────────────────────────────────────── -->

<div id="scannerHeader">

    <i class="fas fa-qrcode text-success"></i>

    <select id="sessionSelector" aria-label="Select session">

        <?php foreach ($sessions as $s): ?>

        <option value="<?= $s['id'] ?>"

                data-enrolled="<?= (int)$s['total_enrolled'] ?>"

                data-confirmed="<?= (int)$s['confirmed_count'] ?>"

            <?= $active_session && $s['id'] == $active_session['id'] ? 'selected' : '' ?>>

            <?= htmlspecialchars($s['session_name']) ?>

            (<?= date('H:i', strtotime($s['start_time'])) ?>–<?= date('H:i', strtotime($s['end_time'])) ?>)

        </option>

        <?php endforeach; ?>

    </select>

    <div id="statsBar">

        <span id="confirmedCount" class="count">0</span>

        <span class="opacity-50">/</span>

        <span id="enrolledCount" class="count">0</span>

    </div>

    <span id="offlineBadge">OFFLINE</span>

    <?php if ($is_role_session): ?>

    <a href="../dashboard.php" class="text-white-50 ms-1" title="Back to EduTrack">

        <i class="fas fa-times"></i>

    </a>

    <?php else: ?>

    <a href="scanner_login.php?logout=1" class="text-white-50 ms-1" title="Lock scanner">

        <i class="fas fa-lock"></i>

    </a>

    <?php endif; ?>

</div>



<!-- ── Camera ─────────────────────────────────────────────────────────────── -->

<div id="cameraWrap">

    <div id="qr-reader"></div>



    <div id="brackets">

        <div class="bracket tl"></div>

        <div class="bracket tr"></div>

        <div class="bracket bl"></div>

        <div class="bracket br"></div>

        <div class="scan-line"></div>

    </div>



    <div id="cameraStatus">

        <i class="fas fa-circle-notch fa-spin"></i>

        <span>Starting camera…</span>

    </div>

</div>



<!-- ── Bottom panel ───────────────────────────────────────────────────────── -->

<div id="bottomPanel">

    <div id="manualForm">

        <input type="text" id="manualInput" placeholder="Enter Student ID"

               autocomplete="off" autocorrect="off" spellcheck="false">

        <button id="manualConfirmBtn" type="button">

            <i class="fas fa-check"></i>

        </button>

    </div>



    <?php if ($fp_enabled || $face_enabled): ?>

    <div id="biometricRow">

        <?php if ($fp_enabled): ?>

        <button class="bio-btn" id="btnFingerprint">

            <i class="fas fa-fingerprint"></i>

            <span>Fingerprint</span>

        </button>

        <?php endif; ?>

        <?php if ($face_enabled): ?>

        <button class="bio-btn" id="btnFaceScan">

            <i class="fas fa-user-circle"></i>

            <span>Face Scan</span>

        </button>

        <?php endif; ?>

    </div>

    <?php endif; ?>



    <p class="text-white-50 mb-1 mt-1" style="font-size:.75rem">Recent scans</p>

    <div id="recentList"></div>

</div>



<!-- ── Face camera overlay ────────────────────────────────────────────────── -->

<div id="faceWrap">

    <video id="faceVideo" autoplay playsinline muted></video>

    <canvas id="faceCanvas"></canvas>

    <div id="faceStatusBanner">Looking for face…</div>

    <div id="faceLowLight"><i class="fas fa-exclamation-triangle me-1"></i>Low lighting</div>

    <button id="btnCloseFace" style="position:absolute;top:8px;right:12px;background:rgba(0,0,0,.5);border:none;color:#fff;border-radius:20px;padding:4px 12px;font-size:.8rem;cursor:pointer">✕ Cancel</button>

</div>



<!-- ── Face confirm overlay ───────────────────────────────────────────────── -->

<div id="faceConfirmOverlay">

    <img id="faceConfPhoto" src="" alt="">

    <div class="conf-name" id="faceConfName"></div>

    <div class="conf-class" id="faceConfClass"></div>

    <div id="faceConfBtns" class="faceConfirmBtns">

        <button id="btnFaceYes"><i class="fas fa-check me-1"></i>Confirm Meal</button>

        <button id="btnFaceNo"><i class="fas fa-times me-1"></i>Cancel</button>

    </div>

</div>



<!-- ── Result overlay ─────────────────────────────────────────────────────── -->

<div id="resultOverlay" role="alert" aria-live="assertive">

    <img id="resultPhoto" src="" alt="">

    <div id="resultIcon" class="ov-icon"></div>

    <div id="resultName" class="ov-name"></div>

    <div id="resultClass" class="ov-class"></div>

    <div id="resultMsg" class="ov-msg"></div>

    <button class="dismiss-btn" id="dismissBtn">Tap to continue</button>

    <button id="manualOverrideBtn">

        <i class="fas fa-user-shield me-1"></i>Add Manually

    </button>

</div>



<!-- ── Manual override modal ─────────────────────────────────────────────── -->

<div class="modal fade" id="overrideModal" tabindex="-1" aria-labelledby="overrideModalLabel">

    <div class="modal-dialog modal-dialog-centered">

        <div class="modal-content">

            <div class="modal-header">

                <h5 class="modal-title" id="overrideModalLabel">

                    <i class="fas fa-user-shield me-2"></i>Manual Override

                </h5>

                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>

            </div>

            <div class="modal-body">

                <div class="mb-3">

                    <label class="form-label">Confirm your PIN</label>

                    <input type="password" id="overridePin" class="form-control"

                           placeholder="4-digit PIN" inputmode="numeric" maxlength="8">

                </div>

                <div class="mb-3">

                    <label class="form-label">Student name or ID</label>

                    <input type="text" id="overrideSearch" class="form-control" placeholder="Search…">

                    <div id="overrideSearchResults" class="mt-2"></div>

                </div>

                <div class="mb-3">

                    <label class="form-label">Reason</label>

                    <select id="overrideReason" class="form-select">

                        <option value="qr_damaged">QR damaged / unreadable</option>

                        <option value="phone_flat">Phone flat / out of charge</option>

                        <option value="late_payment">Late payment — just paid</option>

                        <option value="other">Other</option>

                    </select>

                </div>

            </div>

            <div class="modal-footer">

                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>

                <button type="button" class="btn btn-success" id="overrideSubmitBtn">

                    <i class="fas fa-check me-1"></i>Confirm Manually

                </button>

            </div>

        </div>

    </div>

</div>



<?php endif; ?>



<?php if ($face_enabled): ?>

<script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js" crossorigin="anonymous"></script>

<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>

<?php endif; ?>

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.bundle.min.js"></script>

<!-- html5-qrcode (ZXing-based) — more reliable on printed QR codes -->

<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>

<script>

(function () {

    'use strict';



    // ── Config ────────────────────────────────────────────────────────────────

    const VALIDATE_URL = '../ajax/canteen_validate.php';

    const OVERRIDE_URL = '../ajax/canteen_manual_override.php';

    const SCAN_FPS     = 15;

    const FRAME_MS     = 1000 / SCAN_FPS;



    let currentSessionId = 0;

    let enrolledCount    = 0;

    let confirmedCount   = 0;

    let scanning         = true;

    let lastScan         = 0;

    const DEBOUNCE_MS    = 1500; // ignore same frame until overlay dismissed



    // ── Elements ──────────────────────────────────────────────────────────────

    const cameraStatus  = document.getElementById('cameraStatus');

    const sessionSel    = document.getElementById('sessionSelector');

    const statsConfirmed= document.getElementById('confirmedCount');

    const statsEnrolled = document.getElementById('enrolledCount');

    const resultOverlay = document.getElementById('resultOverlay');

    const resultPhoto   = document.getElementById('resultPhoto');

    const resultIcon    = document.getElementById('resultIcon');

    const resultName    = document.getElementById('resultName');

    const resultClass   = document.getElementById('resultClass');

    const resultMsg     = document.getElementById('resultMsg');

    const dismissBtn    = document.getElementById('dismissBtn');

    const manualOverrideBtn = document.getElementById('manualOverrideBtn');

    const offlineBadge  = document.getElementById('offlineBadge');

    const manualInput   = document.getElementById('manualInput');

    const manualConfirmBtn = document.getElementById('manualConfirmBtn');

    const recentList    = document.getElementById('recentList');



    if (!sessionSel) return; // no sessions page



    // ── Session selector ──────────────────────────────────────────────────────

    function loadSession() {

        const opt = sessionSel.options[sessionSel.selectedIndex];

        if (!opt) return;

        currentSessionId = parseInt(opt.value, 10);

        enrolledCount    = parseInt(opt.dataset.enrolled, 10) || 0;

        confirmedCount   = parseInt(opt.dataset.confirmed, 10) || 0;

        updateStats();

    }



    function updateStats() {

        if (statsConfirmed) statsConfirmed.textContent = confirmedCount;

        if (statsEnrolled)  statsEnrolled.textContent  = enrolledCount;

    }



    sessionSel.addEventListener('change', loadSession);

    loadSession();



    // ── QR Scanner (html5-qrcode / ZXing) ─────────────────────────────────────

    const qrScanner = new Html5Qrcode('qr-reader');



    function startCamera() {

        qrScanner.start(

            { facingMode: { ideal: 'environment' } },

            {

                fps: SCAN_FPS,

                videoConstraints: {

                    facingMode: { ideal: 'environment' },

                    width: { ideal: 1280 },

                    height: { ideal: 720 }

                }

            },

            (decodedText) => {

                if (!scanning) return;

                const now = performance.now();

                if (now - lastScan < DEBOUNCE_MS) return;

                lastScan = now;

                handleToken(decodedText.trim());

            },

            () => {}

        ).then(() => {

            if (cameraStatus) cameraStatus.style.display = 'none';

        }).catch(() => {

            if (cameraStatus) {

                cameraStatus.innerHTML = '<i class="fas fa-exclamation-triangle" style="color:var(--red)"></i><span>Camera unavailable.<br>Use manual ID entry below.</span>';

            }

        });

    }



    // ── Offline / IndexedDB helpers ───────────────────────────────────────────

    const OFFLINE_DB    = 'canteen_offline';

    const OFFLINE_STORE = 'pending_confirmations';

    const PREFETCH_PATH = '../ajax/canteen_prefetch.php';

    const TODAY         = new Date().toISOString().slice(0, 10);



    function openOfflineDB() {

        return new Promise((resolve, reject) => {

            const req = indexedDB.open(OFFLINE_DB, 1);

            req.onupgradeneeded = (e) => {

                const db = e.target.result;

                if (!db.objectStoreNames.contains(OFFLINE_STORE)) {

                    const s = db.createObjectStore(OFFLINE_STORE, { keyPath: 'id', autoIncrement: true });

                    s.createIndex('synced', 'synced', { unique: false });

                }

            };

            req.onsuccess = () => resolve(req.result);

            req.onerror   = () => reject(req.error);

        });

    }



    async function queueOfflineConfirmation(record) {

        const db = await openOfflineDB();

        return new Promise((resolve, reject) => {

            const tx = db.transaction(OFFLINE_STORE, 'readwrite');

            tx.objectStore(OFFLINE_STORE).add({ ...record, synced: 0, queued_at: Date.now() });

            tx.oncomplete = resolve;

            tx.onerror    = reject;

        });

    }



    async function countPending() {

        const db = await openOfflineDB();

        return new Promise((resolve) => {

            const tx  = db.transaction(OFFLINE_STORE, 'readonly');

            const req = tx.objectStore(OFFLINE_STORE).index('synced').count(0);

            req.onsuccess = () => resolve(req.result);

            req.onerror   = () => resolve(0);

        });

    }



    // Read cached entitlement blob from Cache API

    async function getCachedEntries() {

        if (!('caches' in window)) return null;

        try {

            const cacheName = 'canteen-entitlements-' + TODAY;

            const cache     = await caches.open(cacheName);

            const resp      = await cache.match(PREFETCH_PATH + '?date=' + TODAY);

            if (!resp) return null;

            const data = await resp.json();

            return Array.isArray(data.entries) ? data.entries : null;

        } catch {

            return null;

        }

    }



    // ── Token handling ────────────────────────────────────────────────────────

    function handleToken(token) {

        if (!currentSessionId) {

            showResult('error', '', 'No Session', '', 'Select a session above.');

            return;

        }

        scanning = false;



        if (!navigator.onLine) {

            handleTokenOffline(token);

            return;

        }



        const fd = new FormData();

        fd.append('token', token);

        fd.append('session_id', currentSessionId);



        fetch(VALIDATE_URL, { method: 'POST', body: fd })

            .then(r => r.json())

            .then(data => processResult(data))

            .catch(() => {

                // Network failed — fall through to offline mode

                handleTokenOffline(token);

            });

    }



    async function handleTokenOffline(token) {

        const entries = await getCachedEntries();



        if (!entries) {

            showResult('error', '', '⚠', '', '',

                'Offline — no cached data. Connect to internet first.', 2500);

            setTimeout(resumeScanning, 2500);

            return;

        }



        // Skip __MANUAL__ lookups offline (need server to resolve student ID)

        if (token.startsWith('__MANUAL__')) {

            showResult('error', '', '⚠', '', '',

                'Manual ID lookup requires internet connection.', 2500);

            setTimeout(resumeScanning, 2500);

            return;

        }



        const entry = entries.find(e => e.qr_token === token && e.session_id === currentSessionId);



        if (!entry) {

            showResult('error', '', '✗', '', '', 'Not enrolled / invalid pass (offline).', 2000);

            addRecent('invalid', '', 'Offline: not enrolled');

            setTimeout(resumeScanning, 2000);

            return;

        }



        if (entry.status === 'confirmed') {

            showResult('duplicate', '', '⟳', entry.student_name, entry.class_name,

                'Already confirmed (offline cache).', 2000);

            addRecent('duplicate', entry.student_name, 'Already confirmed');

            setTimeout(resumeScanning, 2000);

            return;

        }



        // Valid — queue offline confirmation

        confirmedCount++;

        updateStats();



        const offlineRecord = {

            token:       token,

            session_id:  currentSessionId,

            scanned_at:  Math.floor(Date.now() / 1000),

            device_info: navigator.userAgent.slice(0, 255),

        };

        await queueOfflineConfirmation(offlineRecord);



        // Optimistically mark as confirmed in cached entries

        entry.status = 'confirmed';



        const pending = await countPending();

        showResult(

            'success',

            entry.photo_data_url || '',

            '✓ CONFIRMED',

            entry.student_name,

            entry.class_name,

            'Saved offline — will sync on reconnect (' + pending + ' queued)',

            3000

        );

        addRecent('success', entry.student_name, 'Offline ✓');

        setTimeout(resumeScanning, 3000);

    }



    function processResult(data) {

        if (data.success) {

            confirmedCount++;

            updateStats();

            const photoSrc = data.photo_url ? '../' + data.photo_url : '';

            showResult(

                'success', photoSrc,

                '✓ CONFIRMED',

                data.student_name,

                data.class_name,

                '',

                3000

            );

            addRecent('success', data.student_name, data.session_name);

        } else if (data.code === 'already_confirmed') {

            showResult('duplicate', '', '⟳', data.student_name || '', data.class_name || '', data.message, 2000);

            addRecent('duplicate', data.student_name || 'Unknown', data.message);

        } else if (data.code === 'not_enrolled') {

            showResult('not_enrolled', '', '✗', '', '', data.message, 0, true);

            addRecent('invalid', 'Unknown', 'Not enrolled');

        } else {

            showResult('error', '', '✗', '', '', data.message || 'Invalid canteen pass.', 2000);

            addRecent('invalid', '', data.message || 'Invalid');

        }

    }



    function showResult(type, photoSrc, icon, name, cls, msg, autoDismissMs = 0, showOverride = false) {

        resultOverlay.className = type;

        resultIcon.textContent  = icon;

        resultName.textContent  = name;

        resultClass.textContent = cls;

        resultMsg.textContent   = msg;



        if (photoSrc) {

            resultPhoto.src     = photoSrc;

            resultPhoto.style.display = 'block';

            resultIcon.style.display  = 'none';

        } else {

            resultPhoto.style.display = 'none';

            resultIcon.style.display  = 'block';

        }



        if (manualOverrideBtn) {

            manualOverrideBtn.style.display = showOverride ? 'inline-block' : 'none';

        }



        resultOverlay.style.display = 'flex';

        scanning = false;



        if (autoDismissMs > 0) {

            setTimeout(resumeScanning, autoDismissMs);

        }

    }



    function resumeScanning() {

        resultOverlay.style.display = 'none';

        scanning = true;

        lastScan = performance.now();

    }



    if (dismissBtn) dismissBtn.addEventListener('click', resumeScanning);



    // ── Manual entry ──────────────────────────────────────────────────────────

    if (manualConfirmBtn) {

        manualConfirmBtn.addEventListener('click', function () {

            const val = (manualInput.value || '').trim();

            if (!val) return;

            manualInput.value = '';

            // We send a special prefix so the server does a student_id lookup

            handleToken('__MANUAL__' + val);

        });

    }

    if (manualInput) {

        manualInput.addEventListener('keydown', function (e) {

            if (e.key === 'Enter') manualConfirmBtn.click();

        });

    }



    // ── Recent scans list ─────────────────────────────────────────────────────

    function addRecent(type, name, detail) {

        if (!recentList) return;

        const dotClass = { success: 'dot-success', duplicate: 'dot-duplicate', invalid: 'dot-invalid', override: 'dot-override' }[type] || 'dot-invalid';

        const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });

        const item = document.createElement('div');

        item.className = 'recent-item';

        item.innerHTML = `<span class="badge-dot ${dotClass}"></span>

            <span class="flex-fill">${escHtml(name || '—')}</span>

            <span class="text-white-50">${escHtml(detail || '')}</span>

            <span class="text-white-50 ms-2" style="font-size:.7rem">${now}</span>`;

        recentList.prepend(item);

        if (recentList.children.length > 20) recentList.lastElementChild.remove();

    }



    function escHtml(s) {

        return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');

    }



    // ── Manual override ───────────────────────────────────────────────────────

    const overrideModal = document.getElementById('overrideModal');

    if (manualOverrideBtn && overrideModal) {

        manualOverrideBtn.addEventListener('click', function () {

            resumeScanning();

            new bootstrap.Modal(overrideModal).show();

        });

    }



    const overrideSubmitBtn = document.getElementById('overrideSubmitBtn');

    if (overrideSubmitBtn) {

        overrideSubmitBtn.addEventListener('click', function () {

            const pin    = document.getElementById('overridePin').value.trim();

            const search = document.getElementById('overrideSearch').value.trim();

            const reason = document.getElementById('overrideReason').value;

            if (!pin || !search) {

                alert('PIN and student name/ID required.');

                return;

            }

            const fd = new FormData();

            fd.append('pin',        pin);

            fd.append('search',     search);

            fd.append('reason',     reason);

            fd.append('session_id', currentSessionId);



            fetch(OVERRIDE_URL, { method: 'POST', body: fd })

                .then(r => r.json())

                .then(data => {

                    bootstrap.Modal.getInstance(overrideModal).hide();

                    if (data.success) {

                        confirmedCount++;

                        updateStats();

                        showResult('success', '', '✓', data.student_name || 'Student', '', 'Manual override', 3000);

                        addRecent('override', data.student_name || 'Manual', 'Override');

                    } else {

                        alert(data.message || 'Override failed.');

                    }

                })

                .catch(() => alert('Network error.'));

        });

    }



    // ── Offline detection + sync trigger ─────────────────────────────────────

    function updateOnlineStatus() {

        if (offlineBadge) offlineBadge.style.display = navigator.onLine ? 'none' : 'inline';

    }

    window.addEventListener('online', function () {

        updateOnlineStatus();

        // Trigger sync via SW when back online

        if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {

            navigator.serviceWorker.controller.postMessage({ type: 'SYNC_NOW' });

        }

        // Also try Background Sync API

        if ('serviceWorker' in navigator) {

            navigator.serviceWorker.ready.then(reg => {

                if (reg.sync) reg.sync.register('canteen-confirmations').catch(() => {});

            });

        }

        countPending().then(n => {

            if (n > 0) addRecent('override', 'Sync', 'Syncing ' + n + ' offline item(s)…');

        });

    });

    window.addEventListener('offline', updateOnlineStatus);

    updateOnlineStatus();



    // ── SW message handler ────────────────────────────────────────────────────

    if ('serviceWorker' in navigator) {

        navigator.serviceWorker.addEventListener('message', function (e) {

            if (!e.data) return;

            if (e.data.type === 'PREFETCH_READY') {

                addRecent('override', 'Cache', 'Offline data ready: ' + e.data.count + ' passes');

            }

            if (e.data.type === 'SYNC_COMPLETE' && e.data.synced > 0) {

                addRecent('override', 'Sync', e.data.synced + ' offline scan(s) synced');

            }

        });

    }



    // ── Logout / lock ─────────────────────────────────────────────────────────

    const logoutLink = document.querySelector('a[href="scanner_login.php?logout=1"]');

    if (logoutLink) {

        logoutLink.addEventListener('click', function (e) {

            e.preventDefault();

            fetch('../ajax/canteen_scanner_logout.php', { method: 'POST' })

                .finally(() => { window.location.href = 'scanner_login.php'; });

        });

    }



    // ── Service Worker + morning prefetch ────────────────────────────────────

    if ('serviceWorker' in navigator) {

        const swUrl = 'scanner_sw.js?v=1';

        navigator.serviceWorker.register(swUrl, { scope: './' })

            .then(reg => {

                // Morning prefetch — download today's entitlement blob

                const swReady = reg.active || reg.waiting || reg.installing;

                function triggerPrefetch() {

                    if (reg.active) {

                        reg.active.postMessage({ type: 'PREFETCH', date: TODAY });

                    }

                }

                if (reg.active) {

                    triggerPrefetch();

                } else {

                    const worker = reg.waiting || reg.installing;

                    if (worker) {

                        worker.addEventListener('statechange', function () {

                            if (worker.state === 'activated') triggerPrefetch();

                        });

                    }

                }

            })

            .catch(err => console.warn('[Scanner] SW registration failed:', err));

    }



    // ── Fingerprint (WebAuthn) ────────────────────────────────────────────────

    const btnFP = document.getElementById('btnFingerprint');

    if (btnFP) {

        btnFP.addEventListener('click', async function () {

            if (!currentSessionId) { alert('Select a session first.'); return; }

            scanning = false;

            btnFP.classList.add('scanning-fp');

            btnFP.querySelector('span').textContent = 'Scanning…';



            // 1. Get challenge

            let challenge;

            try {

                const resp = await fetch('../ajax/canteen_webauthn_challenge.php',

                    { method: 'POST', credentials: 'same-origin' });

                const data = await resp.json();

                if (!data.success) throw new Error(data.message);

                challenge = data.challenge;

            } catch (err) {

                btnFP.classList.remove('scanning-fp');

                btnFP.querySelector('span').textContent = 'Fingerprint';

                showResult('error', '', '✗', '', '', 'Challenge error: ' + err.message, 2000);

                setTimeout(resumeScanning, 2000);

                return;

            }



            // 2. Assert fingerprint

            let credential;

            try {

                credential = await navigator.credentials.get({

                    publicKey: {

                        challenge: b64uDecode(challenge),

                        rpId: window.location.hostname,

                        userVerification: 'required',

                        authenticatorSelection: { authenticatorAttachment: 'cross-platform' },

                        timeout: 30000,

                    }

                });

            } catch (err) {

                btnFP.classList.remove('scanning-fp');

                btnFP.querySelector('span').textContent = 'Fingerprint';

                if (err.name !== 'NotAllowedError') {

                    showResult('error', '', '✗', '', '',

                        'Fingerprint not recognised — please use QR code instead.', 2000);

                    setTimeout(resumeScanning, 2000);

                } else {

                    resumeScanning();

                }

                return;

            }



            // 3. Verify on server

            const fd = new FormData();

            fd.append('credential_id',      b64uEncode(credential.rawId));

            fd.append('authenticator_data', b64uEncode(credential.response.authenticatorData));

            fd.append('client_data_json',   b64uEncode(credential.response.clientDataJSON));

            fd.append('signature',          b64uEncode(credential.response.signature));



            btnFP.classList.remove('scanning-fp');

            btnFP.querySelector('span').textContent = 'Fingerprint';



            fetch('../ajax/canteen_webauthn_verify.php',

                { method: 'POST', credentials: 'same-origin', body: fd })

                .then(r => r.json())

                .then(studentData => {

                    if (!studentData.success) {

                        showResult('error', '', '✗', '', '',

                            studentData.message || 'Fingerprint not recognised.', 2000);

                        setTimeout(resumeScanning, 2000);

                        return;

                    }

                    // Student identified — confirm their meal via QR-style token lookup

                    // We POST student_id + session_id for manual_override style confirmation

                    confirmByStudentId(studentData.student_id, studentData.student_name,

                        studentData.class_name, studentData.photo_url, 'fingerprint');

                })

                .catch(() => {

                    showResult('error', '', '✗', '', '', 'Network error.', 2000);

                    setTimeout(resumeScanning, 2000);

                });

        });

    }



    // ── Face scan (MediaPipe) ─────────────────────────────────────────────────

    const btnFaceScan = document.getElementById('btnFaceScan');

    const faceWrap    = document.getElementById('faceWrap');

    const btnCloseFace= document.getElementById('btnCloseFace');

    const faceVideo   = document.getElementById('faceVideo');

    const faceCanvas  = document.getElementById('faceCanvas');

    const faceStatus  = document.getElementById('faceStatusBanner');

    const faceLowLight= document.getElementById('faceLowLight');

    const faceConfOverlay = document.getElementById('faceConfirmOverlay');

    const faceConfPhoto   = document.getElementById('faceConfPhoto');

    const faceConfName    = document.getElementById('faceConfName');

    const faceConfClass   = document.getElementById('faceConfClass');

    const btnFaceYes  = document.getElementById('btnFaceYes');

    const btnFaceNo   = document.getElementById('btnFaceNo');



    let faceActive = false;

    let faceMeshInst = null;

    let faceCamInst  = null;

    let faceCachedDescriptors = null; // loaded from prefetch cache

    let pendingFaceMatch = null;      // { student_id, student_name, class_name, photo, confidence }

    let lightCheckFrame = 0;



    if (btnFaceScan && faceWrap) {

        btnFaceScan.addEventListener('click', async function () {

            if (!currentSessionId) { alert('Select a session first.'); return; }

            scanning = false;

            btnFaceScan.classList.add('scanning-face');



            // Load descriptors from cache

            faceCachedDescriptors = await loadFaceDescriptors();

            if (!faceCachedDescriptors || faceCachedDescriptors.length === 0) {

                btnFaceScan.classList.remove('scanning-face');

                showResult('error', '', '✗', '', '',

                    'No facial data cached. Load scanner online first to download face data.', 2500);

                setTimeout(resumeScanning, 2500);

                return;

            }



            faceWrap.style.display = 'block';

            faceActive = true;

            startFaceMesh();

        });

    }



    if (btnCloseFace) {

        btnCloseFace.addEventListener('click', stopFaceScan);

    }



    function stopFaceScan() {

        faceActive = false;

        if (faceCamInst)  { faceCamInst.stop();  faceCamInst  = null; }

        if (faceMeshInst) { faceMeshInst.close(); faceMeshInst = null; }

        if (faceVideo.srcObject) {

            faceVideo.srcObject.getTracks().forEach(t => t.stop());

            faceVideo.srcObject = null;

        }

        if (faceWrap)    faceWrap.style.display = 'none';

        if (btnFaceScan) btnFaceScan.classList.remove('scanning-face');

        faceConfOverlay.style.display = 'none';

        pendingFaceMatch = null;

        resumeScanning();

    }



    function startFaceMesh() {

        if (typeof FaceMesh === 'undefined') {

            stopFaceScan();

            showResult('error', '', '✗', '', '', 'MediaPipe not loaded.', 2000);

            setTimeout(resumeScanning, 2000);

            return;

        }

        faceMeshInst = new FaceMesh({

            locateFile: f => `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${f}`

        });

        faceMeshInst.setOptions({

            maxNumFaces: 1, refineLandmarks: false,

            minDetectionConfidence: 0.5, minTrackingConfidence: 0.5,

        });

        faceMeshInst.onResults(onFaceResults);



        faceCamInst = new Camera(faceVideo, {

            onFrame: async () => {

                if (faceActive && faceVideo.readyState >= 2) {

                    lightCheckFrame++;

                    if (lightCheckFrame % 20 === 0) checkFaceLighting();

                    await faceMeshInst.send({ image: faceVideo });

                }

            },

            width: 640, height: 480,

        });

        faceCamInst.start().catch(() => {

            stopFaceScan();

            showResult('error', '', '✗', '', '', 'Face camera failed to start.', 2000);

            setTimeout(resumeScanning, 2000);

        });

    }



    function onFaceResults(results) {

        if (!faceActive) return;

        if (!results.multiFaceLandmarks || results.multiFaceLandmarks.length === 0) {

            if (faceStatus) faceStatus.textContent = 'Looking for face…';

            clearFaceCanvas();

            return;

        }



        const landmarks = results.multiFaceLandmarks[0];

        drawFaceDots(landmarks);



        // Match against cached descriptors

        const match = matchFace(landmarks);

        if (!match) {

            if (faceStatus) faceStatus.textContent = 'No match — keep scanning…';

            return;

        }



        // Match found — pause and show confirm overlay (staff must tap Confirm)

        if (faceStatus) faceStatus.textContent = '✓ Face matched — confirm below';

        pendingFaceMatch = match;

        showFaceConfirm(match);

    }



    function matchFace(landmarks) {

        if (!faceCachedDescriptors) return null;

        const q = landmarks.map(lm => [lm.x, lm.y, lm.z]);

        let bestDist = Infinity, bestEntry = null;



        for (const entry of faceCachedDescriptors) {

            if (!entry.facial_descriptor) continue;

            const ref = entry.facial_descriptor;

            if (!Array.isArray(ref) || ref.length !== q.length) continue;



            let sum = 0;

            for (let i = 0; i < q.length; i++) {

                const dx = q[i][0] - (ref[i].x || ref[i][0] || 0);

                const dy = q[i][1] - (ref[i].y || ref[i][1] || 0);

                const dz = q[i][2] - (ref[i].z || ref[i][2] || 0);

                sum += dx*dx + dy*dy + dz*dz;

            }

            const dist = Math.sqrt(sum / q.length);

            if (dist < bestDist) { bestDist = dist; bestEntry = entry; }

        }



        if (!bestEntry || bestDist > 0.35) return null;

        const confidence = Math.max(0, Math.min(1, 1 - bestDist / 0.35));

        return { ...bestEntry, confidence };

    }



    function showFaceConfirm(match) {

        faceConfPhoto.src   = match.photo_data_url || '';

        faceConfPhoto.style.display = match.photo_data_url ? 'block' : 'none';

        faceConfName.textContent  = match.student_name || '';

        faceConfClass.textContent = match.class_name   || '';

        faceConfOverlay.style.display = 'flex';

    }



    if (btnFaceYes) {

        btnFaceYes.addEventListener('click', async function () {

            if (!pendingFaceMatch || !currentSessionId) return;

            btnFaceYes.disabled = true;

            btnFaceYes.innerHTML = '<i class="fas fa-circle-notch fa-spin me-1"></i>Confirming…';



            const fd = new FormData();

            fd.append('student_id',       pendingFaceMatch.student_id);

            fd.append('session_id',       currentSessionId);

            fd.append('confidence_score', pendingFaceMatch.confidence.toFixed(4));



            try {

                const resp = await fetch('../ajax/canteen_facial_confirm.php',

                    { method: 'POST', credentials: 'same-origin', body: fd });

                const data = await resp.json();

                faceConfOverlay.style.display = 'none';

                stopFaceScan();



                if (data.success) {

                    confirmedCount++;

                    updateStats();

                    const photoSrc = data.photo_url ? '../' + data.photo_url : '';

                    showResult('success', photoSrc, '✓ CONFIRMED',

                        data.student_name, data.class_name, '', 3000);

                    addRecent('success', data.student_name, 'Face scan ✓');

                } else if (data.code === 'already_confirmed') {

                    showResult('duplicate', '', '⟳', data.student_name || '', data.class_name || '', data.message, 2000);

                    addRecent('duplicate', data.student_name || '', 'Already confirmed');

                } else {

                    showResult('error', '', '✗', '', '', data.message || 'Confirmation failed.', 2000);

                }

                setTimeout(resumeScanning, data.success ? 3000 : 2000);

            } catch {

                faceConfOverlay.style.display = 'none';

                stopFaceScan();

                showResult('error', '', '✗', '', '', 'Network error.', 2000);

                setTimeout(resumeScanning, 2000);

            } finally {

                btnFaceYes.disabled = false;

                btnFaceYes.innerHTML = '<i class="fas fa-check me-1"></i>Confirm Meal';

            }

        });

    }



    if (btnFaceNo) {

        btnFaceNo.addEventListener('click', function () {

            faceConfOverlay.style.display = 'none';

            pendingFaceMatch = null;

            if (faceStatus) faceStatus.textContent = 'Continue scanning…';

        });

    }



    function checkFaceLighting() {

        if (!faceVideo.videoWidth) return;

        const cvs = document.createElement('canvas');

        cvs.width  = Math.floor(faceVideo.videoWidth  / 4);

        cvs.height = Math.floor(faceVideo.videoHeight / 4);

        const ctx  = cvs.getContext('2d');

        ctx.drawImage(faceVideo, 0, 0, cvs.width, cvs.height);

        const data = ctx.getImageData(0, 0, cvs.width, cvs.height).data;

        let sum = 0;

        for (let i = 0; i < data.length; i += 4) {

            sum += 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];

        }

        const avg = sum / (data.length / 4);

        if (faceLowLight) faceLowLight.style.display = avg < 60 ? 'block' : 'none';

    }



    function drawFaceDots(landmarks) {

        if (!faceCanvas || !faceVideo.videoWidth) return;

        faceCanvas.width  = faceVideo.videoWidth;

        faceCanvas.height = faceVideo.videoHeight;

        const ctx = faceCanvas.getContext('2d');

        ctx.clearRect(0, 0, faceCanvas.width, faceCanvas.height);

        ctx.fillStyle = 'rgba(76,175,80,0.7)';

        landmarks.forEach(lm => {

            ctx.beginPath();

            ctx.arc(lm.x * faceCanvas.width, lm.y * faceCanvas.height, 1.5, 0, Math.PI * 2);

            ctx.fill();

        });

    }



    function clearFaceCanvas() {

        if (faceCanvas) {

            const ctx = faceCanvas.getContext('2d');

            ctx.clearRect(0, 0, faceCanvas.width, faceCanvas.height);

        }

    }



    async function loadFaceDescriptors() {

        const cached = await getCachedEntries();

        if (!cached) return null;

        return cached.filter(e => e.facial_descriptor);

    }



    // ── Confirm by student_id (fingerprint/face identified — find & confirm entitlement) ──

    function confirmByStudentId(studentId, studentName, className, photoUrl, method) {

        const fd = new FormData();

        // Build a synthetic token using student_id+session for server lookup

        fd.append('student_id', studentId);

        fd.append('session_id', currentSessionId);

        fd.append('method',     method);



        fetch('../ajax/canteen_confirm_by_student.php',

            { method: 'POST', credentials: 'same-origin', body: fd })

            .then(r => r.json())

            .then(data => {

                if (data.success) {

                    confirmedCount++;

                    updateStats();

                    const photo = photoUrl ? '../' + photoUrl : '';

                    showResult('success', photo, '✓ CONFIRMED', studentName, className, '', 3000);

                    addRecent('success', studentName, method + ' ✓');

                } else if (data.code === 'already_confirmed') {

                    showResult('duplicate', '', '⟳', studentName, className, data.message, 2000);

                    addRecent('duplicate', studentName, data.message);

                } else {

                    showResult('error', '', '✗', '', '', data.message || 'Confirmation failed.', 2000);

                }

                setTimeout(resumeScanning, data.success ? 3000 : 2000);

            })

            .catch(() => {

                showResult('error', '', '✗', '', '', 'Network error.', 2000);

                setTimeout(resumeScanning, 2000);

            });

    }



    // ── Base64url helpers ─────────────────────────────────────────────────────

    function b64uDecode(str) {

        const b64 = str.replace(/-/g, '+').replace(/_/g, '/') + '=='.slice(0, (4 - str.length % 4) % 4);

        const bin = atob(b64);

        return Uint8Array.from(bin, c => c.charCodeAt(0));

    }



    function b64uEncode(buf) {

        const bytes = new Uint8Array(buf instanceof ArrayBuffer ? buf : buf.buffer || buf);

        let bin = '';

        bytes.forEach(b => { bin += String.fromCharCode(b); });

        return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

    }



    // ── Start ─────────────────────────────────────────────────────────────────

    startCamera();



})();

</script>

</body>

</html>

