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
270 lines
5.5 KiB
JavaScript
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
|
|
};
|