tractatus/src/controllers/contact.controller.js
TheFlow fe3035913e feat(crm): implement unified contact form system
Complete CRM foundation with contact modal in footer

Backend:
- Contact.model.js: Full CRUD model with statistics tracking
- contact.controller.js: Submit, list, assign, respond, update, delete
- contact.routes.js: Public submission + admin management endpoints
- routes/index.js: Mount contact routes at /api/contact

Frontend:
- footer.js: Replace mailto link with Contact Us modal button
- Contact modal: Form with type, name, email, org, subject, message
- CSRF protection: Extracts token from cookie (like newsletter)
- Rate limiting: formRateLimiter (5/min)
- Validation: Input sanitization + required fields
- UX: Success/error messages, auto-close on success

Admin UI:
- navbar-admin.js: New 'CRM & Communications' section
- Links: Contact Management, Case Submissions, Media Inquiries

Foundation for multi-project CRM across tractatus, family-history, sydigital

Next: Build /admin/contact-management.html page
2025-10-24 16:56:21 +13:00

270 lines
5.5 KiB
JavaScript

/**
* Contact Controller
* Handle general contact form submissions
*/
const Contact = require('../models/Contact.model');
const logger = require('../utils/logger.util');
const { getClientIp } = require('../utils/security-logger');
/**
* POST /api/contact/submit
* Submit a contact form
*/
async function submit(req, res) {
try {
const { type, name, email, organization, phone, subject, message } = req.body;
// Create contact record
const contact = await Contact.create({
type: type || 'general',
contact: {
name,
email,
organization,
phone
},
inquiry: {
subject,
message
},
source: 'footer_contact',
metadata: {
user_agent: req.get('user-agent'),
ip: getClientIp(req),
source_page: req.get('referer'),
referrer: req.get('referer')
}
});
logger.info(`[Contact] New submission: ${contact._id} from ${email}`);
res.status(201).json({
success: true,
message: 'Thank you for contacting us. We will respond within 24 hours.',
contact_id: contact._id
});
} catch (error) {
logger.error('[Contact] Submit error:', error);
res.status(500).json({
success: false,
error: 'Failed to submit contact form',
message: 'An error occurred while submitting your message. Please try again.'
});
}
}
/**
* GET /api/contact/admin/list
* List contact submissions (admin)
*/
async function list(req, res) {
try {
const { status, type, priority, limit = 20, skip = 0 } = req.query;
const filters = {};
if (status) filters.status = status;
if (type) filters.type = type;
if (priority) filters.priority = priority;
const contacts = await Contact.list(filters, {
limit: parseInt(limit),
skip: parseInt(skip)
});
const total = await Contact.countByStatus(status || undefined);
res.json({
success: true,
contacts,
pagination: {
total,
limit: parseInt(limit),
skip: parseInt(skip),
hasMore: parseInt(skip) + contacts.length < total
}
});
} catch (error) {
logger.error('[Contact] List error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch contacts'
});
}
}
/**
* GET /api/contact/admin/stats
* Get contact statistics (admin)
*/
async function getStats(req, res) {
try {
const stats = await Contact.getStats();
res.json({
success: true,
stats
});
} catch (error) {
logger.error('[Contact] Stats error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch statistics'
});
}
}
/**
* GET /api/contact/admin/:id
* Get single contact by ID (admin)
*/
async function getById(req, res) {
try {
const { id } = req.params;
const contact = await Contact.findById(id);
if (!contact) {
return res.status(404).json({
success: false,
error: 'Contact not found'
});
}
res.json({
success: true,
contact
});
} catch (error) {
logger.error('[Contact] Get by ID error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch contact'
});
}
}
/**
* POST /api/contact/admin/:id/assign
* Assign contact to admin user
*/
async function assign(req, res) {
try {
const { id } = req.params;
const { user_id } = req.body;
const contact = await Contact.assign(id, user_id);
res.json({
success: true,
message: 'Contact assigned successfully',
contact
});
} catch (error) {
logger.error('[Contact] Assign error:', error);
res.status(500).json({
success: false,
error: 'Failed to assign contact'
});
}
}
/**
* POST /api/contact/admin/:id/respond
* Mark contact as responded
*/
async function respond(req, res) {
try {
const { id } = req.params;
const { content } = req.body;
const contact = await Contact.markResponded(id, {
content,
responder: req.user.id
});
res.json({
success: true,
message: 'Response recorded successfully',
contact
});
} catch (error) {
logger.error('[Contact] Respond error:', error);
res.status(500).json({
success: false,
error: 'Failed to record response'
});
}
}
/**
* PUT /api/contact/admin/:id
* Update contact
*/
async function update(req, res) {
try {
const { id } = req.params;
const { status, priority, assigned_to } = req.body;
const updateData = {};
if (status) updateData.status = status;
if (priority) updateData.priority = priority;
if (assigned_to !== undefined) updateData.assigned_to = assigned_to;
const contact = await Contact.update(id, updateData);
res.json({
success: true,
message: 'Contact updated successfully',
contact
});
} catch (error) {
logger.error('[Contact] Update error:', error);
res.status(500).json({
success: false,
error: 'Failed to update contact'
});
}
}
/**
* DELETE /api/contact/admin/:id
* Delete contact
*/
async function deleteContact(req, res) {
try {
const { id } = req.params;
await Contact.delete(id);
res.json({
success: true,
message: 'Contact deleted successfully'
});
} catch (error) {
logger.error('[Contact] Delete error:', error);
res.status(500).json({
success: false,
error: 'Failed to delete contact'
});
}
}
module.exports = {
submit,
list,
getStats,
getById,
assign,
respond,
update,
deleteContact
};