From fbd1443e35074c60a53a3a2bc779739e2232290d Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 1 Sep 2026 20:30:18 +0000 Subject: [PATCH] Extension: inject Send-to-Tablet next to the arrival reminder banner too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New injection point on bookings_view pages — NewBook's own 'This Booking is due to Arrive today. Do you want to go to Check-In?' banner (.newbook_message.plain), confirmed from a real sample 2026-09-01. Shares the row-injection's button/panel logic rather than duplicating it; makeSendButton/togglePanel now take a variant ('block' for the list row's table cell, 'inline' for this banner's flowing text context). Booking ID extraction differs per shape: the list row has a checkbox value, this banner's Check In button only carries it inside its onclick attribute's URL, no separate structured attribute. Co-Authored-By: Claude Sonnet 5 --- backend/extension-src/content-script.js | 113 +++++++++++++++++------- backend/extension-src/manifest.json | 5 +- 2 files changed, 87 insertions(+), 31 deletions(-) diff --git a/backend/extension-src/content-script.js b/backend/extension-src/content-script.js index e46a587..b9538a2 100644 --- a/backend/extension-src/content-script.js +++ b/backend/extension-src/content-script.js @@ -1,29 +1,34 @@ -// Injects a "Send to Tablet" button into each row of NewBook's "due for -// arrival" list (bookings_lists_checkin), right under the page's own -// Check In button. Only runs on that one page (see manifest.json's -// content_scripts.matches) — the popup covers single-booking pages -// (bookings_view/edit/checkin) already. +// 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). // -// Row markup, booking-ID extraction, and the "generic_button_1"-is-not- -// actually-unique gotcha are all confirmed from a real sample row the user -// provided 2026-09-01 (see project_checkin memory) — not guessed. NewBook's -// own Check In button uses a non-unique id (`generic_button_1` repeated -// across every row), so this script never relies on ids from NewBook's own -// markup — only class/attribute selectors scoped per row, and its own -// injected elements get ids built from the booking ID -// (`tcp-btn-`) to stay collision-free and support idempotent -// re-injection checks. +// 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 rows, not just +// 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 table content via AJAX (see the `via_ajax` tooltip -// classes in the sample row) without a full page reload, so a one-time scan -// on document_idle alone would miss rows added/replaced afterward. +// 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 extractBookingId(row) { +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 @@ -33,6 +38,15 @@ function extractBookingId(row) { 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; +} + 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 @@ -53,13 +67,21 @@ function findCheckInCell(row) { return btn ? btn.closest('td') : null; } -function makeSendButton(bookingId, guestName) { +// 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). +function makeSendButton(bookingId, guestName, variant = 'block') { const btn = document.createElement('button'); - btn.id = `tcp-btn-${bookingId}`; + 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.style.cssText = ` - display: block; margin-top: 6px; width: 100%; + ${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; @@ -67,20 +89,26 @@ function makeSendButton(bookingId, guestName) { btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); - togglePanel(btn, bookingId, guestName); + togglePanel(btn, bookingId, guestName, variant); }); return btn; } let openPanel = null; -async function togglePanel(anchorBtn, bookingId, guestName) { +async function togglePanel(anchorBtn, bookingId, guestName, variant = 'block') { if (openPanel) { openPanel.remove(); openPanel = null; } const panel = document.createElement('div'); + // Always its own block below the anchor regardless of variant — an inline + // 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 width = variant === 'block' ? 'width: 100%;' : 'min-width: 200px; max-width: 280px;'; panel.style.cssText = ` - margin-top: 6px; padding: 8px; background: #f4f5f7; border: 1px solid #e4e8ee; - border-radius: 8px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; + display: block; margin-top: 6px; ${width} padding: 8px; background: #f4f5f7; + border: 1px solid #e4e8ee; border-radius: 8px; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-size: 12px; color: #1e293b; `; panel.textContent = 'Loading tablets…'; @@ -135,26 +163,51 @@ function escapeHtml(s) { function processRow(row) { if (row.hasAttribute(PROCESSED_ATTR)) return; - const bookingId = extractBookingId(row); + const bookingId = extractBookingIdFromRow(row); const cell = findCheckInCell(row); if (!bookingId || !cell) return; // not a real booking row, or no Check In button in it row.setAttribute(PROCESSED_ATTR, '1'); - cell.appendChild(makeSendButton(bookingId, extractGuestName(row))); + cell.appendChild(makeSendButton(bookingId, extractGuestName(row), 'block')); +} + +// The "due to Arrive today" reminder banner on bookings_view pages +// (confirmed 2026-09-01 — see the class comment at the top of this file). +// Scoped specifically to a Check In button sitting inside a +// .newbook_message.plain block, not just any button with that aria-label +// anywhere on the page, so this can't accidentally match something +// unrelated that happens to reuse the same label. +function processCheckinBanner(button) { + if (button.hasAttribute(PROCESSED_ATTR)) return; + const bookingId = extractBookingIdFromOnclick(button); + if (!bookingId) return; + + button.setAttribute(PROCESSED_ATTR, '1'); + button.insertAdjacentElement('afterend', makeSendButton(bookingId, '', 'inline')); } function scanForRows(root = document) { root.querySelectorAll('tr.checkbox_row').forEach(processRow); } -scanForRows(); +function scanForBanners(root = document) { + root.querySelectorAll('.newbook_message.plain .note_text button[aria-label="Check In"]') + .forEach(processCheckinBanner); +} + +function scanAll(root = document) { + scanForRows(root); + scanForBanners(root); +} + +scanAll(); const observer = new MutationObserver(mutations => { for (const m of mutations) { for (const node of m.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue; if (node.matches?.('tr.checkbox_row')) processRow(node); - else scanForRows(node); + else scanAll(node); } } }); diff --git a/backend/extension-src/manifest.json b/backend/extension-src/manifest.json index 6cdbc42..3e7cf7c 100644 --- a/backend/extension-src/manifest.json +++ b/backend/extension-src/manifest.json @@ -19,7 +19,10 @@ "content_scripts": [ { - "matches": ["https://appeu.newbook.cloud/bookings_lists_checkin*"], + "matches": [ + "https://appeu.newbook.cloud/bookings_lists_checkin*", + "https://appeu.newbook.cloud/bookings_view/*" + ], "js": ["content-script.js"], "run_at": "document_idle" }