diff --git a/public/chores-view.js b/public/chores-view.js index c0561e3..94a4a81 100644 --- a/public/chores-view.js +++ b/public/chores-view.js @@ -19,6 +19,8 @@ function crWeekPerson(p) { ${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…

'; @@ -62,8 +64,8 @@ function crReward(r) {
${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(); viewChores(); } 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(); viewChores(); } catch (x) { el.querySelector('#rwe').textContent = x.message; } }; + 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); @@ -80,8 +82,8 @@ function crEdit(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'); viewChores(); } catch (e) { toast(e.message); } }); }; + 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'); viewChores(); } + 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; } }; } diff --git a/public/index.html b/public/index.html index ba166c1..4c90e4a 100644 --- a/public/index.html +++ b/public/index.html @@ -103,6 +103,7 @@ + diff --git a/public/settings-family.js b/public/settings-family.js new file mode 100644 index 0000000..3ee1840 --- /dev/null +++ b/public/settings-family.js @@ -0,0 +1,22 @@ +'use strict'; +// ---- Settings > Family: the family's settings in one place (chat, your PIN, chores and rewards, who is on the family board, shared screens). +// Each row opens the same sheet that used to live inside that part of the hub. +const famRow = (id, ic, title, sub) => ``; +async function famManageChores() { + try { cr.data = await crApi('/?date=' + crYmd(new Date())); } catch (e) { return toast(e.message); } + cr.afterEdit = famManageChores; crManage(); +} +async function famSettings(box) { + if (!box) return; const admin = ME && ME.role === 'admin', can = t => typeof allowed === 'function' && allowed(t); + box.innerHTML = `

You

${can('chat') ? famRow('fs-chat', 'chat', 'Chat', 'Quiet hours for phone alerts' + (admin ? ', and the family announcement' : '')) : ''}
+ ${admin && can('chores') ? `

Chores

${famRow('fs-chores', 'check', 'Chores and weekly rewards', 'Make chores, choose who does them, set the points and the reward tiers')}
` : ''} + ${admin ? '

Family board

Who has a name tile on the shared family screen and shows in its chore progress.

Loading…
' : ''}`; + const c = box.querySelector('#fs-chat'); if (c) c.onclick = () => chSettings(); + const k = box.querySelector('#fs-chores'); if (k) k.onclick = famManageChores; + if (typeof devSettings === 'function') devSettings(box.querySelector('#fs-dev')); + if (admin) try { + const r = await jfetch('/api/users'), b = box.querySelector('#fs-board'); if (!b) return; + b.innerHTML = r.users.filter(u => !u.disabled).map(u => ``).join(''); + b.querySelectorAll('input[data-u]').forEach(i => i.onchange = async () => { try { await jfetch('/api/users/' + encodeURIComponent(i.dataset.u), { method: 'PUT', body: JSON.stringify({ board: i.checked }) }); } catch (e) { toast(e.message); i.checked = !i.checked; } }); + } catch (e) { const b = box.querySelector('#fs-board'); if (b) b.innerHTML = `
${esc(e.message)}
`; } +} diff --git a/public/settings.js b/public/settings.js index 45d6298..c8a3b55 100644 --- a/public/settings.js +++ b/public/settings.js @@ -78,7 +78,7 @@ function viewSettings() { const isAdmin = ME && ME.role === 'admin'; const tabNames = ME ? TABS.filter(t => ME.tabs.includes(t[0]) && !['home', 'settings'].includes(t[0])).map(t => t[1]).join(', ') : ''; $('#view').innerHTML = `

Settings

-
${[['general', 'General'], ['connections', 'Connections'], ['security', 'Security'], ...(isAdmin ? [['admin', 'Admin']] : [])].map(([k, l]) => `${l}`).join('')}
+
${[['general', 'General'], ['connections', 'Connections'], ['family', 'Family'], ['security', 'Security'], ...(isAdmin ? [['admin', 'Admin']] : [])].map(([k, l]) => `${l}`).join('')}
${ME ? `

Signed in as

${esc(ME.name)}${isAdmin ? 'Admin Ā· can use everything and manage people' : 'Can use: ' + esc(tabNames || 'Home and Settings only')}
` : ''}

Signal alerts

How this computer gets your attention when a Signal message arrives (not while you are looking at that chat). Saved on this device only.

@@ -98,9 +98,9 @@ function viewSettings() {
+

Passkeys

Loading…
-

Recovery codes

Checking…One-time codes that let you add a new passkey if you lose every device. Keep them somewhere safe (a password manager or on paper).
${isAdmin ? `

People

Loading…
@@ -196,7 +196,7 @@ function viewSettings() { const el = $('#audit'); if (!el) return; el.innerHTML = events.length ? events.slice(0, 40).map(e => `
${esc(e.event)}${e.who ? ' Ā· ' + esc(e.who) : ''}${esc(new Date(e.t).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }))}${e.detail ? ' Ā· ' + esc(e.detail) : ''}${e.ip ? ' Ā· ' + esc(e.ip) : ''}
`).join('') : '
Nothing yet
'; }).catch(() => { const el = $('#audit'); if (el) el.innerHTML = '
Could not load
'; }); - if (typeof devSettings === 'function') devSettings($('#famdev')); + if (typeof famSettings === 'function') famSettings($('#famset')); $('#invpk').onclick = async () => { try { const { code, minutes } = await authPost('/api/auth/invite'); diff --git a/public/sw.js b/public/sw.js index e3dddf4..9a60e26 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ // App-shell cache only; Seafile/API traffic always goes to the network. -const CACHE = 'hub-v78'; +const CACHE = 'hub-v79'; const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];