'use strict'; // ---- Family: one start page for the household: the announcement, today's events, chores, the shopping list, shared notes, chats and tasks. Each card loads on its own and // only shows if the person has that part of the hub, so one slow or missing piece never blanks the page. const fmCard = (id, title, href, sub) => `

${esc(title)}

${href ? `${esc(sub || 'Open')}` : ''}

Loading…

`; const fmBody = id => document.querySelector('#' + id + ' .fmb'); const fmErr = (id, e) => { const b = fmBody(id); if (b) b.innerHTML = `

${esc(e && e.message || 'Could not load this')}

`; }; function viewFamily() { const h = new Date().getHours(), greet = h < 5 ? 'Good night' : h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening', today = new Date().toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }); const cards = [['cal', allowed('calendar'), fmCard('fmcal', 'Today and tomorrow', '#/calendar', 'Calendar')], ['chores', allowed('chores'), fmCard('fmch', 'Chores today', '#/chores')], ['shop', allowed('recipes'), fmCard('fmsh', 'Shopping list', '#/recipes/shop')], ['notes', allowed('notes'), fmCard('fmnt', 'Family notes and lists', '#/notes')], ['chat', allowed('chat'), fmCard('fmct', 'Chat', '#/chat')], ['tasks', allowed('tasks'), fmCard('fmtk', 'Your tasks', '#/tasks')]].filter(c => c[1]); $('#view').innerHTML = `

${greet}${ME ? ', ' + esc(ME.name.split(' ')[0]) : ''}

${today}

${inboxBell()}
${cards.map(c => c[2]).join('') || '

Nothing here yet. An admin can turn on Calendar, Chores, Notes, Chat and more for you.

'}
`; inboxWire(); if (typeof chAnnounceCard === 'function') chAnnounceCard($('#annc')); const done = new Set(cards.map(c => c[0])), run = (k, f) => { if (done.has(k)) f().catch(e => fmErr({ cal: 'fmcal', chores: 'fmch', shop: 'fmsh', notes: 'fmnt', chat: 'fmct', tasks: 'fmtk' }[k], e)); }; run('cal', fmEvents); run('chores', fmChores); run('shop', fmShopping); run('notes', fmNotes); run('chat', fmChat); run('tasks', fmTasks); } async function fmEvents() { const d0 = new Date(), t0 = crYmd(d0), t1 = crShift(t0, 1), from = new Date(t0 + 'T00:00:00').getTime(), to = new Date(t1 + 'T23:59:59').getTime(); const r = await jfetch('/api/cal/events?' + new URLSearchParams({ from, to, fd: t0, td: t1 })), b = fmBody('fmcal'); if (!b) return; const day = (ymdS, e) => e.allDay ? e.start <= ymdS && e.end > ymdS : crYmd(new Date(e.start)) === ymdS; const section = (label, ymdS) => { const l = r.events.filter(e => day(ymdS, e)).sort((a, c) => (c.allDay - a.allDay) || (a.allDay ? 0 : a.start - c.start)); return `
${label}${l.map(e => `
${esc(e.title)}${e.allDay ? 'All day' : new Date(e.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
`).join('') || 'Nothing'}
`; }; b.innerHTML = section('Today', t0) + section('Tomorrow', t1); } async function fmChores() { const d = await crApi('/?date=' + crYmd(new Date())), b = fmBody('fmch'); if (!b) return; const me = d.me.id; const per = d.people.map(p => { const l = d.board.filter(i => i.user === p.id), dn = l.filter(i => i.done).length; return { p, l, dn }; }).filter(x => x.l.length), any = d.board.filter(i => i.user === null); const mine = d.board.filter(i => i.user === me || i.user === null).filter(i => !i.done); b.innerHTML = `${mine.length ? mine.slice(0, 5).map(i => ``).join('') : '

Nothing left for you today. 🎉

'} ${per.length ? `
${per.map(x => `
${esc(sgInitial(x.p.name))}${esc(x.p.name)} · ${x.dn} of ${x.l.length}
`).join('')}
` : ''}${any.length ? `${any.filter(i => !i.done).length} for anyone` : ''}`; b.querySelectorAll('.critem input').forEach(c => c.onchange = async () => { try { await crPost('/done', { chore: c.dataset.c, date: crYmd(new Date()), user: c.dataset.u || undefined }); toast('Nice work!'); fmChores(); } catch (e) { toast(e.message); c.checked = false; } }); } async function fmShopping() { const b0 = fmBody('fmsh'); const st = await jfetch('/api/recipes/status'); if (!st.enabled) { if (b0) b0.innerHTML = '

The shopping list is not set up yet.

'; return; } const l = await jfetch('/api/recipes/shopping'), b = fmBody('fmsh'); if (!b) return; const open = l.items.filter(i => !i.checked); b.innerHTML = `${open.slice(0, 8).map(i => ``).join('') || '

Nothing to buy.

'}${open.length > 8 ? `+ ${open.length - 8} more` : ''}
`; b.querySelectorAll('input[data-id]').forEach(c => c.onchange = async () => { try { await jfetch('/api/recipes/shopping/check', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: c.dataset.id, checked: true }) }); fmShopping(); } catch (e) { toast(e.message); c.checked = false; } }); b.querySelector('#fmshf').onsubmit = async e => { e.preventDefault(); const i = b.querySelector('#fmshi'), t = i.value.trim(); if (!t) return; try { await jfetch('/api/recipes/shopping/add', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: t }) }); fmShopping(); } catch (x) { toast(x.message); } }; } async function fmNotes() { const r = await jfetch('/api/notes'), b = fmBody('fmnt'); if (!b) return; const l = r.notes.filter(n => !n.archived && n.share !== 'me').slice(0, 4); b.innerHTML = l.map(n => `${esc(n.title || (n.type === 'list' ? 'Checklist' : 'Note'))}${n.pinned ? ' 📌' : ''}${n.type === 'list' ? ntDone(n) + ' of ' + n.items.length + ' done' + (n.items.find(i => !i.done) ? ' · next: ' + esc(n.items.find(i => !i.done).text) : '') : esc(n.body.slice(0, 70))}`).join('') || '

No shared notes yet.

'; } async function fmChat() { const r = await jfetch('/api/chat/convs'), b = fmBody('fmct'); if (!b) return; const l = r.convs.filter(c => !c.archived).slice(0, 4); b.innerHTML = l.map(c => `${c.type === 'group' ? icon('chat') : esc(sgInitial(c.name))}${esc(c.name)}${esc(c.preview || '')}${c.unread ? `${c.unread}` : ''}`).join('') || '

No chats yet.

'; } async function fmTasks() { const r = await jfetch('/api/tasks/upcoming?today=' + crYmd(new Date())), b = fmBody('fmtk'); if (!b) return; b.innerHTML = r.tasks.slice(0, 5).map(t => `${esc(t.title)}${t.due === crYmd(new Date()) ? 'Today' : esc(t.due)}`).join('') || '

Nothing due this week. 🎉

'; }