Files

285 lines
8.5 KiB
JavaScript

const { addBaseUrlToImages } = require("../utils/imageHelper");
const { ICON_OPTIONS, normalizeIconClass } = require("../utils/iconOptions");
const Contact = require("../models/contact");
const ContactSubmission = require("../models/contactSubmission");
const writeAuditLog = require("../audit/writeAuditLog");
const diffObject = require("../audit/diffObject");
const AUDIT_ACTIONS = require("../constants/auditAction");
const SLUG = "contact";
function parseJsonField(raw) {
if (raw == null || raw === "") return null;
if (typeof raw === "object") return raw;
try { return JSON.parse(raw); } catch { return null; }
}
async function getDocument() {
return Contact.findOne({ slug: SLUG }).lean();
}
/**
* GET /api/contact — public payload for frontend LAMS.
*/
exports.api = async (req, res) => {
try {
const doc = await getDocument();
if (!doc) {
return res.status(404).json({ error: "Contact data not found" });
}
const baseUrl =
process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`;
const body = {
metadata: doc.metadata || {},
hero: doc.hero || {},
infoCards: doc.infoCards || {},
form: doc.form || {},
faq: doc.faq || {},
cta: doc.cta || {},
};
res.json(addBaseUrlToImages(body, baseUrl));
} catch (err) {
console.error("contact.api:", err);
res.status(500).json({ error: "Error loading contact data" });
}
};
// Legacy alias
exports.getContactData = exports.api;
/**
* GET /admin/contact — Admin view.
*/
exports.index = async (req, res) => {
try {
const doc = await getDocument();
const data = doc
? {
metadata: doc.metadata || {},
hero: doc.hero || {},
infoCards: doc.infoCards || {},
form: doc.form || {},
faq: doc.faq || {},
cta: doc.cta || {},
}
: {
metadata: {},
hero: { badge: "", titleMain: "", titleHighlight: "", description: "" },
infoCards: { contactInfo: { title: "", channels: [] }, supportHours: { title: "", hours: [], footer: {} } },
form: { heading: "", description: "", fields: {}, submitLabel: "Send Message", successMessage: "", errorMessage: "" },
faq: { title: "", subtitle: "", items: [] },
cta: { title: "", description: "", primaryButton: {}, secondaryButton: {} },
};
const { startDate, endDate } = req.query;
const query = {};
if (startDate || endDate) {
query.createdAt = {};
if (startDate) query.createdAt.$gte = new Date(startDate);
if (endDate) {
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
query.createdAt.$lte = end;
}
}
const submissions = await ContactSubmission.find(query).sort({ createdAt: -1 }).limit(50);
const frontendUrl = process.env.FRONTEND_URL || "";
res.render("admin/contact/index", {
title: "Contact Management",
layout: "layouts/main",
data,
submissions,
startDate,
endDate,
frontendUrl,
iconOptions: ICON_OPTIONS,
currentPath: req.path,
user: req.session.user,
});
} catch (error) {
console.error("contact.index:", error);
req.flash("error_msg", "An error occurred while loading the page");
res.redirect("/admin/dashboard");
}
};
/**
* POST /admin/contact/update — Save & audit.
*/
exports.update = async (req, res) => {
try {
const metadata = parseJsonField(req.body.metadata);
const hero = parseJsonField(req.body.hero);
const infoCards = parseJsonField(req.body.infoCards);
const form = parseJsonField(req.body.form);
const faq = parseJsonField(req.body.faq);
const cta = parseJsonField(req.body.cta);
if (!metadata || !hero || !infoCards || !form || !faq || !cta) {
req.flash("error_msg", "Invalid form payload. Please try again.");
return res.redirect("/admin/contact");
}
// Normalize channel icons against whitelist
if (infoCards?.contactInfo?.channels?.length) {
infoCards.contactInfo.channels = infoCards.contactInfo.channels.map((ch) => ({
...ch,
icon: normalizeIconClass(ch.icon),
}));
}
const payload = { metadata, hero, infoCards, form, faq, cta };
let doc = await Contact.findOne({ slug: SLUG });
const beforeData = doc ? JSON.parse(JSON.stringify(doc.toObject())) : {};
if (!doc) {
doc = new Contact({ slug: SLUG, ...payload });
} else {
doc.set(payload);
}
await doc.save();
const afterData = JSON.parse(JSON.stringify(doc.toObject()));
const changes = diffObject(beforeData, afterData);
if (changes.length > 0) {
await writeAuditLog({
model: "Contact",
documentId: doc._id,
action: AUDIT_ACTIONS.UPDATE_CONTACT,
before: beforeData,
after: afterData,
changes,
req,
});
}
req.flash("success_msg", "Contact updated successfully");
res.redirect("/admin/contact");
} catch (err) {
console.error("contact.update:", err);
req.flash("error_msg", err.message || "Error updating contact");
res.redirect("/admin/contact");
}
};
// ─────────────────────────────────────────────
// Form Submissions (unchanged)
// ─────────────────────────────────────────────
exports.submitForm = async (req, res) => {
try {
const { name, email, phone, address, date, message } = req.body;
if (!name || !email) {
return res.status(400).json({
success: false,
error: "Name and email are required",
});
}
const submission = new ContactSubmission({
name: name.trim(),
email: email.trim().toLowerCase(),
phone: phone?.trim() || "",
address: address?.trim() || "",
date: date?.trim() || "",
message: message?.trim() || "",
ipAddress: req.ip || req.connection?.remoteAddress || "",
userAgent: req.get("User-Agent") || "",
});
await submission.save();
res.status(201).json({
success: true,
message: "Thank you for contacting us! We will get back to you soon.",
data: {
id: submission._id,
name: submission.name,
email: submission.email,
},
});
} catch (err) {
console.error("Error submitting contact form:", err);
if (err.name === "ValidationError") {
const errors = Object.values(err.errors).map((e) => e.message);
return res.status(400).json({ success: false, error: errors.join(", ") });
}
res.status(500).json({
success: false,
error: "Error submitting form. Please try again later.",
});
}
};
exports.getSubmissions = async (req, res) => {
try {
const { status, page = 1, limit = 20 } = req.query;
const query = {};
if (status && ["pending", "read", "replied", "archived"].includes(status)) {
query.status = status;
}
const skip = (parseInt(page) - 1) * parseInt(limit);
const [submissions, total] = await Promise.all([
ContactSubmission.find(query)
.sort({ createdAt: -1 })
.skip(skip)
.limit(parseInt(limit)),
ContactSubmission.countDocuments(query),
]);
res.json({
success: true,
data: submissions,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
totalPages: Math.ceil(total / parseInt(limit)),
},
});
} catch (err) {
console.error("Error getting submissions:", err);
res.status(500).json({ success: false, error: "Error loading submissions" });
}
};
exports.updateSubmissionStatus = async (req, res) => {
try {
const { id } = req.params;
const { status, notes } = req.body;
const validStatuses = ["pending", "read", "replied", "archived"];
if (!validStatuses.includes(status)) {
return res.status(400).json({ success: false, error: "Invalid status" });
}
const updateData = { status };
if (notes !== undefined) updateData.notes = notes;
if (status === "replied") updateData.repliedAt = new Date();
const submission = await ContactSubmission.findByIdAndUpdate(
id,
updateData,
{ new: true }
);
if (!submission) {
return res.status(404).json({ success: false, error: "Submission not found" });
}
res.json({ success: true, data: submission });
} catch (err) {
console.error("Error updating submission:", err);
res.status(500).json({ success: false, error: "Error updating submission" });
}
};