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; } // Three 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; // arrivals is likewise always shown — it's the alternative route to a push // for when reception isn't on a booking page either, picking the guest // from today's list instead of from NewBook itself. contentEl.innerHTML = `
`; const tabletsEl = document.getElementById('tabletsSection'); const arrivalsEl = document.getElementById('arrivalsSection'); 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); loadArrivals(arrivalsEl, devices, defaultDeviceId, 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); } async function loadArrivals(el, devices, defaultDeviceId, baseUrl, apiKey) { el.innerHTML = `

Today's Arrivals

Loading…

`; let arrivals; try { const res = await fetch(`${baseUrl}/api/staff/arrivals`, { headers: { Authorization: `Bearer ${apiKey}` }, }); if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`); arrivals = await res.json(); } catch (err) { el.innerHTML = `

Today's Arrivals

Could not load: ${escapeHtml(err.message)}

`; return; } renderArrivals(el, arrivals, devices, defaultDeviceId, baseUrl, apiKey); } function renderArrivals(el, arrivals, devices, defaultDeviceId, baseUrl, apiKey) { if (!arrivals.length) { el.innerHTML = `

Today's Arrivals

No arrivals today.

`; return; } el.innerHTML = `

Today's Arrivals

`; el.querySelectorAll('.push-btn').forEach(btn => { btn.addEventListener('click', () => handleArrivalPush(btn, devices, defaultDeviceId, baseUrl, apiKey)); }); } // Mirrors renderBooking's own fast-path/picker split (straight to the // default tablet when it's free, a manual picker when it isn't or there's // none), just scoped to this one row instead of the whole popup — several // of these can be mid-send at once across different arrivals. async function handleArrivalPush(btn, devices, defaultDeviceId, baseUrl, apiKey) { const row = btn.closest('.arrival-row'); const bookingId = btn.dataset.booking; const msgEl = row.querySelector('.arrival-msg'); const defaultDevice = devices.find(d => d.device_id === defaultDeviceId); if (defaultDevice && defaultDevice.status !== 'busy') { btn.disabled = true; btn.textContent = '…'; const result = await sendPush(bookingId, defaultDevice.device_id, baseUrl, apiKey); if (result.ok && result.delivered !== false) { btn.remove(); setMsg(msgEl, 'Sent', 'ok'); return; } if (result.ok) { setMsg(msgEl, 'Not connected — choose another:', 'warn'); } else if (result.status === 409) { setMsg(msgEl, 'Busy — choose another:', 'warn'); } else { btn.disabled = false; btn.textContent = 'Push'; setMsg(msgEl, `Failed: ${result.error}`, 'error'); return; } } showArrivalPicker(row, bookingId, devices, defaultDeviceId, baseUrl, apiKey); } function showArrivalPicker(row, bookingId, devices, defaultDeviceId, baseUrl, apiKey) { row.querySelector('.push-btn')?.remove(); const msgEl = row.querySelector('.arrival-msg'); if (!devices.length) { setMsg(msgEl, 'No tablets registered yet', 'error'); return; } const picker = document.createElement('div'); picker.className = 'arrival-picker'; picker.innerHTML = ` `; row.appendChild(picker); const select = picker.querySelector('select'); if (defaultDeviceId) select.value = defaultDeviceId; picker.querySelector('.send-small').addEventListener('click', async () => { const sendBtn = picker.querySelector('.send-small'); sendBtn.disabled = true; const result = await sendPush(bookingId, select.value, baseUrl, apiKey); if (result.ok && result.delivered !== false) { picker.remove(); setMsg(msgEl, 'Sent', 'ok'); } else if (result.ok) { setMsg(msgEl, 'Sent, but that tablet isn’t connected right now', 'warn'); sendBtn.disabled = false; } else { setMsg(msgEl, result.error || 'Failed', 'error'); sendBtn.disabled = false; } }); } 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 && result.delivered !== false) { setMsg(msgEl, 'Sent — the tablet should navigate now.', 'ok'); } else if (result.ok) { // Session created server-side but nothing's actually listening on // that device right now — same "needs a human decision" case as // busy, so offer the picker rather than a dead-end message. renderPicker(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey, 'Default tablet isn’t connected — choose another:'); } 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) { if (result.delivered === false) { setMsg(msgEl, 'Sent, but that tablet isn’t connected right now — it’ll pick this up if it reconnects.', 'warn'); } else { 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, delivered: body.delivered }; } 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();