build-container / build (push) Successful in 4m9s
- Ask a parent on the lock screen also makes a request parents answer in Settings > Family - Parents choose how many minutes; the child's lock screen rechecks every few seconds - Notification links open Settings > Family - Test updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
60 lines
9.7 KiB
JavaScript
60 lines
9.7 KiB
JavaScript
'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) => `<button class="row" id="${id}"><span class="ic" style="background:#6a7280">${icon(ic)}</span><span class="grow"><b>${esc(title)}</b><small style="white-space:normal">${esc(sub)}</small></span>${icon('chev', 'chev')}</button>`;
|
||
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 = `<h2>You</h2><div class="list">${can('chat') ? famRow('fs-chat', 'chat', 'Chat', 'Quiet hours for phone alerts' + (admin ? ', and the family announcement' : '')) : ''}</div><div id="fs-dev"></div>
|
||
${admin && can('chores') ? `<h2>Chores</h2><div class="list">${famRow('fs-chores', 'check', 'Chores and weekly rewards', 'Make chores, choose who does them, set the points and the reward tiers')}</div>` : ''}
|
||
${admin ? '<h2>Children</h2><div id="fs-req"></div><p class="sub" style="margin:8px 0">What each child did this week, and Plex.</p><div class="list" id="fs-kids"><div class="row"><span class="grow"><small>Loading…</small></span></div></div><div class="list" style="margin-top:10px">' + famRow('fs-words', 'search', 'Blocked search words', 'A child\'s search only uses Simple English Wikipedia and a built-in list of blocked words. Add your own words here.') + '</div>' : ''}
|
||
${admin ? '<h2>Family board</h2><p class="sub" style="margin:0 0 8px">Who has a name tile on the shared family screen and shows in its chore progress.</p><div class="list" id="fs-board"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>' : ''}`;
|
||
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) { famRequests(box.querySelector('#fs-req')); famKids(box.querySelector('#fs-kids')); const w = box.querySelector('#fs-words'); if (w) w.onclick = famWords; }
|
||
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 => `<label class="row" style="cursor:pointer"><span class="ic" style="background:${u.role === 'admin' ? '#4f7cff' : '#6a7280'}">${esc(u.name.slice(0, 1).toUpperCase())}</span><span class="grow"><b>${esc(u.name)}</b>${u.kid ? '<small>Child</small>' : ''}</span><input type="checkbox" data-u="${esc(u.id)}" ${u.board === false ? '' : 'checked'}></label>`).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 = `<div class="row"><span class="grow"><small>${esc(e.message)}</small></span></div>`; }
|
||
}
|
||
|
||
// ---- Requests from children: what they asked for, with Yes / No.
|
||
async function famRequests(box) {
|
||
if (!box) return; let r; try { r = await jfetch('/api/kid/requests'); } catch { box.innerHTML = ''; return; }
|
||
const open = r.requests.filter(x => x.status === 'open'); if (!open.length) { box.innerHTML = ''; return; }
|
||
const when = t => new Date(t).toLocaleString([], { weekday: 'short', hour: 'numeric', minute: '2-digit' });
|
||
box.innerHTML = `<h2 style="margin-top:0">Asking you (${open.length})</h2><div class="list">${open.map(q => `<div class="row" style="flex-wrap:wrap"><span class="grow" style="min-width:200px"><b>${esc(q.userName)}: ${esc(q.text)}</b><small style="white-space:normal">${when(q.at)}${q.tab ? ' · wants ' + esc((TABS.find(t => t[0] === q.tab) || [0, q.tab])[1]) : ''}${q.minutes ? ' · +' + q.minutes + ' min' : ''}</small></span>${q.time ? [15, 30, 60].map(m => `<button class="btn" data-q="${esc(q.id)}" data-y="1" data-m="${m}">+${m === 60 ? '1 h' : m + ' min'}</button>`).join('') : `<button class="btn" data-q="${esc(q.id)}" data-y="1">Yes</button>`}<button class="btn ghost" data-q="${esc(q.id)}">No</button></div>`).join('')}</div>`;
|
||
box.querySelectorAll('[data-q]').forEach(b => b.onclick = async () => { const yes = !!b.dataset.y; try { await jfetch('/api/kid/requests/' + b.dataset.q, { method: 'POST', body: JSON.stringify({ approve: yes, minutes: b.dataset.m ? +b.dataset.m : undefined }) }); toast(yes ? 'Said yes' : 'Said no'); famRequests(box); } catch (e) { toast(e.message); } });
|
||
}
|
||
// ---- One row per child: their week (minutes by area, what they played, searched and tried), and Plex locking.
|
||
async function famKids(box) {
|
||
if (!box) return; let r; try { r = await jfetch('/api/users'); } catch (e) { box.innerHTML = `<div class="row"><span class="grow"><small>${esc(e.message)}</small></span></div>`; return; }
|
||
const kids = r.users.filter(u => u.kid && u.kid.on && !u.disabled);
|
||
box.innerHTML = kids.map(u => `<button class="row" data-k="${esc(u.id)}"><span class="ic" style="background:#2f9e6e">${esc(u.name.slice(0, 1).toUpperCase())}</span><span class="grow"><b>${esc(u.name)}</b><small>Activity and Plex</small></span>${icon('chev', 'chev')}</button>`).join('') || '<div class="row"><span class="grow"><small>No child accounts yet. Turn on <b>This is a child</b> for a person under Admin → People.</small></span></div>';
|
||
box.querySelectorAll('[data-k]').forEach(b => b.onclick = () => famChild(kids.find(u => u.id === b.dataset.k)));
|
||
}
|
||
async function famChild(u) {
|
||
const el = openSheet(u.name, 'Last 7 days', '<div id="fcb"><p class="empty">Loading…</p></div>'); let a, px;
|
||
try { [a, px] = await Promise.all([jfetch('/api/kid/activity?user=' + encodeURIComponent(u.id) + '&days=7'), jfetch('/api/kid/plex-lock?user=' + encodeURIComponent(u.id))]); } catch (e) { el.querySelector('#fcb').innerHTML = `<p class="err">${esc(e.message)}</p>`; return; }
|
||
const h = m => m >= 60 ? Math.floor(m / 60) + ' h ' + (m % 60) + ' min' : m + ' min', total = a.days.reduce((n, d) => n + d.minutes, 0), by = {}; a.days.forEach(d => Object.entries(d.areas).forEach(([k, m]) => { by[k] = (by[k] || 0) + m; }));
|
||
const icn = { play: '🎵', watch: '🎬', listen: '🎧', read: '📖', search: '🔎', signin: '🔑', chore: '✅', add: '➕', blocked: '⛔' }, max = Math.max(1, ...a.days.map(d => d.minutes));
|
||
const day = d => new Date(d + 'T12:00:00').toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
|
||
el.querySelector('#fcb').innerHTML = `<div class="list"><div class="row"><span class="grow"><b>${h(total)} in the app</b><small style="white-space:normal">${Object.entries(by).filter(([k, m]) => m > 0 && a.labels[k]).map(([k, m]) => esc(a.labels[k]) + ' ' + h(m)).join(' · ') || 'No time recorded yet'}</small></span></div>
|
||
${a.days.map(d => `<div class="row"><span class="grow"><small>${esc(day(d.day))}</small><div class="statbar"><i style="width:${Math.round(d.minutes / max * 100)}%"></i></div></span><small>${h(d.minutes)}</small></div>`).join('')}</div>
|
||
<h2 style="margin:14px 0 6px">Plex</h2><div class="list"><div class="row"><span class="grow"><b>${px.connected ? (px.profile ? 'Using Plex as ' + esc(px.profile) : 'Connected to Plex') : 'Not connected to Plex'}</b><small style="white-space:normal">${px.locked ? 'Locked to this profile: they cannot switch profiles or servers.' : 'They can switch Plex profiles if the other profile has no PIN. Lock it once you are on their managed profile.'}</small></span><button class="btn ${px.locked ? 'ghost' : ''}" id="fcpl">${px.locked ? 'Unlock' : 'Lock'}</button></div></div>
|
||
<h2 style="margin:14px 0 6px">What they did</h2><div class="list" style="max-height:38dvh;overflow:auto">${a.recent.map(e => `<div class="row"><span style="font-size:20px">${icn[e.kind] || '•'}</span><span class="grow"><b style="white-space:normal">${esc(e.title || e.kind)}</b><small>${esc(new Date(e.t).toLocaleString([], { weekday: 'short', hour: 'numeric', minute: '2-digit' }))}</small></span></div>`).join('') || '<p class="empty" style="margin:8px">Nothing yet.</p>'}</div>`;
|
||
el.querySelector('#fcpl').onclick = async () => { try { await jfetch('/api/kid/plex-lock', { method: 'POST', body: JSON.stringify({ user: u.id, locked: !px.locked }) }); toast(px.locked ? 'Unlocked' : 'Locked to this profile'); closeSheet(); famChild(u); } catch (e) { toast(e.message); } };
|
||
}
|
||
|
||
// ---- The words a parent blocks from a child's search (on top of a built-in list).
|
||
async function famWords() {
|
||
let r; try { r = await jfetch('/api/kidsearch/words'); } catch (e) { return toast(e.message); }
|
||
const el = openSheet('Blocked search words', 'A child searches Simple English Wikipedia only. Any search or result with one of these words (or a built-in blocked word) is hidden. One per line or separated by commas.', `<form id="fwf"><textarea id="fww" rows="6" style="width:100%" placeholder="a word you want blocked">${esc(r.words.join('\n'))}</textarea><div class="bar"><button type="button" class="btn ghost" id="fwx" style="flex:1;justify-content:center">Cancel</button><button class="btn" style="flex:1;justify-content:center">Save</button></div></form>`);
|
||
el.querySelector('#fwx').onclick = closeSheet; el.querySelector('#fwf').onsubmit = async e => { e.preventDefault(); try { await jfetch('/api/kidsearch/words', { method: 'POST', body: JSON.stringify({ words: el.querySelector('#fww').value }) }); closeSheet(); toast('Saved'); } catch (x) { toast(x.message); } };
|
||
}
|