Extension: add Today's Arrivals list to popup, pushable independent of a NewBook booking page

Adds GET /api/staff/arrivals (NewBook bookings_list, same list_type: 'staying'
pattern room-planner/hvac/maintenance already use) and a popup section that
lists each arrival with its own Push button, falling back to a per-row device
picker on busy/offline/no-default — a second route to the same push flow,
separate from the content-script's injected NewBook page buttons.
This commit is contained in:
jtricerolph 2026-10-09 12:09:01 +00:00
parent d7fbdce04d
commit ebf9875ca8
4 changed files with 239 additions and 3 deletions

View file

@ -33,11 +33,15 @@ async function main() {
return;
}
// Two independent sections: tablet status + reset is always shown (not
// Three independent sections: tablet status + reset is always shown (not
// gated on a booking being open) so reception can clear a stuck-busy
// tablet from the terminal without needing to be on a booking page at all.
contentEl.innerHTML = `<div id="tabletsSection"></div><div id="bookingSection"></div>`;
// tablet from the terminal without needing to be on a booking page at all;
// arrivals is likewise always shown — it's the alternative route to a push
// for when reception isn't on a booking page either, picking the guest
// from today's list instead of from NewBook itself.
contentEl.innerHTML = `<div id="tabletsSection"></div><div id="arrivalsSection"></div><div id="bookingSection"></div>`;
const tabletsEl = document.getElementById('tabletsSection');
const arrivalsEl = document.getElementById('arrivalsSection');
const bookingEl = document.getElementById('bookingSection');
let devices = [];
@ -53,6 +57,7 @@ async function main() {
}
renderTablets(tabletsEl, devices, baseUrl, apiKey);
loadArrivals(arrivalsEl, devices, defaultDeviceId, baseUrl, apiKey);
if (!bookingId) {
bookingEl.innerHTML = `<p class="muted">Open a booking in NewBook to send it to a check-in tablet.</p>`;
@ -62,6 +67,123 @@ async function main() {
renderBooking(bookingEl, bookingId, devices, defaultDeviceId, baseUrl, apiKey);
}
async function loadArrivals(el, devices, defaultDeviceId, baseUrl, apiKey) {
el.innerHTML = `<p class="section-label">Today's Arrivals</p><p class="muted">Loading…</p>`;
let arrivals;
try {
const res = await fetch(`${baseUrl}/api/staff/arrivals`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
arrivals = await res.json();
} catch (err) {
el.innerHTML = `<p class="section-label">Today's Arrivals</p><p class="muted">Could not load: ${escapeHtml(err.message)}</p>`;
return;
}
renderArrivals(el, arrivals, devices, defaultDeviceId, baseUrl, apiKey);
}
function renderArrivals(el, arrivals, devices, defaultDeviceId, baseUrl, apiKey) {
if (!arrivals.length) {
el.innerHTML = `<p class="section-label">Today's Arrivals</p><p class="muted">No arrivals today.</p>`;
return;
}
el.innerHTML = `
<p class="section-label">Today's Arrivals</p>
<ul class="arrival-list">
${arrivals.map(a => `
<li class="arrival-row">
<div class="arrival-info">
<span class="arrival-name">${escapeHtml(a.guest_name)}</span>
${a.site_name ? `<span class="arrival-site">${escapeHtml(a.site_name)}</span>` : ''}
</div>
${a.checked_in
? `<span class="arrival-done">Checked in</span>`
: `<button type="button" class="push-btn" data-booking="${a.booking_id}">Push</button>`}
<div class="arrival-msg"></div>
</li>
`).join('')}
</ul>
`;
el.querySelectorAll('.push-btn').forEach(btn => {
btn.addEventListener('click', () => handleArrivalPush(btn, devices, defaultDeviceId, baseUrl, apiKey));
});
}
// Mirrors renderBooking's own fast-path/picker split (straight to the
// default tablet when it's free, a manual picker when it isn't or there's
// none), just scoped to this one row instead of the whole popup — several
// of these can be mid-send at once across different arrivals.
async function handleArrivalPush(btn, devices, defaultDeviceId, baseUrl, apiKey) {
const row = btn.closest('.arrival-row');
const bookingId = btn.dataset.booking;
const msgEl = row.querySelector('.arrival-msg');
const defaultDevice = devices.find(d => d.device_id === defaultDeviceId);
if (defaultDevice && defaultDevice.status !== 'busy') {
btn.disabled = true;
btn.textContent = '…';
const result = await sendPush(bookingId, defaultDevice.device_id, baseUrl, apiKey);
if (result.ok && result.delivered !== false) {
btn.remove();
setMsg(msgEl, 'Sent', 'ok');
return;
}
if (result.ok) {
setMsg(msgEl, 'Not connected — choose another:', 'warn');
} else if (result.status === 409) {
setMsg(msgEl, 'Busy — choose another:', 'warn');
} else {
btn.disabled = false;
btn.textContent = 'Push';
setMsg(msgEl, `Failed: ${result.error}`, 'error');
return;
}
}
showArrivalPicker(row, bookingId, devices, defaultDeviceId, baseUrl, apiKey);
}
function showArrivalPicker(row, bookingId, devices, defaultDeviceId, baseUrl, apiKey) {
row.querySelector('.push-btn')?.remove();
const msgEl = row.querySelector('.arrival-msg');
if (!devices.length) {
setMsg(msgEl, 'No tablets registered yet', 'error');
return;
}
const picker = document.createElement('div');
picker.className = 'arrival-picker';
picker.innerHTML = `
<select>
${devices.map(d => `<option value="${d.device_id}" ${d.status === 'busy' ? 'disabled' : ''}>${escapeHtml(d.name)}${d.status === 'busy' ? ' (busy)' : ''}</option>`).join('')}
</select>
<button type="button" class="send-small">Send</button>
`;
row.appendChild(picker);
const select = picker.querySelector('select');
if (defaultDeviceId) select.value = defaultDeviceId;
picker.querySelector('.send-small').addEventListener('click', async () => {
const sendBtn = picker.querySelector('.send-small');
sendBtn.disabled = true;
const result = await sendPush(bookingId, select.value, baseUrl, apiKey);
if (result.ok && result.delivered !== false) {
picker.remove();
setMsg(msgEl, 'Sent', 'ok');
} else if (result.ok) {
setMsg(msgEl, 'Sent, but that tablet isn’t connected right now', 'warn');
sendBtn.disabled = false;
} else {
setMsg(msgEl, result.error || 'Failed', 'error');
sendBtn.disabled = false;
}
});
}
function renderTablets(el, devices, baseUrl, apiKey) {
if (!devices.length) {
el.innerHTML = `<p class="muted">No tablets registered yet</p>`;