import { isNewBookLoggedIn } from '../newbook-auth.js'; // Same regex as background.js — re-derived independently here from the live // tab URL each time the popup opens (rather than trusting chrome.storage // left over from the background script) so a stale badge state can never // show a booking that isn't actually the one currently open. Confirmed // 2026-09-01 by the user directly (real NewBook page URLs). const BOOKING_URL_RE = /\/bookings_(?:view|edit|checkin)\/(\d+)/i; const contentEl = document.getElementById('content'); const nbStatusEl = document.getElementById('nbStatus'); async function main() { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); const bookingMatch = tab?.url?.match(BOOKING_URL_RE); const bookingId = bookingMatch ? bookingMatch[1] : null; isNewBookLoggedIn().then(ok => { nbStatusEl.textContent = ok ? 'NewBook: signed in' : 'NewBook: not detected'; nbStatusEl.className = `nb-status ${ok ? 'ok' : 'no'}`; }); const { baseUrl, apiKey, defaultDeviceId } = await chrome.storage.local.get(['baseUrl', 'apiKey', 'defaultDeviceId']); if (!baseUrl || !apiKey) { contentEl.innerHTML = `

Not set up yet — add the tablet-checkin URL and a terminal API key first.

Open Settings `; document.getElementById('openSettings').addEventListener('click', e => { e.preventDefault(); chrome.runtime.openOptionsPage(); }); return; } // Two independent sections: tablet status + reset is always shown (not // gated on a booking being open) so reception can clear a stuck-busy // tablet from the terminal without needing to be on a booking page at all. contentEl.innerHTML = `
`; const tabletsEl = document.getElementById('tabletsSection'); const bookingEl = document.getElementById('bookingSection'); let devices = []; try { const res = await fetch(`${baseUrl}/api/staff/devices`, { headers: { Authorization: `Bearer ${apiKey}` }, }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`); devices = await res.json(); } catch (err) { tabletsEl.innerHTML = `

Could not reach tablet-checkin: ${escapeHtml(err.message)}

`; return; } renderTablets(tabletsEl, devices, baseUrl, apiKey); if (!bookingId) { bookingEl.innerHTML = `

Open a booking in NewBook to send it to a check-in tablet.

`; return; } renderBooking(bookingEl, bookingId, devices, defaultDeviceId, baseUrl, apiKey); } function renderTablets(el, devices, baseUrl, apiKey) { if (!devices.length) { el.innerHTML = `

No tablets registered yet

`; return; } el.innerHTML = `

Tablets

`; el.querySelectorAll('.reset-btn').forEach(btn => { btn.addEventListener('click', async () => { btn.disabled = true; btn.textContent = '…'; try { const res = await fetch(`${baseUrl}/api/staff/devices/${btn.dataset.device}/reset`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}` }, }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`); // Simplest way to get the tablet list (and any in-flight booking // auto-send, which depends on device status) back in sync — reload // the whole popup rather than hand-patching multiple sections. main(); } catch (err) { btn.disabled = false; btn.textContent = 'Reset'; alert(`Reset failed: ${err.message}`); } }); }); } function renderBooking(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey) { const defaultDevice = devices.find(d => d.device_id === defaultDeviceId); if (defaultDevice && defaultDevice.status !== 'busy') { // Straight to pushing, no extra click — matches the injected row/banner // buttons' behaviour (see content-script.js's handleSendClick). "Send to // a different tablet instead" stays one click away below in case the // default is wrong for this particular booking. el.innerHTML = `

Booking #${bookingId}

Sending to ${escapeHtml(defaultDevice.name)}…
Send to a different tablet instead `; document.getElementById('changeTablet').addEventListener('click', e => { e.preventDefault(); renderPicker(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey); }); sendPush(bookingId, defaultDevice.device_id, baseUrl, apiKey).then(result => { const msgEl = document.getElementById('msg'); if (!msgEl) return; // "Send to a different tablet" was clicked before this resolved if (result.ok) { setMsg(msgEl, 'Sent — the tablet should navigate now.', 'ok'); } else if (result.status === 409) { // Default went busy in the moment between loading the popup and // sending — fall back to the manual picker rather than leaving the // guest on a dead-end message. renderPicker(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey, 'Default tablet is busy — choose another:'); } else { setMsg(msgEl, `Failed: ${result.error}`, 'error'); } }); return; } renderPicker(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey); } function renderPicker(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey, note = '') { if (!devices.length) { el.innerHTML = `

Booking #${bookingId}

No tablets registered yet

`; return; } el.innerHTML = `

Booking #${bookingId}

${note ? `

${escapeHtml(note)}

` : ''}
`; const deviceSelect = document.getElementById('device'); const sendBtn = document.getElementById('send'); const msgEl = document.getElementById('msg'); // Setting .value to a disabled (busy) option is a no-op in every browser // that matters here, so a busy default just falls through to whatever the // browser's own first-enabled-option default would have been anyway. if (defaultDeviceId) deviceSelect.value = defaultDeviceId; sendBtn.addEventListener('click', async () => { sendBtn.disabled = true; setMsg(msgEl, 'Sending…', 'warn'); const result = await sendPush(bookingId, deviceSelect.value, baseUrl, apiKey); if (result.ok) { setMsg(msgEl, 'Sent — the tablet should navigate now.', 'ok'); } else if (result.status === 409) { setMsg(msgEl, result.error || 'That tablet is busy with another guest right now.', 'warn'); } else { setMsg(msgEl, `Failed: ${result.error}`, 'error'); } sendBtn.disabled = false; }); } async function sendPush(bookingId, deviceId, baseUrl, apiKey) { try { const res = await fetch(`${baseUrl}/api/staff/push`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ booking_id: bookingId, device_id: deviceId }), }); const body = await res.json().catch(() => ({})); if (!res.ok) return { ok: false, status: res.status, error: body.error || `HTTP ${res.status}` }; return { ok: true }; } catch (err) { return { ok: false, status: 0, error: err.message }; } } function setMsg(el, text, kind) { el.textContent = text; el.className = `msg ${kind}`; } function escapeHtml(s) { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; } main();