diff --git a/backend/extension-src/content-script.js b/backend/extension-src/content-script.js
index 5334635..f322596 100644
--- a/backend/extension-src/content-script.js
+++ b/backend/extension-src/content-script.js
@@ -47,6 +47,17 @@ function extractBookingIdFromOnclick(button) {
return match ? match[1] : null;
}
+// Same URL shape as background.js/popup.js's BOOKING_URL_RE — re-derived here
+// too since this needs to resolve synchronously inside the MutationObserver
+// callback (menu items and bookings_checkin's own Confirm Check-In link
+// carry no separate booking-id attribute at all, unlike the row/banner
+// shapes above).
+const MENU_BOOKING_URL_RE = /\/bookings_(?:view|edit|checkin)\/(\d+)/i;
+function extractBookingIdFromLocation() {
+ const match = window.location.href.match(MENU_BOOKING_URL_RE);
+ return match ? match[1] : null;
+}
+
function extractGuestName(row) {
// The guest/company link's own text node — tooltip content is a sibling
// element (easytooltip_content), not part of the visible text, so a plain
@@ -72,19 +83,22 @@ function findCheckInCell(row) {
// variant 'inline' = sized to its own content, sitting next to the anchor
// (the reminder banner context, which flows inline with other text/buttons
// rather than living in a narrow fixed-width cell).
+// Reuses NewBook's own `.button` class (and `.most_width` for the row
+// variant) rather than hand-rolled CSS, so the injected button inherits
+// NewBook's real height/padding/border-radius/font and sits flush with its
+// own Check In button — only the colour is overridden, to keep it visually
+// distinct as a non-native action. The tablet icon is Font Awesome, already
+// loaded by NewBook itself for its own icons (e.g. fa-concierge-bell on
+// Check In), so no extra asset is needed.
function makeSendButton(bookingId, guestName, variant = 'block') {
const btn = document.createElement('button');
btn.id = `tcp-btn-${bookingId}-${variant}`;
btn.type = 'button';
- btn.textContent = 'Send to Tablet';
- const layout = variant === 'block'
- ? 'display: block; margin-top: 6px; width: 100%;'
- : 'display: inline-block; margin-left: 8px;';
+ btn.className = variant === 'block' ? 'button most_width' : 'button';
+ btn.innerHTML = ' Tablet Push';
btn.style.cssText = `
- ${layout}
- padding: 4px 8px; font-size: 12px; font-weight: 600;
- background: #be123c; color: #fff; border: none; border-radius: 6px;
- cursor: pointer; font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+ ${variant === 'block' ? 'margin-top: 6px;' : 'margin-right: 8px;'}
+ background: #be123c; border-color: #be123c; color: #fff;
`;
btn.addEventListener('click', e => {
e.preventDefault();
@@ -136,18 +150,38 @@ async function handleSendClick(anchorBtn, bookingId, guestName, variant) {
await togglePanel(anchorBtn, bookingId, guestName, variant);
}
+// The context-menu buttons (variant 'menu') live inside a horizontal nav
+// bar
— inserting a block-level status/panel 'afterend' there (like the
+// row/banner variants do) would stretch that menu item's row height instead
+// of floating over the page. Anchoring to the viewport via the button's own
+// bounding rect sidesteps needing the menu bar to be a positioned ancestor.
+function positionFloating(el, anchorBtn) {
+ const rect = anchorBtn.getBoundingClientRect();
+ el.style.position = 'fixed';
+ el.style.top = `${rect.bottom + 4}px`;
+ el.style.left = `${rect.left}px`;
+ el.style.zIndex = '99999';
+}
+
// Small text-only status line for the default-tablet fast path — no
// dropdown, nothing to pick, just feedback on what happened.
function showInlineStatus(anchorBtn, variant, text) {
if (openPanel) { openPanel.remove(); openPanel = null; }
const el = document.createElement('div');
const width = variant === 'block' ? 'width: 100%;' : '';
+ const floating = variant === 'menu';
el.style.cssText = `
- display: block; margin-top: 6px; ${width} font-size: 12px; color: #64748b;
+ display: block; ${floating ? '' : 'margin-top: 6px;'} ${width} font-size: 12px; color: #64748b;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+ ${floating ? 'background: #fff; padding: 6px 10px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.15);' : ''}
`;
el.textContent = text;
- anchorBtn.insertAdjacentElement('afterend', el);
+ if (floating) {
+ document.body.appendChild(el);
+ positionFloating(el, anchorBtn);
+ } else {
+ anchorBtn.insertAdjacentElement('afterend', el);
+ }
openPanel = el;
return el;
}
@@ -162,15 +196,22 @@ async function togglePanel(anchorBtn, bookingId, guestName, variant = 'block', n
// anchor (the banner button) has no full-width block context to size
// against naturally, so give it an explicit min-width instead of relying
// on parent width like the row-cell variant can.
+ const floating = variant === 'menu';
const width = variant === 'block' ? 'width: 100%;' : 'min-width: 200px; max-width: 280px;';
panel.style.cssText = `
- display: block; margin-top: 6px; ${width} padding: 8px; background: #f4f5f7;
+ display: block; ${floating ? '' : 'margin-top: 6px;'} ${width} padding: 8px; background: #f4f5f7;
border: 1px solid #e4e8ee; border-radius: 8px;
+ ${floating ? 'box-shadow: 0 2px 10px rgba(0,0,0,0.2);' : ''}
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 12px; color: #1e293b;
`;
panel.textContent = note ? `${note} loading…` : 'Loading tablets…';
- anchorBtn.insertAdjacentElement('afterend', panel);
+ if (floating) {
+ document.body.appendChild(panel);
+ positionFloating(panel, anchorBtn);
+ } else {
+ anchorBtn.insertAdjacentElement('afterend', panel);
+ }
openPanel = panel;
// A stale content script (injected before the extension was last
@@ -238,6 +279,94 @@ function escapeHtml(s) {
return div.innerHTML;
}
+// Context-menu entries use a completely different markup shape than the
+// .button-based row/banner injections above —
— so this builds that shape directly rather
+// than reusing makeSendButton. Click handling (default-tablet fast path,
+// picker fallback) is identical, via the 'menu' variant plumbed through
+// handleSendClick/showInlineStatus/togglePanel above.
+function makeMenuLink(bookingId) {
+ const li = document.createElement('li');
+ li.className = 'context tcp-menu-item';
+ const a = document.createElement('a');
+ a.href = '#';
+ a.innerHTML = `
+
+ Tablet Push
+ `;
+ a.addEventListener('click', e => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleSendClick(a, bookingId, '', 'menu');
+ });
+ li.appendChild(a);
+ return li;
+}
+
+// The top (#context-menu-header) and bottom (#context-menu-footer) nav bars
+// on bookings_checkin/ — NewBook clears each one (`.html("")`) and
+// rebuilds it from a fixed sequence of `.append()` calls on every load/AJAX
+// navigation, always ending with "Confirm Check-In" (identified by its
+// `a.checkin_button` class — the only stable marker across both nav bars and
+// the "More" dropdown they sometimes collapse into on narrow viewports).
+// Inserting immediately before that
is what the user asked for
+// ("both of those to the left of the default checkin button"). No
+// PROCESSED_ATTR here — the clear+rebuild wipes our injected
along with
+// everything else, so re-running this on every mutation is what makes it
+// self-healing rather than a bug; the previousElementSibling check below is
+// what keeps it from double-inserting within a single rebuild.
+function injectCheckinMenuButton(container) {
+ if (!container) return;
+ const checkinLink = container.querySelector('a.checkin_button');
+ if (!checkinLink) return;
+ const checkinLi = checkinLink.closest('li');
+ if (!checkinLi || checkinLi.previousElementSibling?.classList.contains('tcp-menu-item')) return;
+
+ const bookingId = extractBookingIdFromLocation();
+ if (!bookingId) return;
+
+ checkinLi.insertAdjacentElement('beforebegin', makeMenuLink(bookingId));
+}
+
+// Looked up by id directly (not scoped to whatever `root` the observer
+// happened to pass in) because NewBook's rebuild adds the menu
s one at
+// a time — when one of those individual
s is itself the mutation's
+// added node, it doesn't contain '#context-menu-header' as a descendant, so
+// a root-scoped querySelector would silently miss it.
+function processCheckinMenus() {
+ injectCheckinMenuButton(document.getElementById('context-menu-header'));
+ injectCheckinMenuButton(document.getElementById('context-menu-footer'));
+}
+
+// Dedicated banner on bookings_checkin/, purely for "send this guest to
+// a tablet instead" — distinct from processCheckinBanner above, which
+// detects and augments NewBook's own existing arrival reminder on
+// bookings_view. Reuses NewBook's own `.newbook_message.plain` class for
+// free styling consistency (same approach as makeSendButton reusing
+// `.button`), positioned directly above the Mandatory/Optional Information
+// tabs as requested.
+function buildTabletBanner(bookingId) {
+ const banner = document.createElement('div');
+ banner.className = 'newbook_message plain';
+ banner.innerHTML = `
+
+
Information
+
Prefer the guest to complete this on a tablet instead?
`;
return;
}
- deviceSelect.innerHTML = devices.map(d =>
- ``
- ).join('');
- // Pre-select this machine's configured default tablet — still just a
- // starting point, staff can pick a different one from the dropdown.
+ el.innerHTML = `
+