A per-event queue board for a repair cafe. Customers sign items in on a shared tablet; volunteers drag repairs through Queue, In progress and Done on a dashboard, then export the day's records as CSV. Zero npm dependencies: node:http plus JSON files on disk, no build step. Categories and house rules are set in config.json. Events are deleted 7 days after creation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
2.9 KiB
JavaScript
97 lines
2.9 KiB
JavaScript
'use strict';
|
|
|
|
const slug = location.pathname.split('/').filter(Boolean)[0];
|
|
|
|
const form = document.getElementById('form');
|
|
const customer = document.getElementById('customer');
|
|
const item = document.getElementById('item');
|
|
const category = document.getElementById('category');
|
|
const problem = document.getElementById('problem');
|
|
const agree = document.getElementById('agree');
|
|
const submitBtn = document.getElementById('submit');
|
|
const savedBox = document.getElementById('saved');
|
|
const errorBox = document.getElementById('error');
|
|
const rules = document.getElementById('rules');
|
|
|
|
let categories = [];
|
|
let savedTimer = null;
|
|
|
|
document.getElementById('dashboard-link').href = `/${slug}/dashboard`;
|
|
|
|
async function init() {
|
|
const [config, event] = await Promise.all([
|
|
fetch('/api/config').then((r) => r.json()),
|
|
fetch(`/api/events/${slug}`).then((r) => r.json()),
|
|
]);
|
|
|
|
document.title = `Sign in a repair — ${event.name}`;
|
|
document.getElementById('event-name').textContent = event.name;
|
|
|
|
categories = config.categories.map((c) => c.name);
|
|
const list = document.getElementById('categories');
|
|
for (const name of categories) {
|
|
const option = document.createElement('option');
|
|
option.value = name;
|
|
list.append(option);
|
|
}
|
|
|
|
document.getElementById('rules-text').textContent = config.houseRules;
|
|
customer.focus();
|
|
}
|
|
|
|
/* house rules popup */
|
|
document.getElementById('rules-link').addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
rules.showModal();
|
|
});
|
|
document.getElementById('rules-ok').addEventListener('click', () => rules.close());
|
|
|
|
/* submit */
|
|
function showError(message) {
|
|
errorBox.textContent = message;
|
|
errorBox.hidden = false;
|
|
savedBox.hidden = true;
|
|
}
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
errorBox.hidden = true;
|
|
|
|
const chosen = category.value.trim().toLowerCase();
|
|
if (!categories.includes(chosen)) {
|
|
showError(`Please pick a category from the list: ${categories.join(', ')}.`);
|
|
category.focus();
|
|
return;
|
|
}
|
|
|
|
submitBtn.disabled = true;
|
|
try {
|
|
const res = await fetch(`/api/events/${slug}/repairs`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
customer: customer.value,
|
|
item: item.value,
|
|
category: chosen,
|
|
problem: problem.value,
|
|
agreedRules: agree.checked,
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Could not submit.');
|
|
|
|
// Clear everything and hand the tablet back, ready for the next person.
|
|
form.reset();
|
|
customer.focus();
|
|
savedBox.hidden = false;
|
|
clearTimeout(savedTimer);
|
|
savedTimer = setTimeout(() => { savedBox.hidden = true; }, 6000);
|
|
} catch (err) {
|
|
showError(err.message);
|
|
} finally {
|
|
submitBtn.disabled = false;
|
|
}
|
|
});
|
|
|
|
init().catch(() => showError('Could not load this event. Try reloading the page.'));
|