Family board: rewards show only the chosen goal, with a Choose a reward pop-up for children without one
build-container / build (push) Successful in 3m44s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-29 08:45:44 -04:00
1 parent 0a33119807
commit 48c65779d3
4 files changed
+24 -3

No files matched your search

+15 -2
View File
@@ -9,7 +9,20 @@ function clock() { const d = new Date(); $('#clock').textContent = d.toLocaleTim
const card = (t, body, add, cls) => `<section class="card ${cls || ''}"><h2 style="display:flex;justify-content:space-between;align-items:center">${t}${add ? `<button class="pbtn" data-add="${add}" aria-label="Add">+</button>` : ''}</h2>${body}</section>`;
function weekCard(w) {
if (!w || !w.people.length) return none('No points yet');
return w.people.map(p => { const mine = w.tiers.filter(t => p.tiers.includes(t.id)), top = Math.max(1, ...mine.map(t => t.points), p.points), next = mine.find(t => t.points > p.points), goal = mine.find(t => t.id === p.goal), got = mine.filter(t => p.earned.includes(t.id)); return `<div style="margin-top:8px"><div style="display:flex;justify-content:space-between"><b>${esc(p.name)}</b><small class="sub">${p.points} pts · ${p.count} done</small></div><div class="bar2" style="position:relative">${mine.map(t => `<span style="position:absolute;top:0;left:${Math.round(t.points / top * 100)}%;width:2px;height:100%;background:var(--text);opacity:.5"></span>`).join('')}<i style="width:${Math.round(p.points / top * 100)}%"></i></div><small>${got.length ? '🎁 ' + esc(got.map(t => t.title).join(', ')) : ''}${goal ? (got.length ? ' · ' : '') + (p.points >= goal.points ? '🎯 ' + esc(goal.title) + ' reached!' : '🎯 ' + (goal.points - p.points) + ' more for ' + esc(goal.title)) : next ? (got.length ? ' · ' : '') + (next.points - p.points) + ' more for ' + esc(next.title) : ''}</small></div>`; }).join('');
return w.people.map(p => { const mine = w.tiers.filter(t => p.tiers.includes(t.id)), goal = mine.find(t => t.id === p.goal), top = Math.max(1, goal ? goal.points : 0, p.points); return `<div style="margin-top:8px"><div style="display:flex;justify-content:space-between"><b>${esc(p.name)}</b><small class="sub">${p.points} pts · ${p.count} done</small></div><div class="bar2" style="position:relative">${goal ? `<span style="position:absolute;top:0;left:${Math.min(100, Math.round(goal.points / top * 100))}%;width:2px;height:100%;background:var(--text);opacity:.5"></span>` : ''}<i style="width:${Math.round(p.points / top * 100)}%"></i></div>${goal ? `<small>${p.points >= goal.points ? '🎯 ' + esc(goal.title) + ' reached! 🎉' : '🎯 ' + (goal.points - p.points) + ' more for ' + esc(goal.title)}</small>` : mine.length ? `<button type="button" class="btn ghost" data-goalfor="${esc(p.id)}" style="margin-top:4px;min-height:40px">Choose a reward</button>` : ''}</div>`; }).join('');
}
// ---- Choose a reward to aim for: pick it from a pop-up, then PIN (unless a child with no PIN), and it is set as that person
function goalFlow(pid) {
const p = data.people.find(x => x.id === pid), w = data.week; if (!p || !w) return; const mine = w.tiers.filter(t => ((w.people.find(x => x.id === pid) || {}).tiers || []).includes(t.id));
const { box, close } = sheet(`<h3 style="margin:0 0 4px">${esc(p.name.split(' ')[0])}, what are you aiming for?</h3><p class="sub" style="margin:0 0 8px">Pick one reward for this week.</p><div class="list" style="text-align:left">${mine.map(t => `<button class="row" data-g="${esc(t.id)}" style="width:100%"><span class="grow"><b>${esc(t.title)}</b><small>${t.points} points in a week${t.minutes ? ' · +' + t.minutes + ' min screen time' : ''}</small></span></button>`).join('')}</div><div class="err" id="e"></div><p><button class="btn ghost" id="no" style="width:100%;justify-content:center">Cancel</button></p>`);
box.querySelector('#no').onclick = close;
const send = async (reward, pin) => { const r = await fetch('/api/board/goal', { method: 'POST', body: JSON.stringify({ uid: p.id, pin, reward, date: ymd(new Date()) }) }), j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(j.error || 'That did not work'); close(); await load(); toast('Goal chosen'); };
box.querySelectorAll('[data-g]').forEach(b => b.onclick = () => { const reward = b.dataset.g;
if (p.kid && !p.pin) return send(reward).catch(e => { box.querySelector('#e').textContent = e.message; });
if (!p.pin) { box.querySelector('#e').textContent = p.name + ' has not set a PIN yet. Set one in Settings on your own device.'; return; }
let pin = ''; box.innerHTML = `<h3 style="margin:0 0 4px">${esc(p.name)}, enter your PIN</h3><div class="dots" id="dots"></div><div class="err" id="e"></div><div class="pad">${[1, 2, 3, 4, 5, 6, 7, 8, 9, '⌫', 0, '✓'].map(k => `<button data-k="${k}">${k}</button>`).join('')}</div><p><button class="btn ghost" id="no" style="width:100%;justify-content:center">Cancel</button></p>`;
const paint = () => { box.querySelector('#dots').innerHTML = Array.from({ length: Math.max(4, pin.length) }, (_, i) => `<i class="${i < pin.length ? 'on' : ''}"></i>`).join(''); }; paint(); box.querySelector('#no').onclick = close;
box.querySelectorAll('.pad button').forEach(k2 => k2.onclick = async () => { let k = k2.dataset.k; box.querySelector('#e').textContent = ''; if (k === '⌫') pin = pin.slice(0, -1); else if (k !== '✓' && pin.length < 8) { pin += k; if (p.pinLen && pin.length === p.pinLen) k = '✓'; } if (k === '✓') { try { await send(reward, pin); return; } catch (e) { box.querySelector('#e').textContent = e.message; pin = ''; } } paint(); }); });
}
const none = t => `<p class="none">${t}</p>`;
@@ -33,7 +46,7 @@ function poolAll() {
const list = data.chores.filter(c => c.claimable && !c.done), { box, close } = sheet(`<h3 style="margin:0 0 4px">Up for grabs</h3><p class="sub">${list.length} chores. Tap one to pick it</p><div style="text-align:left;max-height:58dvh;overflow:auto">${list.map(c => `<button class="pill pool" data-c="${esc(c.chore)}" style="width:100%;justify-content:space-between;font-size:18px;padding:12px 14px;margin:4px 0"><span>${esc(c.title)}</span>${c.points ? `<small>${c.points} pts</small>` : ''}</button>`).join('')}</div><p><button class="btn ghost" id="no" style="width:100%;justify-content:center">Close</button></p>`);
box.querySelector('#no').onclick = close; box.querySelectorAll('.pill[data-c]').forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c && (c.user || '') === (b.dataset.u || ''))));
}
function wireChores() { document.querySelectorAll('[data-add]').forEach(b => b.onclick = () => addFlow(b.dataset.add)); const all = document.getElementById('poolall'); if (all) all.onclick = poolAll; document.querySelectorAll(".pill[data-c]").forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c && (c.user || '') === (b.dataset.u || '')))); }
function wireChores() { document.querySelectorAll('[data-add]').forEach(b => b.onclick = () => addFlow(b.dataset.add)); document.querySelectorAll('[data-goalfor]').forEach(b => b.onclick = () => goalFlow(b.dataset.goalfor)); const all = document.getElementById('poolall'); if (all) all.onclick = poolAll; document.querySelectorAll(".pill[data-c]").forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c && (c.user || '') === (b.dataset.u || '')))); }
async function load() { try { const r = await fetch('/api/board?day=' + ymd(new Date())); if (r.status === 403) { location.replace('/login'); return; } data = await r.json(); draw(); } catch {} }
function pick(p) {