Extension: Send to Tablet on the checkin page, popup auto-push, reset, new icon
- Inject Send to Tablet into the bookings_checkin page's top/bottom context menus (left of Confirm Check-In) and a dedicated banner above the tabs - Popup now auto-pushes to the default tablet instead of just pre-selecting it, with a terminal-key-authed reset route so reception can clear a stuck-busy tablet from the extension popup - Replace the extension icon with a person+checkmark glyph matching the app's own UserCheck icon and accent colour Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2b83f47556
commit
9f3129b8b5
10 changed files with 337 additions and 72 deletions
|
|
@ -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 = '<i class="far fa-tablet-alt fa-fw" aria-hidden="true"></i> <span>Tablet Push</span>';
|
||||
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 <li> — 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 — <li><a><div class="page_icon">
|
||||
// <i/></div><span/></a></li> — 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 = `
|
||||
<div class="page_icon"><i class="far fa-tablet-alt fa-fw" alt="" style="margin-right: 0; margin-left: 0px; vertical-align: middle; font-size: 24px;" aria-hidden="true"></i></div>
|
||||
<span>Tablet Push</span>
|
||||
`;
|
||||
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/<id> — 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 <li> 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 <li> 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 <li>s one at
|
||||
// a time — when one of those individual <li>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/<id>, 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 = `
|
||||
<i class="message_icon far fa-info-circle fa-fw" alt="" style="vertical-align: middle;font-size:16px;" aria-hidden="true"></i>
|
||||
<div class="nb-header">Information</div>
|
||||
<div class="indent"><div class="note_output"><div class="note_text">Prefer the guest to complete this on a tablet instead?</div></div></div>
|
||||
`;
|
||||
banner.querySelector('.note_text').appendChild(makeSendButton(bookingId, '', 'inline'));
|
||||
return banner;
|
||||
}
|
||||
|
||||
function processCheckinPageBanner() {
|
||||
const tablist = document.querySelector('ul[role="tablist"].ui-tabs-nav');
|
||||
if (!tablist || tablist.hasAttribute(PROCESSED_ATTR)) return;
|
||||
const bookingId = extractBookingIdFromLocation();
|
||||
if (!bookingId) return;
|
||||
|
||||
tablist.setAttribute(PROCESSED_ATTR, '1');
|
||||
tablist.insertAdjacentElement('beforebegin', buildTabletBanner(bookingId));
|
||||
}
|
||||
|
||||
function processRow(row) {
|
||||
if (row.hasAttribute(PROCESSED_ATTR)) return;
|
||||
const bookingId = extractBookingIdFromRow(row);
|
||||
|
|
@ -260,7 +389,10 @@ function processCheckinBanner(button) {
|
|||
if (!bookingId) return;
|
||||
|
||||
button.setAttribute(PROCESSED_ATTR, '1');
|
||||
button.insertAdjacentElement('afterend', makeSendButton(bookingId, '', 'inline'));
|
||||
// NewBook floats these buttons right, so the earlier element in DOM order
|
||||
// ends up rightmost visually — 'beforebegin' puts ours to the right of
|
||||
// Check In (not 'afterend', which would float it to the left instead).
|
||||
button.insertAdjacentElement('beforebegin', makeSendButton(bookingId, '', 'inline'));
|
||||
}
|
||||
|
||||
function scanForRows(root = document) {
|
||||
|
|
@ -275,6 +407,8 @@ function scanForBanners(root = document) {
|
|||
function scanAll(root = document) {
|
||||
scanForRows(root);
|
||||
scanForBanners(root);
|
||||
processCheckinMenus();
|
||||
processCheckinPageBanner();
|
||||
}
|
||||
|
||||
scanAll();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue