Family board: greets the family by name, cheers on finished chores with confetti, and says what it takes to reach the goal
build-container / build (push) Successful in 4m21s
build-container / build (push) Successful in 4m21s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6ba9992e55
commit
522bd64b46
5 files changed
+40
-7
No files matched your search
@@ -32,6 +32,13 @@ const ymdOk = s => /^\d{4}-\d{2}-\d{2}$/.test(s || '');
|
||||
const isKid = u => !!(u.kid && u.kid.on);
|
||||
const palette = ['#e0742b', '#2f9e6e', '#4f7cff', '#8a5cf0', '#d9453a', '#2aa6a0', '#e5a00d'];
|
||||
const onBoard = u => !u.disabled && u.board !== false; // the admin picks who is on the family board
|
||||
const NAME_FILE = () => path.join(process.env.DATA_DIR || path.join(__dirname, 'data'), 'family-name.json');
|
||||
const savedName = () => { try { return String(JSON.parse(fs.readFileSync(NAME_FILE(), 'utf8')).name || ''); } catch { return ''; } };
|
||||
/** What the board calls the family: the name an admin set, or "<surname> Family" when two or more people share a last name, else "family". */
|
||||
function familyName() {
|
||||
const s = savedName(); if (s) return s; const last = {}; for (const u of users.all().filter(onBoard)) { const w = String(u.name || '').trim().split(/\s+/); if (w.length > 1) { const k = w[w.length - 1]; last[k] = (last[k] || 0) + 1; } }
|
||||
const top = Object.entries(last).sort((a, b) => b[1] - a[1])[0]; return top && top[1] >= 2 ? top[0] + ' Family' : 'family';
|
||||
}
|
||||
const people = () => users.all().filter(onBoard).map((u, i) => ({ id: u.id, name: u.name, kid: isKid(u), pin: hasPin(u.id), pinLen: (db()[u.id] || {}).n || 0, color: palette[i % palette.length] }));
|
||||
|
||||
/** Check it is really this person: a child with no PIN just taps their name; everyone else needs their PIN. */
|
||||
@@ -48,7 +55,7 @@ function eventFields(title, b) {
|
||||
async function board(day) {
|
||||
const t1 = chores.addDays(day, 1), from = new Date(day + 'T00:00:00').getTime() - 864e5, to = new Date(t1 + 'T23:59:59').getTime() + 864e5;
|
||||
let events = []; try { events = cal.familyEvents({ from, to, fd: day, td: t1 }); } catch {}
|
||||
return { day, people: people(), announcement: chat.announcement(), events, chores: chores.board(day).filter(i => !i.user || onBoard(users.get(i.user) || {})).map(i => ({ chore: i.chore, title: i.title, points: i.points, user: i.user, userName: i.userName, done: !!i.done, doneBy: i.done ? i.done.byName : '', claimable: !!i.claimable, claimed: !!i.claimed })), week: (w => ({ from: w.from, tiers: w.tiers, people: w.people.filter(p => onBoard(users.get(p.id) || {})).map(p => ({ id: p.id, name: p.name, points: p.points, count: p.count, tiers: p.tiers, earned: p.earned, given: p.given, goal: p.goal })) }))(chores.week(day)), shopping: await recipes.shoppingOpen(), notes: notes.familyNotes(), tasks: tasks.familyOpen() };
|
||||
return { day, familyName: familyName(), people: people(), announcement: chat.announcement(), events, chores: chores.board(day).filter(i => !i.user || onBoard(users.get(i.user) || {})).map(i => ({ chore: i.chore, title: i.title, points: i.points, user: i.user, userName: i.userName, done: !!i.done, doneBy: i.done ? i.done.byName : '', claimable: !!i.claimable, claimed: !!i.claimed })), week: (w => ({ from: w.from, tiers: w.tiers, people: w.people.filter(p => onBoard(users.get(p.id) || {})).map(p => ({ id: p.id, name: p.name, points: p.points, count: p.count, tiers: p.tiers, earned: p.earned, given: p.given, goal: p.goal })) }))(chores.week(day)), shopping: await recipes.shoppingOpen(), notes: notes.familyNotes(), tasks: tasks.familyOpen() };
|
||||
}
|
||||
|
||||
async function handle(req, res) {
|
||||
@@ -62,6 +69,11 @@ async function handle(req, res) {
|
||||
const body = JSON.stringify({ chore: b.chore, date: ymdOk(b.date) ? b.date : undefined, undo: b.action === 'unclaim' || b.action === 'undo' || undefined }), r = Readable.from([Buffer.from(body)]); r.url = path; r.method = 'POST'; r.headers = {};
|
||||
return users.runAs(t, () => chores.handle(r, res)); // (as that person: all of the chore rules apply to them)
|
||||
}
|
||||
if (p === '/api/board/family' && req.method === 'GET') { if (!me) throw fail(403, 'Sign in first'); return json(res, 200, { name: savedName(), shown: familyName() }); }
|
||||
if (p === '/api/board/family' && req.method === 'POST') { // an admin sets what the family board calls the family
|
||||
if (!me || me.role !== 'admin') throw fail(403, 'Only an admin can change this'); const b = JSON.parse(await readBody(req, 1e3) || '{}'), name = String(b.name || '').replace(/[\u0000-\u001f<>]/g, ' ').trim().slice(0, 40);
|
||||
if (name) { fs.mkdirSync(path.dirname(NAME_FILE()), { recursive: true }); atomicWrite(NAME_FILE(), JSON.stringify({ name })); } else fs.rmSync(NAME_FILE(), { force: true }); return json(res, 200, { name, shown: familyName() });
|
||||
}
|
||||
if (p === '/api/board/goal' && req.method === 'POST') { // a child chooses the reward they are aiming for this week, from the shared screen
|
||||
if (!dev) throw fail(403, 'This is not a family device'); const b = JSON.parse(await readBody(req, 2e3) || '{}'), t = users.get(String(b.uid));
|
||||
if (!t || !onBoard(t)) throw fail(404, 'Person not found'); if (!users.can(t, 'chores')) throw fail(403, 'Chores are not turned on for that person'); identify(t, b.pin);
|
||||
|
||||
Reference in new issue
Block a user