// Injects a "Send to Tablet" button next to two different shapes of
// NewBook's own Check In button, on whichever pages carry them (see
// manifest.json's content_scripts.matches) — the popup covers single-booking
// pages independently of this, this is purely about inline injection:
// 1. Each row of the "due for arrival" list (bookings_lists_checkin).
// 2. The "This Booking is due to Arrive today. Do you want to go to
// Check-In?" reminder banner NewBook shows inline (confirmed 2026-09-01
// from a real sample — a `.newbook_message.plain` block, most likely on
// bookings_view/edit but the exact page isn't nailed down, hence the
// slightly broader matches list below rather than one exact page).
//
// Both shapes share the same "generic_button_1"-is-not-actually-unique
// gotcha (NewBook reuses that id on every instance of its own Check In
// button, confirmed 2026-09-01) — this script never relies on ids from
// NewBook's own markup for either shape, only class/attribute selectors
// scoped to each button's own container. Booking-ID extraction differs
// between the two shapes though: the list row carries it in a checkbox's
// `value` (see extractBookingIdFromRow), the banner button only carries it
// inside its `onclick` attribute's URL (see extractBookingIdFromOnclick) —
// there's no separate structured attribute for it there.
//
// MutationObserver pattern (watching for AJAX-refreshed content, not just
// initial page load) is the same approach chrome-newbook-assistant already
// had to use for this same NewBook UI (~/laptop-archive) — this system
// clearly refreshes content via AJAX (see the `via_ajax` tooltip classes in
// the row sample) without a full page reload, so a one-time scan on
// document_idle alone would miss content added/replaced afterward.
const PROCESSED_ATTR = 'data-tcp-processed';
function extractBookingIdFromRow(row) {
const checkbox = row.querySelector('input.booking_id_checkbox');
if (checkbox?.value) return checkbox.value;
// Fallback: class name carries it too (`booking_37117`) — confirmed from
// the sample row, kept as a fallback in case a future NewBook markup
// change drops the checkbox but keeps the class.
const match = row.className.match(/\bbooking_(\d+)\b/);
return match ? match[1] : null;
}
// The reminder banner's Check In button has no separate booking-ID
// attribute at all — only its onclick handler carries it, embedded in the
// NewBook URL it navigates to: load_url("https://.../bookings_checkin/37129")
function extractBookingIdFromOnclick(button) {
const onclick = button.getAttribute('onclick') || '';
const match = onclick.match(/bookings_checkin\/(\d+)/);
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
// textContent read on the link itself would double up with it. Grab just
// the first non-empty text child instead.
const link = row.querySelector('a.easytooltip[class*="booking_"]');
if (!link) return '';
for (const node of link.childNodes) {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
return node.textContent.trim();
}
}
return link.textContent.trim();
}
function findCheckInCell(row) {
const btn = row.querySelector('button[aria-label="Check In"]');
return btn ? btn.closest('td') : null;
}
// variant 'block' = full-width, stacked below the anchor (table cell — the
// list row context, where width is already constrained by the column).
// 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.className = variant === 'block' ? 'button most_width' : 'button';
btn.innerHTML = ' Tablet Push';
btn.style.cssText = `
${variant === 'block' ? 'margin-top: 6px;' : 'margin-right: 8px;'}
background: #be123c; border-color: #be123c; color: #fff;
`;
btn.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
handleSendClick(btn, bookingId, guestName);
});
return btn;
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
// Visual feedback is split two ways: a per-button icon swap for the simple
// pass/fail cases (no page real estate taken, self-explanatory at a glance),
// and the picker overlay below for anything that actually needs a human
// decision (no default configured, default busy, default offline). Detail
// text lives in the title attribute (hover tooltip) rather than a persistent
// inline block — keeps every variant (table-cell row, banner, nav-bar menu
// item) visually identical instead of needing per-container layout math.
const STATE_ICON = {
idle: 'fa-tablet-alt',
loading: 'fa-spinner fa-spin',
success: 'fa-check',
warning: 'fa-exclamation-triangle',
error: 'fa-exclamation-triangle',
};
const STATE_BG = {
idle: '#be123c',
loading: '#be123c',
success: '#166534',
warning: '#b45309',
error: '#dc2626',
};
const STATE_ICON_COLOR = {
success: '#86efac',
warning: '#fde68a',
error: '#fecaca',
};
//