tractatus/src/models/Organization.model.js
TheFlow edb1540631 feat(crm): complete Phase 3 multi-project CRM + critical bug fixes
Phase 3 Multi-Project CRM Implementation:
- Add UnifiedContact model for cross-project contact linking
- Add Organization model with domain-based auto-detection
- Add ActivityTimeline model for comprehensive interaction tracking
- Add SLATracking model for 24-hour response commitment
- Add ResponseTemplate model with variable substitution
- Add CRM controller with 8 API endpoints
- Add Inbox controller for unified communications
- Add CRM dashboard frontend with tabs (Contacts, Orgs, SLA, Templates)
- Add Contact Management interface (Phase 1)
- Add Unified Inbox interface (Phase 2)
- Integrate CRM routes into main API

Critical Bug Fixes:
- Fix newsletter DELETE button (event handler context issue)
- Fix case submission invisible button (invalid CSS class)
- Fix Chart.js CSP violation (add cdn.jsdelivr.net to policy)
- Fix Chart.js SRI integrity hash mismatch

Technical Details:
- Email-based contact deduplication across projects
- Automatic organization linking via email domain
- Cross-project activity timeline aggregation
- SLA breach detection and alerting system
- Template rendering with {placeholder} substitution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 18:10:14 +13:00

232 lines
6.1 KiB
JavaScript

/**
* Organization Model
* Shared across all projects - tracks companies, institutions, media outlets
*/
const { ObjectId } = require('mongodb');
const { getCollection } = require('../utils/db.util');
class Organization {
/**
* Create a new organization
*/
static async create(data) {
const collection = await getCollection('organizations');
const organization = {
name: data.name,
domain: data.domain || null, // Primary domain (e.g., "economist.com")
type: data.type || 'other', // media, academic, corporate, nonprofit, government, other
country: data.country || null,
// Contact information
contact_info: {
website: data.contact_info?.website || null,
email: data.contact_info?.email || null,
phone: data.contact_info?.phone || null,
address: data.contact_info?.address || null
},
// Metadata
metadata: {
industry: data.metadata?.industry || null,
size: data.metadata?.size || null, // small, medium, large
description: data.metadata?.description || null,
notes: data.metadata?.notes || null
},
// Projects this organization has interacted with
projects: data.projects || [], // ['tractatus', 'family-history', 'sydigital']
// Relationship status
relationship: {
status: data.relationship?.status || 'prospect', // prospect, active, partner, archived
since: data.relationship?.since || new Date(),
tier: data.relationship?.tier || 'standard' // standard, priority, strategic
},
// Social/web presence
social: {
linkedin: data.social?.linkedin || null,
twitter: data.social?.twitter || null,
other: data.social?.other || {}
},
created_at: new Date(),
updated_at: new Date()
};
const result = await collection.insertOne(organization);
return { ...organization, _id: result.insertedId };
}
/**
* Find organization by ID
*/
static async findById(id) {
const collection = await getCollection('organizations');
return await collection.findOne({ _id: new ObjectId(id) });
}
/**
* Find organization by name (case-insensitive)
*/
static async findByName(name) {
const collection = await getCollection('organizations');
return await collection.findOne({
name: { $regex: new RegExp(`^${name}$`, 'i') }
});
}
/**
* Find organization by domain
*/
static async findByDomain(domain) {
const collection = await getCollection('organizations');
return await collection.findOne({ domain });
}
/**
* Find or create organization
*/
static async findOrCreate(data) {
// Try to find by domain first (most reliable)
if (data.domain) {
const existing = await this.findByDomain(data.domain);
if (existing) return existing;
}
// Try to find by name
if (data.name) {
const existing = await this.findByName(data.name);
if (existing) return existing;
}
// Create new organization
return await this.create(data);
}
/**
* List organizations with filtering
*/
static async list(filters = {}, options = {}) {
const collection = await getCollection('organizations');
const { limit = 50, skip = 0 } = options;
const query = {};
if (filters.type) query.type = filters.type;
if (filters.country) query.country = filters.country;
if (filters.project) query.projects = filters.project;
if (filters.status) query['relationship.status'] = filters.status;
if (filters.tier) query['relationship.tier'] = filters.tier;
return await collection
.find(query)
.sort({ name: 1 })
.skip(skip)
.limit(limit)
.toArray();
}
/**
* Search organizations by name or domain
*/
static async search(searchTerm, options = {}) {
const collection = await getCollection('organizations');
const { limit = 20 } = options;
return await collection
.find({
$or: [
{ name: { $regex: searchTerm, $options: 'i' } },
{ domain: { $regex: searchTerm, $options: 'i' } },
{ 'contact_info.website': { $regex: searchTerm, $options: 'i' } }
]
})
.limit(limit)
.toArray();
}
/**
* Update organization
*/
static async update(id, data) {
const collection = await getCollection('organizations');
const updateData = {
...data,
updated_at: new Date()
};
await collection.updateOne(
{ _id: new ObjectId(id) },
{ $set: updateData }
);
return await this.findById(id);
}
/**
* Add project to organization
*/
static async addProject(id, projectName) {
const collection = await getCollection('organizations');
await collection.updateOne(
{ _id: new ObjectId(id) },
{
$addToSet: { projects: projectName },
$set: { updated_at: new Date() }
}
);
return await this.findById(id);
}
/**
* Delete organization
*/
static async delete(id) {
const collection = await getCollection('organizations');
return await collection.deleteOne({ _id: new ObjectId(id) });
}
/**
* Get statistics
*/
static async getStats() {
const collection = await getCollection('organizations');
const [total, byType, byStatus, byProject] = await Promise.all([
collection.countDocuments(),
collection.aggregate([
{ $group: { _id: '$type', count: { $sum: 1 } } }
]).toArray(),
collection.aggregate([
{ $group: { _id: '$relationship.status', count: { $sum: 1 } } }
]).toArray(),
collection.aggregate([
{ $unwind: '$projects' },
{ $group: { _id: '$projects', count: { $sum: 1 } } }
]).toArray()
]);
return {
total,
by_type: byType.reduce((acc, item) => {
acc[item._id] = item.count;
return acc;
}, {}),
by_status: byStatus.reduce((acc, item) => {
acc[item._id] = item.count;
return acc;
}, {}),
by_project: byProject.reduce((acc, item) => {
acc[item._id] = item.count;
return acc;
}, {})
};
}
}
module.exports = Organization;