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
|
|
@ -33,21 +33,12 @@ async function main() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!bookingId) {
|
||||
contentEl.innerHTML = `<p class="muted">Open a booking in NewBook to send it to a check-in tablet.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
contentEl.innerHTML = `
|
||||
<p class="booking">Booking <strong>#${bookingId}</strong></p>
|
||||
<select id="device"><option>Loading tablets…</option></select>
|
||||
<button class="primary" id="send" disabled>Send to Check-In Tablet</button>
|
||||
<div id="msg"></div>
|
||||
`;
|
||||
|
||||
const deviceSelect = document.getElementById('device');
|
||||
const sendBtn = document.getElementById('send');
|
||||
const msgEl = document.getElementById('msg');
|
||||
// Two 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>`;
|
||||
const tabletsEl = document.getElementById('tabletsSection');
|
||||
const bookingEl = document.getElementById('bookingSection');
|
||||
|
||||
let devices = [];
|
||||
try {
|
||||
|
|
@ -57,54 +48,155 @@ async function main() {
|
|||
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
|
||||
devices = await res.json();
|
||||
} catch (err) {
|
||||
deviceSelect.innerHTML = '<option>Could not load tablets</option>';
|
||||
setMsg(msgEl, `Could not reach tablet-checkin: ${err.message}`, 'error');
|
||||
tabletsEl.innerHTML = `<p class="muted">Could not reach tablet-checkin: ${escapeHtml(err.message)}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
renderTablets(tabletsEl, devices, baseUrl, apiKey);
|
||||
|
||||
if (!bookingId) {
|
||||
bookingEl.innerHTML = `<p class="muted">Open a booking in NewBook to send it to a check-in tablet.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
renderBooking(bookingEl, bookingId, devices, defaultDeviceId, baseUrl, apiKey);
|
||||
}
|
||||
|
||||
function renderTablets(el, devices, baseUrl, apiKey) {
|
||||
if (!devices.length) {
|
||||
deviceSelect.innerHTML = '<option>No tablets registered yet</option>';
|
||||
el.innerHTML = `<p class="muted">No tablets registered yet</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
deviceSelect.innerHTML = devices.map(d =>
|
||||
`<option value="${d.device_id}" ${d.status === 'busy' ? 'disabled' : ''}>
|
||||
${escapeHtml(d.name)}${d.status === 'busy' ? ' (busy)' : ''}
|
||||
</option>`
|
||||
).join('');
|
||||
// Pre-select this machine's configured default tablet — still just a
|
||||
// starting point, staff can pick a different one from the dropdown.
|
||||
el.innerHTML = `
|
||||
<p class="section-label">Tablets</p>
|
||||
<ul class="device-list">
|
||||
${devices.map(d => `
|
||||
<li class="device-row">
|
||||
<span class="device-name">${escapeHtml(d.name)}</span>
|
||||
<span class="device-status ${d.status}">${escapeHtml(d.status)}</span>
|
||||
${d.status === 'busy' ? `<button type="button" class="reset-btn" data-device="${d.device_id}">Reset</button>` : ''}
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
|
||||
el.querySelectorAll('.reset-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/staff/devices/${btn.dataset.device}/reset`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
|
||||
// Simplest way to get the tablet list (and any in-flight booking
|
||||
// auto-send, which depends on device status) back in sync — reload
|
||||
// the whole popup rather than hand-patching multiple sections.
|
||||
main();
|
||||
} catch (err) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Reset';
|
||||
alert(`Reset failed: ${err.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderBooking(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey) {
|
||||
const defaultDevice = devices.find(d => d.device_id === defaultDeviceId);
|
||||
|
||||
if (defaultDevice && defaultDevice.status !== 'busy') {
|
||||
// Straight to pushing, no extra click — matches the injected row/banner
|
||||
// buttons' behaviour (see content-script.js's handleSendClick). "Send to
|
||||
// a different tablet instead" stays one click away below in case the
|
||||
// default is wrong for this particular booking.
|
||||
el.innerHTML = `
|
||||
<p class="booking">Booking <strong>#${bookingId}</strong></p>
|
||||
<div id="msg" class="msg warn">Sending to ${escapeHtml(defaultDevice.name)}…</div>
|
||||
<a href="#" class="settings-link" id="changeTablet">Send to a different tablet instead</a>
|
||||
`;
|
||||
document.getElementById('changeTablet').addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
renderPicker(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey);
|
||||
});
|
||||
|
||||
sendPush(bookingId, defaultDevice.device_id, baseUrl, apiKey).then(result => {
|
||||
const msgEl = document.getElementById('msg');
|
||||
if (!msgEl) return; // "Send to a different tablet" was clicked before this resolved
|
||||
|
||||
if (result.ok) {
|
||||
setMsg(msgEl, 'Sent — the tablet should navigate now.', 'ok');
|
||||
} else if (result.status === 409) {
|
||||
// Default went busy in the moment between loading the popup and
|
||||
// sending — fall back to the manual picker rather than leaving the
|
||||
// guest on a dead-end message.
|
||||
renderPicker(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey, 'Default tablet is busy — choose another:');
|
||||
} else {
|
||||
setMsg(msgEl, `Failed: ${result.error}`, 'error');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
renderPicker(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey);
|
||||
}
|
||||
|
||||
function renderPicker(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey, note = '') {
|
||||
if (!devices.length) {
|
||||
el.innerHTML = `<p class="booking">Booking <strong>#${bookingId}</strong></p><p class="muted">No tablets registered yet</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<p class="booking">Booking <strong>#${bookingId}</strong></p>
|
||||
${note ? `<p class="msg warn">${escapeHtml(note)}</p>` : ''}
|
||||
<select id="device">
|
||||
${devices.map(d => `<option value="${d.device_id}" ${d.status === 'busy' ? 'disabled' : ''}>${escapeHtml(d.name)}${d.status === 'busy' ? ' (busy)' : ''}</option>`).join('')}
|
||||
</select>
|
||||
<button class="primary" id="send">Send to Check-In Tablet</button>
|
||||
<div id="msg"></div>
|
||||
`;
|
||||
|
||||
const deviceSelect = document.getElementById('device');
|
||||
const sendBtn = document.getElementById('send');
|
||||
const msgEl = document.getElementById('msg');
|
||||
// Setting .value to a disabled (busy) option is a no-op in every browser
|
||||
// that matters here, so a busy default just falls through to whatever the
|
||||
// browser's own first-enabled-option default would have been anyway.
|
||||
if (defaultDeviceId) deviceSelect.value = defaultDeviceId;
|
||||
sendBtn.disabled = false;
|
||||
|
||||
sendBtn.addEventListener('click', async () => {
|
||||
sendBtn.disabled = true;
|
||||
setMsg(msgEl, 'Sending…', 'warn');
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/staff/push`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
||||
body: JSON.stringify({ booking_id: bookingId, device_id: deviceSelect.value }),
|
||||
});
|
||||
const body = await res.json().catch(() => ({}));
|
||||
if (res.status === 409) {
|
||||
setMsg(msgEl, body.error || 'That tablet is busy with another guest right now.', 'warn');
|
||||
} else if (!res.ok) {
|
||||
throw new Error(body.error || `HTTP ${res.status}`);
|
||||
} else {
|
||||
setMsg(msgEl, 'Sent — the tablet should navigate now.', 'ok');
|
||||
}
|
||||
} catch (err) {
|
||||
setMsg(msgEl, `Failed: ${err.message}`, 'error');
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
const result = await sendPush(bookingId, deviceSelect.value, baseUrl, apiKey);
|
||||
if (result.ok) {
|
||||
setMsg(msgEl, 'Sent — the tablet should navigate now.', 'ok');
|
||||
} else if (result.status === 409) {
|
||||
setMsg(msgEl, result.error || 'That tablet is busy with another guest right now.', 'warn');
|
||||
} else {
|
||||
setMsg(msgEl, `Failed: ${result.error}`, 'error');
|
||||
}
|
||||
sendBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function sendPush(bookingId, deviceId, baseUrl, apiKey) {
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/staff/push`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
||||
body: JSON.stringify({ booking_id: bookingId, device_id: deviceId }),
|
||||
});
|
||||
const body = await res.json().catch(() => ({}));
|
||||
if (!res.ok) return { ok: false, status: res.status, error: body.error || `HTTP ${res.status}` };
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, status: 0, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
function setMsg(el, text, kind) {
|
||||
el.textContent = text;
|
||||
el.className = `msg ${kind}`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue