'use strict'; // ---- Chores: today's chores for you and the family, ticking them off, points for the week, and (admin) making and changing chores. See chores.js. const cr = { day: '', data: null, wk: '', week: null }; const crApi = (p, o) => jfetch('/api/chores' + p, o); const crPost = (p, b) => crApi(p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(b) }); const crYmd = d => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); const crShift = (s, n) => { const d = new Date(s + 'T12:00:00'); d.setDate(d.getDate() + n); return crYmd(d); }; const crDayLabel = s => s === crYmd(new Date()) ? 'Today' : s === crShift(crYmd(new Date()), -1) ? 'Yesterday' : s === crShift(crYmd(new Date()), 1) ? 'Tomorrow' : new Date(s + 'T12:00:00').toLocaleDateString([], { weekday: 'long', month: 'short', day: 'numeric' }); const CR_REPEAT = { daily: 'Every day', weekdays: 'Weekdays', weekly: 'Weekly', monthly: 'Monthly', once: 'One time' }; const CR_DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const crMonday = s => crShift(s, -((new Date(s + 'T12:00:00').getDay() + 6) % 7)); const crWeekLabel = w => { const f = new Date(w.from + 'T12:00:00'), t = new Date(w.to + 'T12:00:00'), o = { month: 'short', day: 'numeric' }; return (w.from === crMonday(crYmd(new Date())) ? 'This week · ' : '') + f.toLocaleDateString(undefined, o) + ' to ' + t.toLocaleDateString(undefined, o); }; /** One person's week: points, how far to the next reward, the rewards reached (an admin marks each as handed over), and the chores they did. */ function crWeekPerson(p) { const w = cr.week, admin = cr.data.me.admin, tiers = w.tiers.filter(t => p.tiers.includes(t.id)), top = Math.max(1, ...tiers.map(t => t.points), p.points), next = tiers.find(t => t.points > p.points); return `
${esc(sgInitial(p.name))}${esc(p.name)}
${tiers.map(t => ``).join('')}
${p.points} pts · ${p.count} done
${next ? `${next.points - p.points} more for ${esc(next.title)}` : tiers.length ? 'Every reward reached 🎉' : ''} ${p.earned.map(id => { const t = w.tiers.find(x => x.id === id), given = p.given.includes(id); return `
🎁${esc(t.title)}${given ? 'Given' : 'Earned, not given yet'}${admin ? `` : ''}
`; }).join('')} ${p.done.length ? `
Chores done (${p.done.length})${p.done.map(c => `
${esc(c.title)}${esc(new Date(c.date + 'T12:00:00').toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }))}${c.pts ? c.pts + ' pts' : ''}
`).join('')}
` : ''}
`; } /** After a change in the manage sheets: redraw the Chores page, or (when opened from Settings) reopen the manager. */ const crRefresh = () => location.hash.startsWith('#/chores') ? viewChores() : (cr.afterEdit && cr.afterEdit()); async function viewChores() { const v = $('#view'); cr.day = cr.day || crYmd(new Date()); v.innerHTML = '

Chores

Loading…

'; try { cr.wk = cr.wk || crYmd(new Date()); [cr.data, cr.week] = await Promise.all([crApi('/?date=' + cr.day), crApi('/week?date=' + cr.wk)]); } catch (e) { v.innerHTML = `

Chores

${esc(e.message)}

`; return; } if (!location.hash.startsWith('#/chores')) return; const d = cr.data, me = d.me.id, mine = d.board.filter(i => i.user === me), anyone = d.board.filter(i => i.user === null), others = d.people.filter(p => p.id !== me); const item = i => ``; const person = p => { const l = d.board.filter(i => i.user === p.id), done = l.filter(i => i.done).length; return `
${esc(sgInitial(p.name))}${esc(p.name)}${l.length ? done + ' of ' + l.length : 'nothing today'}
${l.length ? `
` : ''}
${l.map(item).join('')}
`; }; const max = Math.max(1, ...cr.sum.people.map(p => p.points)); v.innerHTML = `

