/** * ActivityTimeline Model * Tracks all interactions with contacts and organizations across projects */ const { ObjectId } = require('mongodb'); const { getCollection } = require('../utils/db.util'); class ActivityTimeline { /** * Create a new activity entry */ static async create(data) { const collection = await getCollection('activity_timeline'); const activity = { // Who/what this activity is related to contact_id: data.contact_id ? new ObjectId(data.contact_id) : null, organization_id: data.organization_id ? new ObjectId(data.organization_id) : null, // Activity details type: data.type, // contact_form, media_inquiry, case_submission, email_sent, call, meeting, note project: data.project || 'tractatus', // Which project this activity belongs to // Activity content title: data.title, description: data.description || null, // Related record (original submission/inquiry) related_record: data.related_record ? { type: data.related_record.type, // contact, media, case, email, etc id: new ObjectId(data.related_record.id) } : null, // Activity metadata metadata: { direction: data.metadata?.direction || 'inbound', // inbound, outbound channel: data.metadata?.channel || 'web', // web, email, phone, meeting status: data.metadata?.status || 'completed', // completed, pending, failed outcome: data.metadata?.outcome || null, // responded, no_response, follow_up_needed duration: data.metadata?.duration || null, // for calls/meetings (minutes) attachments: data.metadata?.attachments || [] }, // Who performed this activity (user) performed_by: data.performed_by ? new ObjectId(data.performed_by) : null, performed_by_name: data.performed_by_name || 'System', // Timestamps occurred_at: data.occurred_at || new Date(), created_at: new Date() }; const result = await collection.insertOne(activity); return { ...activity, _id: result.insertedId }; } /** * Get timeline for a contact */ static async getByContact(contactId, options = {}) { const collection = await getCollection('activity_timeline'); const { limit = 50, skip = 0 } = options; return await collection .find({ contact_id: new ObjectId(contactId) }) .sort({ occurred_at: -1 }) .skip(skip) .limit(limit) .toArray(); } /** * Get timeline for an organization */ static async getByOrganization(organizationId, options = {}) { const collection = await getCollection('activity_timeline'); const { limit = 50, skip = 0 } = options; return await collection .find({ organization_id: new ObjectId(organizationId) }) .sort({ occurred_at: -1 }) .skip(skip) .limit(limit) .toArray(); } /** * Get combined timeline (contact + organization) */ static async getCombinedTimeline(contactId, organizationId, options = {}) { const collection = await getCollection('activity_timeline'); const { limit = 100, skip = 0 } = options; const query = { $or: [ { contact_id: new ObjectId(contactId) }, { organization_id: new ObjectId(organizationId) } ] }; return await collection .find(query) .sort({ occurred_at: -1 }) .skip(skip) .limit(limit) .toArray(); } /** * Get recent activity across all projects */ static async getRecent(options = {}) { const collection = await getCollection('activity_timeline'); const { limit = 50, project = null, type = null } = options; const query = {}; if (project) query.project = project; if (type) query.type = type; return await collection .find(query) .sort({ occurred_at: -1 }) .limit(limit) .toArray(); } /** * Delete activity */ static async delete(id) { const collection = await getCollection('activity_timeline'); return await collection.deleteOne({ _id: new ObjectId(id) }); } /** * Get statistics */ static async getStats(filters = {}) { const collection = await getCollection('activity_timeline'); const query = {}; if (filters.project) query.project = filters.project; if (filters.contact_id) query.contact_id = new ObjectId(filters.contact_id); if (filters.organization_id) query.organization_id = new ObjectId(filters.organization_id); const [total, byType, byProject, byChannel] = await Promise.all([ collection.countDocuments(query), collection.aggregate([ { $match: query }, { $group: { _id: '$type', count: { $sum: 1 } } } ]).toArray(), collection.aggregate([ { $match: query }, { $group: { _id: '$project', count: { $sum: 1 } } } ]).toArray(), collection.aggregate([ { $match: query }, { $group: { _id: '$metadata.channel', count: { $sum: 1 } } } ]).toArray() ]); return { total, by_type: byType.reduce((acc, item) => { acc[item._id] = item.count; return acc; }, {}), by_project: byProject.reduce((acc, item) => { acc[item._id] = item.count; return acc; }, {}), by_channel: byChannel.reduce((acc, item) => { acc[item._id] = item.count; return acc; }, {}) }; } } module.exports = ActivityTimeline;