/**
* 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 = `
`;
// 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 = ``;
}
// 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 = `
`;
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 };
})();