Family board: tap chores to take, finish or undo them; style number inputs
build-container / build (push) Successful in 4m42s
build-container / build (push) Successful in 4m42s
- Chores on the board are tappable; pick your name (PIN unless a child) and it happens as you - Up-for-grabs chores offer "I did it" or "Take it for later" - Number inputs (chore points, time limit) use the dark input style so they are readable - Tests and README updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
870c7129b9
commit
0df1ff5705
7 files changed
+55
-13
No files matched your search
@@ -5,6 +5,7 @@ const atomicWrite = require('./atomic');
|
||||
const users = require('./users'), auth = require('./auth'), audit = require('./audit'), devices = require('./devices');
|
||||
const cal = require('./cal'), chores = require('./chores'), notes = require('./notes'), tasks = require('./tasks'), recipes = require('./recipes'), chat = require('./chat');
|
||||
const { readBody } = require('./body');
|
||||
const { Readable } = require('stream');
|
||||
|
||||
const json = (res, code, o) => { res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(o)); };
|
||||
const fail = (code, message) => Object.assign(new Error(message), { code });
|
||||
@@ -28,23 +29,33 @@ const palette = ['#e0742b', '#2f9e6e', '#4f7cff', '#8a5cf0', '#d9453a', '#2aa6a0
|
||||
const onBoard = u => !u.disabled && u.board !== false; // the admin picks who is on the family board
|
||||
const people = () => users.all().filter(onBoard).map((u, i) => ({ id: u.id, name: u.name, kid: isKid(u), pin: hasPin(u.id), 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. */
|
||||
function identify(t, pin, req) {
|
||||
if (isKid(t) && !hasPin(t.id)) return;
|
||||
if (!hasPin(t.id)) throw fail(403, 'Set a PIN first (Settings, on your own device), or sign in with your passkey');
|
||||
if (!checkPin(t.id, pin)) { audit.log('family device: wrong PIN', { who: t.name }, req); throw fail(401, 'That PIN is not right'); }
|
||||
}
|
||||
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 => ({ title: i.title, points: i.points, user: i.user, userName: i.userName, done: !!i.done })), shopping: await recipes.shoppingOpen(), notes: notes.familyNotes(), tasks: tasks.familyOpen() };
|
||||
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 })), shopping: await recipes.shoppingOpen(), notes: notes.familyNotes(), tasks: tasks.familyOpen() };
|
||||
}
|
||||
|
||||
async function handle(req, res) {
|
||||
const u = new URL(req.url, 'http://x'), p = u.pathname, me = auth.currentUser(req), dev = auth.deviceOf(req);
|
||||
try {
|
||||
if (p === '/api/board' && req.method === 'GET') { if (!dev) throw fail(403, 'This is not a family device'); const day = u.searchParams.get('day'); return json(res, 200, await board(ymdOk(day) ? day : new Date().toISOString().slice(0, 10))); }
|
||||
if (p === '/api/board/chore' && req.method === 'POST') { // a person on the shared screen takes a chore, ticks it off, or undoes it, without signing in
|
||||
if (!dev) throw fail(403, 'This is not a family device'); const b = JSON.parse(await readBody(req, 5e3) || '{}'), 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);
|
||||
const path = { claim: '/api/chores/claim', unclaim: '/api/chores/claim', done: '/api/chores/done', undo: '/api/chores/done' }[b.action]; if (!path) throw fail(400, 'Unknown action');
|
||||
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/switch' && req.method === 'POST') {
|
||||
if (!dev) throw fail(403, 'This is not a family device'); const b = JSON.parse(await readBody(req, 5e3) || '{}'), t = users.get(String(b.uid));
|
||||
if (!t || !onBoard(t)) throw fail(404, 'Person not found');
|
||||
if (!isKid(t) || hasPin(t.id)) { // a child with no PIN just taps their name; everyone else needs theirs
|
||||
if (!hasPin(t.id)) throw fail(403, 'Set a PIN first (Settings, on your own device), or sign in with your passkey');
|
||||
if (!checkPin(t.id, b.pin)) { audit.log('family device: wrong PIN', { who: t.name }, req); throw fail(401, 'That PIN is not right'); }
|
||||
}
|
||||
identify(t, b.pin, req);
|
||||
auth.startSession(req, res, t); audit.log('family device: picked a profile', { who: t.name }, req); return json(res, 200, { ok: true });
|
||||
}
|
||||
if (p === '/api/device' && req.method === 'GET') { if (!me) throw fail(401, 'Not signed in'); return json(res, 200, { device: !!dev, pin: hasPin(me.id), devices: me.role === 'admin' ? devices.load() : undefined }); }
|
||||
|
||||
Reference in new issue
Block a user