let children = []; let currentParent = null; function showToast(msg) { const toast = document.getElementById('toast'); toast.textContent = msg; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2200); } function fmtDate(iso) { if (!iso) return ''; return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); } async function boot() { try { const me = await api.get('/api/auth/me'); currentParent = me.parent; document.getElementById('householdName').textContent = me.household.name; document.getElementById('whoami').textContent = `Logged in as ${me.parent.name} (${me.parent.email})`; document.getElementById('inviteBtn').style.display = currentParent.isAdmin ? '' : 'none'; } catch { window.location.href = '/login.html'; return; } await Promise.all([loadParents(), loadChildren()]); } async function loadParents() { const { parents } = await api.get('/api/household'); const el = document.getElementById('parentsList'); el.innerHTML = parents.map((p) => parentCardHtml(p)).join(''); el.querySelectorAll('[data-action]').forEach((btn) => { btn.addEventListener('click', () => handleParentAction(btn.dataset.action, btn.dataset)); }); } function parentCardHtml(p) { const isSelf = p.id === currentParent.id; const badge = p.is_admin ? 'Admin' : ''; const adminActions = (currentParent.isAdmin && !isSelf) ? ` ` : ''; const selfActions = isSelf ? ` ` : ''; return `

${escapeHtml(p.name)} ${badge}

${escapeHtml(p.email)}
${adminActions}${selfActions}
`; } async function handleParentAction(action, ds) { try { if (action === 'promote' || action === 'demote') { await api.patch(`/api/household/parents/${ds.parent}`, { isAdmin: action === 'promote' }); showToast(action === 'promote' ? 'Promoted to admin' : 'Admin removed'); await loadParents(); } else if (action === 'remove-parent') { if (!confirm(`Remove ${ds.name} from the household?`)) return; await api.del(`/api/household/parents/${ds.parent}`); showToast('Parent removed'); await loadParents(); } else if (action === 'set-password') { document.getElementById('setPasswordParentId').value = ds.parent; document.getElementById('setPasswordParentName').textContent = ds.name; document.getElementById('newParentPasswordInput').value = ''; openModal('setParentPasswordModal'); } else if (action === 'change-own-password') { document.getElementById('currentPasswordInput').value = ''; document.getElementById('newOwnPasswordInput').value = ''; openModal('changeOwnPasswordModal'); } else if (action === 'leave') { if (!confirm('Leave this household? You will lose access immediately.')) return; await api.post('/api/household/leave'); window.location.href = '/login.html'; } } catch (err) { showToast(err.message); } } async function loadChildren() { const data = await api.get('/api/children'); children = data.children; const el = document.getElementById('childrenList'); el.innerHTML = ''; if (children.length === 0) { el.innerHTML = '
No children yet — add one to create their first calendar.
'; return; } for (const child of children) { const card = await renderChildCard(child); el.appendChild(card); } } async function renderChildCard(child) { const { calendars } = await api.get(`/api/children/${child.id}/calendars`); const card = document.createElement('div'); card.className = 'entity-card'; const calendarsHtml = calendars.length ? calendars.map((c) => calendarRowHtml(child, c)).join('') : '
No calendars yet.
'; card.innerHTML = `

${escapeHtml(child.name)}

${calendars.length} calendar${calendars.length === 1 ? '' : 's'}
${calendarsHtml}
`; card.querySelectorAll('[data-action]').forEach((btn) => { btn.addEventListener('click', () => handleCardAction(btn.dataset.action, btn.dataset)); }); return card; } function calendarRowHtml(child, cal) { const isActive = child.activeCalendarId === cal.id; return `
${escapeHtml(cal.title)}
Updated ${fmtDate(cal.updated_at)}${isActive ? ' · on tablet' : ''}
Open ${isActive ? '' : ``}
`; } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } async function handleCardAction(action, ds) { if (action === 'new-cal') { document.getElementById('newCalChildId').value = ds.child; document.getElementById('newCalTitle').value = ''; document.getElementById('newCalDuplicate').checked = false; openModal('newCalendarModal'); } else if (action === 'duplicate-cal') { const title = prompt('Title for the duplicated calendar:', `Copy of ${ds.title}`); if (!title) return; await api.post(`/api/children/${ds.child}/calendars`, { title, duplicateFromCalendarId: Number(ds.cal) }); showToast('Calendar duplicated'); await loadChildren(); } else if (action === 'set-active') { await api.patch(`/api/children/${ds.child}`, { activeCalendarId: Number(ds.cal) }); showToast('Tablet will now show this calendar'); await loadChildren(); } else if (action === 'delete-cal') { if (!confirm('Delete this calendar? This cannot be undone.')) return; await api.del(`/api/calendars/${ds.cal}`); showToast('Calendar deleted'); await loadChildren(); } else if (action === 'kiosk-link') { const child = children.find((c) => c.id === Number(ds.child)); openKioskModal(child); } else if (action === 'delete-child') { if (!confirm('Delete this child and all their calendars? This cannot be undone.')) return; await api.del(`/api/children/${ds.child}`); showToast('Child deleted'); await loadChildren(); } } function openModal(id) { document.getElementById(id).classList.remove('hidden'); } function closeModal(id) { document.getElementById(id).classList.add('hidden'); } document.getElementById('addChildBtn').addEventListener('click', () => { document.getElementById('childNameInput').value = ''; openModal('addChildModal'); }); document.getElementById('cancelAddChildBtn').addEventListener('click', () => closeModal('addChildModal')); document.getElementById('addChildForm').addEventListener('submit', async (e) => { e.preventDefault(); await api.post('/api/children', { name: document.getElementById('childNameInput').value }); closeModal('addChildModal'); showToast('Child added'); await loadChildren(); }); document.getElementById('cancelNewCalBtn').addEventListener('click', () => closeModal('newCalendarModal')); document.getElementById('newCalendarForm').addEventListener('submit', async (e) => { e.preventDefault(); const childId = document.getElementById('newCalChildId').value; const title = document.getElementById('newCalTitle').value; const duplicate = document.getElementById('newCalDuplicate').checked; let body = { title }; if (duplicate) { const { calendars } = await api.get(`/api/children/${childId}/calendars`); if (calendars.length) body.duplicateFromCalendarId = calendars[0].id; } const { calendar } = await api.post(`/api/children/${childId}/calendars`, body); closeModal('newCalendarModal'); window.location.href = `/calendar.html?calendarId=${calendar.id}`; }); document.getElementById('inviteBtn').addEventListener('click', async () => { const { invite } = await api.post('/api/household/invites'); const link = `${window.location.origin}${invite.joinPath}`; document.getElementById('inviteLinkField').value = link; openModal('inviteModal'); }); document.getElementById('closeInviteBtn').addEventListener('click', () => closeModal('inviteModal')); document.getElementById('copyInviteBtn').addEventListener('click', () => { document.getElementById('inviteLinkField').select(); navigator.clipboard.writeText(document.getElementById('inviteLinkField').value); showToast('Link copied'); }); let kioskModalChild = null; function openKioskModal(child) { kioskModalChild = child; document.getElementById('kioskChildName').textContent = child.name; document.getElementById('kioskLinkField').value = `${window.location.origin}${child.kioskPath}`; openModal('kioskModal'); } document.getElementById('closeKioskBtn').addEventListener('click', () => closeModal('kioskModal')); document.getElementById('copyKioskBtn').addEventListener('click', () => { document.getElementById('kioskLinkField').select(); navigator.clipboard.writeText(document.getElementById('kioskLinkField').value); showToast('Link copied'); }); document.getElementById('regenKioskBtn').addEventListener('click', async () => { if (!confirm('Regenerate this tablet link? The old link will stop working.')) return; const data = await api.post(`/api/children/${kioskModalChild.id}/kiosk-token/regenerate`); document.getElementById('kioskLinkField').value = `${window.location.origin}${data.kioskPath}`; showToast('Link regenerated'); await loadChildren(); }); document.getElementById('cancelChangeOwnPasswordBtn').addEventListener('click', () => closeModal('changeOwnPasswordModal')); document.getElementById('changeOwnPasswordForm').addEventListener('submit', async (e) => { e.preventDefault(); try { await api.post('/api/household/me/password', { currentPassword: document.getElementById('currentPasswordInput').value, newPassword: document.getElementById('newOwnPasswordInput').value, }); closeModal('changeOwnPasswordModal'); showToast('Password changed'); } catch (err) { showToast(err.message); } }); document.getElementById('cancelSetParentPasswordBtn').addEventListener('click', () => closeModal('setParentPasswordModal')); document.getElementById('setParentPasswordForm').addEventListener('submit', async (e) => { e.preventDefault(); try { const parentId = document.getElementById('setPasswordParentId').value; await api.patch(`/api/household/parents/${parentId}/password`, { newPassword: document.getElementById('newParentPasswordInput').value, }); closeModal('setParentPasswordModal'); showToast('Password updated'); } catch (err) { showToast(err.message); } }); document.getElementById('logoutBtn').addEventListener('click', async () => { await api.post('/api/auth/logout'); window.location.href = '/login.html'; }); boot();