Chores

${d.me.admin ? `` : ''}
${esc(crDayLabel(cr.day))}${cr.day !== crYmd(new Date()) ? '' : ''}

Mine

${mine.map(item).join('') || '

Nothing for you ' + (cr.day === crYmd(new Date()) ? 'today' : 'on this day') + '. 🎉

'}
${anyone.length ? `

Anyone can do

${anyone.map(item).join('')}
` : ''} ${others.length ? `

The family

${others.map(person).join('')}
` : ''}

Weekly goals

${esc(crWeekLabel(cr.week))}${cr.week.from !== crMonday(crYmd(new Date())) ? '' : ''}
${cr.week.tiers.length ? `

${cr.week.tiers.map(t => esc(t.points) + ' pts: ' + esc(t.title) + (Array.isArray(t.for) ? ' (' + esc(t.for.map(id => (cr.data.people.find(p => p.id === id) || { name: '?' }).name).join(', ')) + ')' : '')).join(' · ')}. Points start again every Monday.

` : (d.me.admin ? '

Add rewards in Manage (for example 20 points: pick the movie) and everyone works toward them each week.

' : '')}
${cr.week.people.map(crWeekPerson).join('') || '

No chores yet.

'}
`; v.querySelectorAll('.crclaim').forEach(b => b.onclick = async e => { e.preventDefault(); try { await crPost('/claim', { chore: b.dataset.c, date: cr.day, undo: !!b.dataset.undo }); if (!b.dataset.undo) toast('It is yours'); viewChores(); } catch (x) { toast(x.message); viewChores(); } }); v.querySelectorAll('.critem input').forEach(c => c.onchange = async () => { try { await crPost('/done', { chore: c.dataset.c, date: cr.day, user: c.dataset.u || undefined, undo: !c.checked }); if (c.checked) toast('Nice work!'); viewChores(); } catch (e) { toast(e.message); c.checked = !c.checked; } }); $('#crprev').onclick = () => { cr.day = crShift(cr.day, -1); viewChores(); }; $('#crnext').onclick = () => { cr.day = crShift(cr.day, 1); viewChores(); }; if ($('#crtoday')) $('#crtoday').onclick = () => { cr.day = crYmd(new Date()); viewChores(); }; $('#wkprev').onclick = () => { cr.wk = crShift(cr.week.from, -7); viewChores(); }; $('#wknext').onclick = () => { cr.wk = crShift(cr.week.from, 7); viewChores(); }; if ($('#wktoday')) $('#wktoday').onclick = () => { cr.wk = crYmd(new Date()); viewChores(); }; v.querySelectorAll('[data-give]').forEach(b => b.onclick = async () => { const [u, r] = b.dataset.give.split('|'); try { await crPost('/give', { week: cr.week.from, user: u, reward: r, undo: !!b.dataset.undo }); viewChores(); } catch (e) { toast(e.message); } }); if ($('#crman')) $('#crman').onclick = crManage; } // ---- making and changing chores (admin) function crManage() { const d = cr.data, who = c => c.assign === 'anyone' ? 'Anyone' : d.people.filter(p => c.assign.includes(p.id)).map(p => p.name).join(', '); const when = c => CR_REPEAT[c.repeat] + (c.repeat === 'weekly' && c.days && c.days.length ? ' · ' + c.days.map(x => CR_DAYS[x]).join(' ') : '') + (c.repeat === 'monthly' ? ' · day ' + (c.dom || 1) : '') + (c.repeat === 'once' ? ' · ' + c.date : ''); const el = openSheet('Manage chores', 'Make jobs for the family and choose who does them', `
${d.chores.map(c => ``).join('') || '

No chores yet.

'}

Weekly rewards

${d.rewards.map(r => ``).join('') || '

No rewards yet.

'}
`); el.querySelector('#rwnew').onclick = () => { closeSheet(); crReward(null); }; el.querySelectorAll('[data-rw]').forEach(b => b.onclick = () => { closeSheet(); crReward(d.rewards.find(r => r.id === b.dataset.rw)); }); el.querySelector('#crnew').onclick = () => { closeSheet(); crEdit(null); }; el.querySelectorAll('[data-e]').forEach(b => b.onclick = () => { closeSheet(); crEdit(d.chores.find(c => c.id === b.dataset.e)); }); } function crReward(r) { const el = openSheet(r ? 'Edit reward' : 'New reward', 'Anyone who reaches the points in a week earns it. Points start again each Monday.', `
${cr.data.people.map(p => ``).join('')}
${r ? '' : ''}
`); const rsync = () => { el.querySelector('#rwpp').hidden = el.querySelector('#rwall').checked; }; el.querySelector('#rwall').onchange = rsync; rsync(); el.querySelector('#rwx').onclick = closeSheet; if (r) el.querySelector('#rwdel').onclick = async () => { try { await crPost('/reward', { id: r.id, remove: true }); closeSheet(); crRefresh(); } catch (e) { toast(e.message); } }; el.querySelector('#rwf').onsubmit = async e => { e.preventDefault(); try { await crPost('/reward', { id: r && r.id, title: el.querySelector('#rwt').value, points: +el.querySelector('#rwp').value, for: el.querySelector('#rwall').checked ? 'all' : [...el.querySelectorAll('#rwpp [data-p]:checked')].map(i => i.dataset.p) }); closeSheet(); crRefresh(); } catch (x) { el.querySelector('#rwe').textContent = x.message; } }; } function crEdit(c) { const d = cr.data, x = c || { title: '', notes: '', repeat: 'daily', days: [], dom: 1, date: crYmd(new Date()), assign: 'anyone', points: 0, active: true }, chosen = new Set(x.assign === 'anyone' ? [] : x.assign); const el = openSheet(c ? 'Edit chore' : 'New chore', '', `
${d.people.map(p => ``).join('')}
${c ? '' : ''}
`); const sync = () => { const r = el.querySelector('#crr').value; el.querySelector('#crd').hidden = r !== 'weekly'; el.querySelector('#crm').hidden = r !== 'monthly'; el.querySelector('#cro').hidden = r !== 'once'; el.querySelector('#crp').hidden = el.querySelector('[name=crw]:checked').value !== 'people'; }; el.querySelector('#crr').onchange = sync; el.querySelectorAll('[name=crw]').forEach(r => r.onchange = sync); sync(); el.querySelectorAll('#crd [data-d]').forEach(a => a.onclick = e => { e.preventDefault(); a.classList.toggle('on'); }); el.querySelector('#crx').onclick = closeSheet; if (c) el.querySelector('#crdel').onclick = () => { closeSheet(); confirmSheet('Delete this chore?', c.title + ' and its history.', 'Delete', async () => { try { await crApi('/' + c.id, { method: 'DELETE' }); toast('Deleted'); crRefresh(); } catch (e) { toast(e.message); } }); }; el.querySelector('#cref').onsubmit = async e => { e.preventDefault(); const people = [...el.querySelectorAll('[data-p]:checked')].map(i => i.dataset.p); try { await crPost('/', { id: c && c.id, title: el.querySelector('#crt').value, notes: el.querySelector('#crn').value, repeat: el.querySelector('#crr').value, days: [...el.querySelectorAll('#crd .on')].map(a => +a.dataset.d), dom: +el.querySelector('#crdom').value, date: el.querySelector('#crdt').value, assign: el.querySelector('[name=crw]:checked').value === 'anyone' ? 'anyone' : people, points: +el.querySelector('#crpt').value, active: el.querySelector('#cra').checked }); closeSheet(); toast('Saved'); crRefresh(); } catch (x) { el.querySelector('#cre').textContent = x.message; } }; }