Initial commit: repair cafe kanban board
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>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
'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.'));
|
||||
Reference in New Issue
Block a user