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>
67 lines
1.6 KiB
JavaScript
67 lines
1.6 KiB
JavaScript
'use strict';
|
||
|
||
const COLUMNS = [
|
||
'event',
|
||
'date/time added',
|
||
'customer',
|
||
'item',
|
||
'category',
|
||
'problem',
|
||
'status',
|
||
'repairer',
|
||
'outcome',
|
||
'tools/consumables',
|
||
'notes',
|
||
];
|
||
|
||
// Quote a field if it contains anything that would break the row, doubling any
|
||
// quotes inside it. Everything else goes out bare.
|
||
function escapeField(value) {
|
||
const text = value === null || value === undefined ? '' : String(value);
|
||
if (/[",\r\n]/.test(text)) return '"' + text.replace(/"/g, '""') + '"';
|
||
return text;
|
||
}
|
||
|
||
function row(fields) {
|
||
return fields.map(escapeField).join(',');
|
||
}
|
||
|
||
// "2026-08-15 17:14" in the server's timezone — spreadsheets read that as a
|
||
// date, which a raw UTC ISO string does not.
|
||
function localDateTime(iso) {
|
||
const d = new Date(iso);
|
||
if (Number.isNaN(d.getTime())) return iso;
|
||
const pad = (n) => String(n).padStart(2, '0');
|
||
return (
|
||
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
||
`${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||
);
|
||
}
|
||
|
||
function repairRows(event) {
|
||
return event.repairs.map((r) =>
|
||
row([
|
||
event.name,
|
||
localDateTime(r.createdAt),
|
||
r.customer,
|
||
r.item,
|
||
r.category,
|
||
r.problem,
|
||
r.status,
|
||
r.repairer,
|
||
r.outcome,
|
||
r.toolsUsed,
|
||
(r.notes || []).map((n) => n.text).join(' | '),
|
||
])
|
||
);
|
||
}
|
||
|
||
// A leading BOM so Excel reads it as UTF-8 rather than mangling accents.
|
||
function toCsv(events) {
|
||
const lines = [row(COLUMNS)];
|
||
for (const event of events) lines.push(...repairRows(event));
|
||
return '' + lines.join('\r\n') + '\r\n';
|
||
}
|
||
|
||
module.exports = { toCsv, escapeField, COLUMNS };
|