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:
parent
d7fbdce04d
commit
ebf9875ca8
4 changed files with 239 additions and 3 deletions
|
|
@ -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>`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue