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)}
`;
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();