Visitor sign in kiosk: multi-site, badge printing, WWCC expiry warnings, admin accounts with 2FA

This commit is contained in:
2026-09-01 15:45:47 +10:00
parent a7564b9033
commit cccba98d97
5 changed files with 92 additions and 22 deletions
+54 -14
View File
@@ -150,6 +150,9 @@ function resetState() {
$$('#app input').forEach((i) => {
i.value = '';
});
$$('#app select').forEach((sel) => {
sel.selectedIndex = 0;
});
$$('.choice').forEach((b) => b.setAttribute('aria-pressed', 'false'));
$('#check-number-field').hidden = true;
$('#check-continue').hidden = true;
@@ -182,16 +185,51 @@ tickClock();
/* --------------------------------------------------------------- hosts */
function renderHosts(listEl, searchValue, onPick) {
const term = searchValue.trim().toLowerCase();
/**
* Two ways to say who you are visiting, because neither suits everyone: a dropdown
* for someone who wants to browse a short list, and a filter box for someone who
* knows the name and would rather type it. The filter narrows both, so the two
* never disagree about what is on offer.
*/
function renderHosts(listEl, searchValue, onPick, selectEl = null) {
const term = String(searchValue || '').trim().toLowerCase();
const matches = term
? hosts.filter((h) => h.name.toLowerCase().includes(term) || (h.area || '').toLowerCase().includes(term))
? hosts.filter(
(h) => h.name.toLowerCase().includes(term) || (h.area || '').toLowerCase().includes(term)
)
: hosts;
const choose = (id, name) => {
state.hostId = Number(id);
state.hostName = name;
onPick();
};
if (selectEl) {
const placeholder =
matches.length === hosts.length
? `${hosts.length} ${hosts.length === 1 ? 'person' : 'people'} — choose a name`
: `${matches.length} ${matches.length === 1 ? 'match' : 'matches'}`;
selectEl.innerHTML =
`<option value="">${escapeHtml(placeholder)}</option>` +
matches
.map(
(h) =>
`<option value="${h.id}">${escapeHtml(h.name)}${h.area ? `${escapeHtml(h.area)}` : ''}</option>`
)
.join('');
selectEl.disabled = matches.length === 0;
selectEl.onchange = () => {
const picked = hosts.find((h) => h.id === Number(selectEl.value));
if (picked) choose(picked.id, picked.name);
};
}
if (!matches.length) {
listEl.innerHTML = `<p class="host-empty">No one matches that. Check the spelling, or ask the front desk.</p>`;
return;
}
listEl.innerHTML = matches
.slice(0, 60)
.map(
@@ -202,11 +240,7 @@ function renderHosts(listEl, searchValue, onPick) {
)
.join('');
listEl.querySelectorAll('[data-host-id]').forEach((btn) => {
btn.addEventListener('click', () => {
state.hostId = Number(btn.dataset.hostId);
state.hostName = btn.dataset.hostName;
onPick();
});
btn.addEventListener('click', () => choose(btn.dataset.hostId, btn.dataset.hostName));
});
}
@@ -413,7 +447,7 @@ async function loadSiteContext() {
change.textContent = siteConfig.site ? `Site: ${siteConfig.site.name} — change` : 'Choose site';
hosts = await api('/api/hosts');
renderHosts($('#host-list'), '', () => show('guest-contact'));
renderHosts($('#host-list'), '', () => show('guest-contact'), $('#host-select'));
}
$('#change-site').addEventListener('click', chooseSite);
@@ -448,7 +482,7 @@ $('[data-next="guest-name"]').addEventListener('click', () => {
});
$('#in-host-search').addEventListener('input', (e) =>
renderHosts($('#host-list'), e.target.value, () => show('guest-contact'))
renderHosts($('#host-list'), e.target.value, () => show('guest-contact'), $('#host-select'))
);
$('[data-next="guest-contact"]').addEventListener('click', () => {
@@ -522,8 +556,11 @@ $('#do-freq-auth').addEventListener('click', async () => {
state.hasStoredPhoto = Boolean(person.hasPhoto);
$('#freq-greeting').textContent = `Hi ${person.firstName}. Who are you here to see?`;
$('#in-freq-host-search').value = '';
renderHosts($('#freq-host-list'), '', () =>
state.hasStoredPhoto ? submitSignIn() : show('photo')
renderHosts(
$('#freq-host-list'),
'',
() => (state.hasStoredPhoto ? submitSignIn() : show('photo')),
$('#freq-host-select')
);
show('freq-host');
} catch (err) {
@@ -535,8 +572,11 @@ $('#do-freq-auth').addEventListener('click', async () => {
});
$('#in-freq-host-search').addEventListener('input', (e) =>
renderHosts($('#freq-host-list'), e.target.value, () =>
state.hasStoredPhoto ? submitSignIn() : show('photo')
renderHosts(
$('#freq-host-list'),
e.target.value,
() => (state.hasStoredPhoto ? submitSignIn() : show('photo')),
$('#freq-host-select')
)
);