tractatus/src/controllers/crm.controller.js
TheFlow 0b853c537d 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

287 lines
6.6 KiB
JavaScript

/**
* CRM Controller
* Multi-project CRM system for contacts, organizations, activities, and SLAs
*/
const UnifiedContact = require('../models/UnifiedContact.model');
const Organization = require('../models/Organization.model');
const ActivityTimeline = require('../models/ActivityTimeline.model');
const ResponseTemplate = require('../models/ResponseTemplate.model');
const SLATracking = require('../models/SLATracking.model');
const Contact = require('../models/Contact.model');
const MediaInquiry = require('../models/MediaInquiry.model');
const CaseSubmission = require('../models/CaseSubmission.model');
/**
* Get CRM dashboard statistics
*/
async function getDashboardStats(req, res) {
try {
const [contactStats, orgStats, slaStats, templateStats] = await Promise.all([
UnifiedContact.getStats(),
Organization.getStats(),
SLATracking.getStats({ project: 'tractatus' }),
ResponseTemplate.getStats()
]);
res.json({
success: true,
stats: {
contacts: contactStats,
organizations: orgStats,
sla: slaStats,
templates: templateStats
}
});
} catch (error) {
console.error('CRM dashboard stats error:', error);
res.status(500).json({
success: false,
error: 'Failed to load dashboard statistics'
});
}
}
/**
* List unified contacts
*/
async function listContacts(req, res) {
try {
const { project, status, tag, organization_id, limit = 50, skip = 0 } = req.query;
const filters = {};
if (project) filters.project = project;
if (status) filters.status = status;
if (tag) filters.tag = tag;
if (organization_id) filters.organization_id = organization_id;
const contacts = await UnifiedContact.list(filters, {
limit: parseInt(limit),
skip: parseInt(skip)
});
res.json({
success: true,
total: contacts.length,
contacts
});
} catch (error) {
console.error('List contacts error:', error);
res.status(500).json({
success: false,
error: 'Failed to list contacts'
});
}
}
/**
* Get single contact with full details
*/
async function getContact(req, res) {
try {
const { id } = req.params;
const contact = await UnifiedContact.findById(id);
if (!contact) {
return res.status(404).json({
success: false,
error: 'Contact not found'
});
}
// Get organization if linked
let organization = null;
if (contact.organization_id) {
organization = await Organization.findById(contact.organization_id);
}
// Get activity timeline
const activities = await ActivityTimeline.getByContact(id, { limit: 50 });
res.json({
success: true,
contact,
organization,
activities
});
} catch (error) {
console.error('Get contact error:', error);
res.status(500).json({
success: false,
error: 'Failed to load contact'
});
}
}
/**
* List organizations
*/
async function listOrganizations(req, res) {
try {
const { type, country, project, status, tier, limit = 50, skip = 0 } = req.query;
const filters = {};
if (type) filters.type = type;
if (country) filters.country = country;
if (project) filters.project = project;
if (status) filters.status = status;
if (tier) filters.tier = tier;
const organizations = await Organization.list(filters, {
limit: parseInt(limit),
skip: parseInt(skip)
});
res.json({
success: true,
total: organizations.length,
organizations
});
} catch (error) {
console.error('List organizations error:', error);
res.status(500).json({
success: false,
error: 'Failed to list organizations'
});
}
}
/**
* Get single organization with contacts and activity
*/
async function getOrganization(req, res) {
try {
const { id } = req.params;
const organization = await Organization.findById(id);
if (!organization) {
return res.status(404).json({
success: false,
error: 'Organization not found'
});
}
// Get contacts at this organization
const contacts = await UnifiedContact.findByOrganization(id, { limit: 100 });
// Get activity timeline
const activities = await ActivityTimeline.getByOrganization(id, { limit: 50 });
res.json({
success: true,
organization,
contacts,
activities
});
} catch (error) {
console.error('Get organization error:', error);
res.status(500).json({
success: false,
error: 'Failed to load organization'
});
}
}
/**
* Get SLA dashboard
*/
async function getSLADashboard(req, res) {
try {
const { project = 'tractatus' } = req.query;
const [stats, pending, approaching, breached] = await Promise.all([
SLATracking.getStats({ project }),
SLATracking.getPending({ project, limit: 20 }),
SLATracking.getApproachingBreach({ project }),
SLATracking.getBreached({ project, limit: 20 })
]);
res.json({
success: true,
stats,
pending,
approaching_breach: approaching,
breached
});
} catch (error) {
console.error('SLA dashboard error:', error);
res.status(500).json({
success: false,
error: 'Failed to load SLA dashboard'
});
}
}
/**
* List response templates
*/
async function listTemplates(req, res) {
try {
const { category, language, project, tag, limit = 50, skip = 0 } = req.query;
const filters = {};
if (category) filters.category = category;
if (language) filters.language = language;
if (project) filters.project = project;
if (tag) filters.tag = tag;
const templates = await ResponseTemplate.list(filters, {
limit: parseInt(limit),
skip: parseInt(skip)
});
res.json({
success: true,
total: templates.length,
templates
});
} catch (error) {
console.error('List templates error:', error);
res.status(500).json({
success: false,
error: 'Failed to list templates'
});
}
}
/**
* Render template with variables
*/
async function renderTemplate(req, res) {
try {
const { id } = req.params;
const { variables } = req.body;
const rendered = await ResponseTemplate.render(id, variables || {});
res.json({
success: true,
rendered
});
} catch (error) {
console.error('Render template error:', error);
res.status(500).json({
success: false,
error: 'Failed to render template'
});
}
}
module.exports = {
getDashboardStats,
listContacts,
getContact,
listOrganizations,
getOrganization,
getSLADashboard,
listTemplates,
renderTemplate
};