- setBookingCustomFields: use bookings_update's own custom_fields array (confirmed from the real legacy plugin code) instead of instance_custom_fields_set, which was only ever proven for a different, unrelated plugin - online_checkin_complete is now a configurable per-hotel custom field (online_checkin_complete_custom_field_id setting), not a bogus top-level bookings_update field NewBook would silently ignore - Fix field-name mismatches vs a real live NewBook booking response: booking_arrival/departure/length, guest address (street/city/state/ postcode/country, not a single field), phone/mobile (in contact_details, not top-level), and pickPrimaryGuest now checks primary_client (was checking fields that don't exist in the real API) - Bundle the standalone push extension into backend/extension-src/, add GET /api/extension/download (zips on request via archiver) and a download button on the Settings page - Remove newbook_hotel_id — vestigial from the legacy system's multi-hotel routing model, never actually reached NewBook's API, this stack is single-hotel-per-deployment so there's nothing to route between Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
111 lines
4.4 KiB
JavaScript
111 lines
4.4 KiB
JavaScript
// Background service worker — detects when the active tab is a NewBook
|
|
// booking page and shows a badge, so staff notice the extension has
|
|
// something to offer before even opening the popup. Tab-update/activated/
|
|
// history-state listeners and the booking-ID regex are adapted directly from
|
|
// chrome-newbook-assistant/background.js (~/laptop-archive) — that pattern
|
|
// is already proven working against real NewBook navigation (including its
|
|
// SPA-style in-page routing, hence the webNavigation listener), not
|
|
// reinvented here. See project_checkin memory's "Standalone push extension".
|
|
|
|
// Confirmed 2026-09-01 by the user directly (real NewBook page URLs):
|
|
// bookings_view/<id>, bookings_edit/<id>, bookings_checkin/<id>.
|
|
const BOOKING_URL_RE = /\/bookings_(?:view|edit|checkin)\/(\d+)/i;
|
|
const NEWBOOK_HOSTS = ['appeu.newbook.cloud', 'login.newbook.cloud'];
|
|
|
|
function isNewBookUrl(url) {
|
|
try {
|
|
const u = new URL(url);
|
|
return NEWBOOK_HOSTS.some(h => u.hostname === h);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function handleTabUpdate(tabId, url) {
|
|
if (!url || !isNewBookUrl(url)) {
|
|
await chrome.action.setBadgeText({ tabId, text: '' });
|
|
return;
|
|
}
|
|
|
|
const match = url.match(BOOKING_URL_RE);
|
|
if (match) {
|
|
const bookingId = match[1];
|
|
await chrome.storage.session.set({ [`bookingId:${tabId}`]: bookingId });
|
|
await chrome.action.setBadgeText({ tabId, text: '→' }); // →
|
|
await chrome.action.setBadgeBackgroundColor({ tabId, color: '#be123c' });
|
|
await chrome.action.setTitle({ tabId, title: `Send booking #${bookingId} to a check-in tablet` });
|
|
} else {
|
|
await chrome.storage.session.remove(`bookingId:${tabId}`);
|
|
await chrome.action.setBadgeText({ tabId, text: '' });
|
|
await chrome.action.setTitle({ tabId, title: 'Send to Check-In Tablet' });
|
|
}
|
|
}
|
|
|
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
if (changeInfo.status === 'complete' && tab.url) handleTabUpdate(tabId, tab.url);
|
|
});
|
|
|
|
chrome.tabs.onActivated.addListener(async ({ tabId }) => {
|
|
const tab = await chrome.tabs.get(tabId);
|
|
if (tab.url) handleTabUpdate(tabId, tab.url);
|
|
});
|
|
|
|
// NewBook's booking view is (per chrome-newbook-assistant's own findings) an
|
|
// SPA that can change URL without a full page load — this catches that case,
|
|
// same as the assistant already has to.
|
|
chrome.webNavigation.onHistoryStateUpdated.addListener(details => {
|
|
handleTabUpdate(details.tabId, details.url);
|
|
});
|
|
|
|
// ── Messages from content-script.js (bookings_lists_checkin row buttons) ───
|
|
// Content scripts route their actual network calls through here rather than
|
|
// fetching directly — keeps the baseUrl/apiKey read-and-fetch logic in one
|
|
// place instead of duplicated across popup.js and content-script.js.
|
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
if (message.action === 'tcpListDevices') {
|
|
listDevices().then(sendResponse);
|
|
return true; // keep the message channel open for the async response
|
|
}
|
|
if (message.action === 'tcpPush') {
|
|
pushBooking(message.bookingId, message.deviceId).then(sendResponse);
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
async function getConfig() {
|
|
const { baseUrl, apiKey } = await chrome.storage.local.get(['baseUrl', 'apiKey']);
|
|
return { baseUrl, apiKey };
|
|
}
|
|
|
|
async function listDevices() {
|
|
const { baseUrl, apiKey } = await getConfig();
|
|
if (!baseUrl || !apiKey) return { ok: false, error: 'not configured' };
|
|
try {
|
|
const res = await fetch(`${baseUrl}/api/staff/devices`, {
|
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
});
|
|
const body = await res.json().catch(() => ({}));
|
|
if (!res.ok) return { ok: false, error: body.error || `HTTP ${res.status}` };
|
|
return { ok: true, devices: body };
|
|
} catch (err) {
|
|
return { ok: false, error: err.message };
|
|
}
|
|
}
|
|
|
|
async function pushBooking(bookingId, deviceId) {
|
|
const { baseUrl, apiKey } = await getConfig();
|
|
if (!baseUrl || !apiKey) return { ok: false, error: 'not configured' };
|
|
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, error: body.error || `HTTP ${res.status}` };
|
|
return { ok: true };
|
|
} catch (err) {
|
|
return { ok: false, error: err.message };
|
|
}
|
|
}
|