/** * GoingsOn - Contact Dashboard Module * * Full-page dashboard for a contact, showing linked tasks, events, and emails * as a unified activity timeline. Modeled after the task overview page. */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttr; let currentContactId = null; /** * Open the contact dashboard for a given contact. * @param {string} contactId */ async function open(contactId) { currentContactId = contactId; GoingsOn.navigation.switchView('contact-dashboard'); const content = document.getElementById('contact-dashboard-content'); const titleEl = document.getElementById('contact-dashboard-title'); const actionsEl = document.getElementById('contact-dashboard-actions'); try { const [contact, tasks, events, emails] = await Promise.all([ GoingsOn.api.contacts.get(contactId), GoingsOn.api.contacts.listTasksForContact(contactId), GoingsOn.api.contacts.listEventsForContact(contactId), GoingsOn.api.contacts.listEmailsForContact(contactId), ]); titleEl.textContent = contact.displayName || contact.display_name; render(content, actionsEl, contact, tasks, events, emails); } catch (err) { content.innerHTML = `

Failed to load contact: ${esc(GoingsOn.utils.getErrorMessage(err))}

`; } } function close() { currentContactId = null; GoingsOn.navigation.switchView('contacts'); } function render(container, actionsEl, contact, tasks, events, emails) { const name = contact.displayName || contact.display_name; const initials = contact.initials || name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase(); // Actions bar if (contact.isImplicit) { actionsEl.innerHTML = ` `; } else { actionsEl.innerHTML = ` `; } // Header card const company = contact.company ? `${esc(contact.company)}` : ''; const title = contact.title ? `${esc(contact.title)}` : ''; const companyTitle = [company, title].filter(Boolean).join(' · '); const tags = (contact.tags || []).map(t => `${esc(t)}`).join(' '); let headerHtml = `
${esc(initials)}

${esc(name)}

${companyTitle ? `
${companyTitle}
` : ''} ${tags ? `
${tags}
` : ''}
`; // Contact info (email addresses, phones, social handles) let infoHtml = ''; const infoItems = []; for (const e of contact.emails || []) { infoItems.push(`Email: ${esc(e.address)}${e.label ? ' ' + esc(e.label) + '' : ''}`); } for (const p of contact.phones || []) { infoItems.push(`Phone: ${esc(p.number)}${p.label ? ' ' + esc(p.label) + '' : ''}`); } for (const s of contact.socialHandles || contact.social_handles || []) { infoItems.push(`${esc(s.platform)}: ${esc(s.handle)}`); } if (infoItems.length > 0) { infoHtml = `
${infoItems.map(i => `
${i}
`).join('')}
`; } // Activity timeline — merge all entities, sort by date desc const timeline = []; for (const t of tasks) { timeline.push({ type: 'task', date: new Date(t.createdAt || t.created_at), title: t.description, status: t.statusDisplay || t.status, id: t.id, }); } for (const e of events) { timeline.push({ type: 'event', date: new Date(e.startTime || e.start_time), title: e.displayTitle || e.title, id: e.id, }); } for (const e of emails) { timeline.push({ type: 'email', date: new Date(e.receivedAt || e.received_at), title: e.subject || '(no subject)', from: e.from, isOutgoing: e.isOutgoing || e.is_outgoing || false, id: e.id, }); } timeline.sort((a, b) => b.date - a.date); const INITIAL_SHOW = 20; const totalCount = timeline.length; const visibleTimeline = timeline.slice(0, INITIAL_SHOW); let timelineHtml = ''; if (totalCount === 0) { timelineHtml = '

No interactions yet.

'; } else { const items = visibleTimeline.map(item => { const dateStr = item.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const icon = item.type === 'task' ? '☑' : item.type === 'event' ? '📅' : (item.isOutgoing ? '✉︎→' : '←✉︎'); const badge = item.type === 'task' && item.status ? `${esc(item.status)}` : ''; let onclick = ''; if (item.type === 'task') onclick = `GoingsOn.taskOverview.open('${escAttr(item.id)}')`; else if (item.type === 'event') onclick = `GoingsOn.events.open('${escAttr(item.id)}')`; else if (item.type === 'email') onclick = `GoingsOn.emails.open('${escAttr(item.id)}')`; return `
${icon} ${esc(item.title)} ${badge} ${dateStr}
`; }).join(''); const showAllBtn = totalCount > INITIAL_SHOW ? `` : ''; timelineHtml = `
${items}
${showAllBtn} `; } // Notes section const notesHtml = contact.notes ? `

Notes

${esc(contact.notes)}

` : ''; // Linked entities summary const taskCount = tasks.length; const eventCount = events.length; const emailCount = emails.length; const summaryHtml = `
${taskCount} Tasks
${eventCount} Events
${emailCount} Emails
`; container.innerHTML = headerHtml + infoHtml + summaryHtml + `

Activity

${timelineHtml}
` + notesHtml; // Store full timeline for "show all" container._fullTimeline = timeline; } function showAllTimeline() { const container = document.getElementById('contact-dashboard-content'); const timeline = container?._fullTimeline; if (!timeline) return; const timelineEl = document.getElementById('contact-timeline'); const showAllBtn = document.getElementById('contact-timeline-show-all'); if (showAllBtn) showAllBtn.remove(); // Re-render full timeline timelineEl.innerHTML = timeline.map(item => { const dateStr = item.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const icon = item.type === 'task' ? '☑' : item.type === 'event' ? '📅' : (item.isOutgoing ? '✉︎→' : '←✉︎'); const badge = item.type === 'task' && item.status ? `${esc(item.status)}` : ''; let onclick = ''; if (item.type === 'task') onclick = `GoingsOn.taskOverview.open('${escAttr(item.id)}')`; else if (item.type === 'event') onclick = `GoingsOn.events.open('${escAttr(item.id)}')`; else if (item.type === 'email') onclick = `GoingsOn.emails.open('${escAttr(item.id)}')`; return `
${icon} ${esc(item.title)} ${badge} ${dateStr}
`; }).join(''); } async function promote(contactId) { try { await GoingsOn.api.contacts.promoteContact(contactId); GoingsOn.cache.invalidate('contacts'); GoingsOn.autocomplete.refresh(); GoingsOn.ui.showToast('Contact saved!', 'success'); open(contactId); // re-render } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to save contact'), 'error'); } } GoingsOn.contactDashboard = { open, close, promote, showAllTimeline }; })();