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();