Family board: add to shopping, tasks, checklists and the Family calendar; record who added each
build-container / build (push) Successful in 4m27s

- + button on each board card: write it, tap your name (PIN unless a child), it is added as you
- Tasks, events, checklist items and shopping items remember who added them
- Names shown on the board and in Tasks, Notes and Calendar
- Tests and README updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-26 14:01:40 -04:00
1 parent 472e7bec7e
commit f87a218eea
13 files changed
+86 -27

No files matched your search

+9 -3
View File
@@ -48,7 +48,13 @@ async function shop(p, opt) {
let last; for (const b of ['/api/households/shopping', '/api/groups/shopping']) { try { const r = await mealie(b + p, opt); shopBase = b; return r; } catch (e) { last = e; if (e.status !== 404) throw e; } }
throw last;
}
const itemOut = i => ({ id: i.id, text: String(i.display || i.note || (i.food && i.food.name) || '').trim(), checked: !!i.checked, label: (i.label && i.label.name) || '', position: i.position || 0 });
// who put each item on the shopping list (Mealie does not keep that): remembered here by the item's words
const whoFile = () => path.join(process.env.DATA_DIR || path.join(__dirname, 'data'), 'shopping-by.json');
let whoMap = null; const whoAll = () => whoMap || (whoMap = (() => { try { return JSON.parse(fs.readFileSync(whoFile(), 'utf8')); } catch { return {}; } })());
const whoKey = t => String(t || '').toLowerCase().replace(/\s+/g, ' ').trim();
const rememberWho = (text, name) => { const m = whoAll(), k = whoKey(text); m[k] = { by: name, at: Date.now() }; const keys = Object.keys(m); if (keys.length > 500) for (const o of keys.sort((a, b) => m[a].at - m[b].at).slice(0, keys.length - 400)) delete m[o]; try { fs.mkdirSync(path.dirname(whoFile()), { recursive: true }); require('./atomic')(whoFile(), JSON.stringify(m)); } catch {} };
const itemOut0 = i => ({ id: i.id, text: String(i.display || i.note || (i.food && i.food.name) || '').trim(), checked: !!i.checked, label: (i.label && i.label.name) || '', position: i.position || 0 });
const itemOut = i => { const o = itemOut0(i); o.by = (whoAll()[whoKey(o.text)] || {}).by || ''; return o; };
async function mainList() { // the first shopping list (the one called "Shopping List" if there is more than one)
const l = await shop('/lists?perPage=50'), items = Array.isArray(l) ? l : l.items || [];
let pick = items.find(x => /shopping/i.test(x.name || '')) || items[0];
@@ -128,7 +134,7 @@ async function handle(req, res) {
}
if (p === '/shopping/add' && req.method === 'POST') {
const b = JSON.parse(await readBody(req, 1e4) || '{}'), text = String(b.text || '').trim().slice(0, 200); if (!text) throw fail(400, 'Type what to buy');
const list = await mainList(); await shop('/items', { method: 'POST', body: { shoppingListId: list.id, note: text, quantity: 1, checked: false } }); await sync(); return json(res, 200, { ok: true });
const list = await mainList(); await shop('/items', { method: 'POST', body: { shoppingListId: list.id, note: text, quantity: 1, checked: false } }); rememberWho(text, users.current().name); await sync(); return json(res, 200, { ok: true });
}
if (p === '/shopping/check' && req.method === 'POST') {
const b = JSON.parse(await readBody(req, 1e4) || '{}'); if (!idOk(b.id)) throw fail(400, 'Bad item');
@@ -148,5 +154,5 @@ async function handle(req, res) {
/** Recipes in Mealie that match the words (none when Mealie is not set up or does not answer). */
async function search(q) { if (!enabled()) return []; try { const j = await mealie(`/api/recipes?page=1&perPage=6&orderBy=name&orderDirection=asc&search=${encodeURIComponent(q)}`, { timeout: 6000 }); return (j.items || []).map(card); } catch { return []; } }
/** The open items on the shopping list, for the family board ([] when Mealie is not set up or does not answer). */
async function shoppingOpen() { if (!enabled()) return []; try { return (await readList()).items.filter(i => !i.checked).slice(0, 12).map(i => ({ id: i.id, text: i.text })); } catch { return []; } }
async function shoppingOpen() { if (!enabled()) return []; try { return (await readList()).items.filter(i => !i.checked).slice(0, 12).map(i => ({ id: i.id, text: i.text, by: i.by })); } catch { return []; } }
module.exports = { shoppingOpen, handle, enabled, minutes, full, ingredientText, search };