forked from UKSOURCE/cms.hailearning.edu.vn
Fix merge conflicts with main
This commit is contained in:
377
controllers/appointmentController.js
Normal file
377
controllers/appointmentController.js
Normal file
@@ -0,0 +1,377 @@
|
||||
const AppointmentSubmission = require("../models/appointmentSubmission");
|
||||
const Appointment = require("../models/appointment");
|
||||
|
||||
// ==================== CMS ADMIN FUNCTIONS ====================
|
||||
|
||||
// Render admin page for appointment management
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
|
||||
// If no data in DB, try to load from JSON file
|
||||
if (!appointment) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/appointment.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
appointment = await Appointment.migrateFromJson(jsonData);
|
||||
} else {
|
||||
// Create default appointment
|
||||
appointment = await Appointment.create({
|
||||
name: "default",
|
||||
hero: {
|
||||
title: "Make Appointment",
|
||||
backgroundImage: "",
|
||||
subtitle: "",
|
||||
heading: "",
|
||||
description: "",
|
||||
},
|
||||
visaOptions: [],
|
||||
form: {
|
||||
heading: "Request Appointment",
|
||||
fields: [],
|
||||
submitButton: {
|
||||
text: "Request Appointment",
|
||||
icon: "fa-solid fa-arrow-right",
|
||||
buttonClass: "theme-btn",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { startDate, endDate } = req.query;
|
||||
const query = {};
|
||||
|
||||
if (startDate || endDate) {
|
||||
query.createdAt = {};
|
||||
if (startDate) {
|
||||
query.createdAt.$gte = new Date(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
// Set end date to end of day
|
||||
const end = new Date(endDate);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
query.createdAt.$lte = end;
|
||||
}
|
||||
}
|
||||
|
||||
const submissions = await AppointmentSubmission.find(query).sort({ createdAt: -1 }).limit(50);
|
||||
|
||||
res.render("admin/appointment/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Appointment Management",
|
||||
data: appointment,
|
||||
submissions,
|
||||
startDate,
|
||||
endDate,
|
||||
user: req.session.user,
|
||||
frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading appointment admin page:", err);
|
||||
req.flash("error", "Error loading appointment data");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Update appointment data
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { hero, visaOptions, form } = req.body;
|
||||
|
||||
// Parse JSON strings if needed
|
||||
const heroData = typeof hero === "string" ? JSON.parse(hero) : hero;
|
||||
const visaOptionsData = typeof visaOptions === "string" ? JSON.parse(visaOptions) : visaOptions;
|
||||
const formData = typeof form === "string" ? JSON.parse(form) : form;
|
||||
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
|
||||
if (appointment) {
|
||||
appointment.hero = heroData;
|
||||
appointment.visaOptions = visaOptionsData;
|
||||
appointment.form = formData;
|
||||
await appointment.save();
|
||||
} else {
|
||||
appointment = await Appointment.create({
|
||||
name: "default",
|
||||
hero: heroData,
|
||||
visaOptions: visaOptionsData,
|
||||
form: formData,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success", "Appointment data updated successfully");
|
||||
res.redirect("/admin/appointment");
|
||||
} catch (err) {
|
||||
console.error("Error updating appointment:", err);
|
||||
req.flash("error", "Error updating appointment data");
|
||||
res.redirect("/admin/appointment");
|
||||
}
|
||||
};
|
||||
|
||||
// API to get appointment data
|
||||
exports.getAppointmentData = async (req, res) => {
|
||||
try {
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
|
||||
if (!appointment) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/appointment.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
appointment = await Appointment.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointment,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointment data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointment data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Public API to get appointment page data (for frontend)
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
let appointment = await Appointment.findOne({ name: "default" });
|
||||
|
||||
if (!appointment) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/appointment.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
appointment = await Appointment.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment data not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: appointment.hero,
|
||||
visaOptions: appointment.visaOptions,
|
||||
form: appointment.form,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointment API data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointment data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== APPOINTMENT SUBMISSIONS API ====================
|
||||
|
||||
// API để submit appointment form (từ frontend)
|
||||
exports.submitAppointment = async (req, res) => {
|
||||
try {
|
||||
const { name, email, phone, address, appointmentDate, message, visaTypes } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!name || !email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Name and email are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Create new submission
|
||||
const submission = new AppointmentSubmission({
|
||||
name: name.trim(),
|
||||
email: email.trim().toLowerCase(),
|
||||
phone: phone?.trim() || "",
|
||||
address: address?.trim() || "",
|
||||
appointmentDate: appointmentDate?.trim() || "",
|
||||
message: message?.trim() || "",
|
||||
visaTypes: Array.isArray(visaTypes) ? visaTypes : [],
|
||||
ipAddress: req.ip || req.connection?.remoteAddress || "",
|
||||
userAgent: req.get("User-Agent") || "",
|
||||
});
|
||||
|
||||
await submission.save();
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: "Thank you! Your appointment request has been submitted. We will contact you soon.",
|
||||
data: {
|
||||
id: submission._id,
|
||||
name: submission.name,
|
||||
email: submission.email,
|
||||
appointmentDate: submission.appointmentDate,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error submitting appointment:", err);
|
||||
|
||||
// Handle validation errors
|
||||
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 appointment. Please try again later.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để lấy danh sách appointments (cho admin)
|
||||
exports.getAppointments = async (req, res) => {
|
||||
try {
|
||||
const { status, page = 1, limit = 20 } = req.query;
|
||||
|
||||
const query = {};
|
||||
if (status && ["pending", "confirmed", "completed", "cancelled"].includes(status)) {
|
||||
query.status = status;
|
||||
}
|
||||
|
||||
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
const [appointments, total] = await Promise.all([
|
||||
AppointmentSubmission.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(parseInt(limit)),
|
||||
AppointmentSubmission.countDocuments(query),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointments,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
totalPages: Math.ceil(total / parseInt(limit)),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointments:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointments",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để cập nhật status của appointment
|
||||
exports.updateAppointmentStatus = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, notes } = req.body;
|
||||
|
||||
const validStatuses = ["pending", "confirmed", "completed", "cancelled"];
|
||||
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 === "confirmed") updateData.confirmedAt = new Date();
|
||||
if (status === "completed") updateData.completedAt = new Date();
|
||||
|
||||
const appointment = await AppointmentSubmission.findByIdAndUpdate(
|
||||
id,
|
||||
updateData,
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointment,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error updating appointment:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error updating appointment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để lấy chi tiết một appointment
|
||||
exports.getAppointmentById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const appointment = await AppointmentSubmission.findById(id);
|
||||
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: appointment,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting appointment:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading appointment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để xóa appointment
|
||||
exports.deleteAppointment = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const appointment = await AppointmentSubmission.findByIdAndDelete(id);
|
||||
|
||||
if (!appointment) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Appointment not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Appointment deleted successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error deleting appointment:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error deleting appointment",
|
||||
});
|
||||
}
|
||||
};
|
||||
231
controllers/blogCategoryController.js
Normal file
231
controllers/blogCategoryController.js
Normal file
@@ -0,0 +1,231 @@
|
||||
const BlogCategory = require('../models/blogCategory');
|
||||
|
||||
// -------------------- Admin Controllers --------------------
|
||||
|
||||
// Display category management page
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const categories = await BlogCategory.find()
|
||||
.sort({ name: 1 })
|
||||
.lean();
|
||||
|
||||
res.render('admin/blog/categories/index', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Blog Categories',
|
||||
categories,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Category index error:', err);
|
||||
req.flash('error_msg', 'Error loading categories');
|
||||
res.redirect('/admin/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
// Show create category form
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
res.render('admin/blog/categories/create', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Create New Category',
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Category create form error:', err);
|
||||
req.flash('error_msg', 'Error loading create form');
|
||||
res.redirect('/admin/blog/categories');
|
||||
}
|
||||
};
|
||||
|
||||
// Store new category
|
||||
exports.store = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
isActive
|
||||
} = req.body;
|
||||
|
||||
// Generate slug
|
||||
const slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim('-');
|
||||
|
||||
// Check if slug exists
|
||||
const existingCategory = await BlogCategory.findOne({ slug });
|
||||
if (existingCategory) {
|
||||
req.flash('error_msg', 'A category with this name already exists');
|
||||
return res.redirect('/admin/blog/categories/create');
|
||||
}
|
||||
|
||||
// Create category data
|
||||
const categoryData = {
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
isActive: isActive === 'on'
|
||||
};
|
||||
|
||||
// Create category
|
||||
const category = new BlogCategory(categoryData);
|
||||
await category.save();
|
||||
|
||||
req.flash('success_msg', 'Category created successfully');
|
||||
res.redirect('/admin/blog/categories');
|
||||
} catch (err) {
|
||||
console.error('Category store error:', err);
|
||||
req.flash('error_msg', 'Error creating category');
|
||||
res.redirect('/admin/blog/categories/create');
|
||||
}
|
||||
};
|
||||
|
||||
// Show edit category form
|
||||
exports.edit = async (req, res) => {
|
||||
try {
|
||||
const category = await BlogCategory.findById(req.params.id);
|
||||
|
||||
if (!category) {
|
||||
req.flash('error_msg', 'Category not found');
|
||||
return res.redirect('/admin/blog/categories');
|
||||
}
|
||||
|
||||
res.render('admin/blog/categories/edit', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Edit Category',
|
||||
category,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Category edit form error:', err);
|
||||
req.flash('error_msg', 'Error loading category');
|
||||
res.redirect('/admin/blog/categories');
|
||||
}
|
||||
};
|
||||
|
||||
// Update category
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const category = await BlogCategory.findById(req.params.id);
|
||||
|
||||
if (!category) {
|
||||
req.flash('error_msg', 'Category not found');
|
||||
return res.redirect('/admin/blog/categories');
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
isActive
|
||||
} = req.body;
|
||||
|
||||
// Update category data
|
||||
category.name = name;
|
||||
category.description = description;
|
||||
category.isActive = isActive === 'on';
|
||||
|
||||
// Generate new slug if name changed
|
||||
const newSlug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim('-');
|
||||
|
||||
if (newSlug !== category.slug) {
|
||||
const existingCategory = await BlogCategory.findOne({
|
||||
slug: newSlug,
|
||||
_id: { $ne: category._id }
|
||||
});
|
||||
if (existingCategory) {
|
||||
req.flash('error_msg', 'A category with this name already exists');
|
||||
return res.redirect(`/admin/blog/categories/${category._id}/edit`);
|
||||
}
|
||||
category.slug = newSlug;
|
||||
}
|
||||
|
||||
await category.save();
|
||||
|
||||
req.flash('success_msg', 'Category updated successfully');
|
||||
res.redirect('/admin/blog/categories');
|
||||
} catch (err) {
|
||||
console.error('Category update error:', err);
|
||||
req.flash('error_msg', 'Error updating category');
|
||||
res.redirect(`/admin/blog/categories/${req.params.id}/edit`);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete category
|
||||
exports.destroy = async (req, res) => {
|
||||
try {
|
||||
const category = await BlogCategory.findById(req.params.id);
|
||||
|
||||
if (!category) {
|
||||
req.flash('error_msg', 'Category not found');
|
||||
return res.redirect('/admin/blog/categories');
|
||||
}
|
||||
|
||||
// Check if category has posts
|
||||
const Blog = require('../models/blog');
|
||||
const postCount = await Blog.countDocuments({ category: { $in: [category.name] } });
|
||||
|
||||
if (postCount > 0) {
|
||||
req.flash('error_msg', 'Cannot delete category that has blog posts');
|
||||
return res.redirect('/admin/blog/categories');
|
||||
}
|
||||
|
||||
await BlogCategory.findByIdAndDelete(req.params.id);
|
||||
|
||||
req.flash('success_msg', 'Category deleted successfully');
|
||||
res.redirect('/admin/blog/categories');
|
||||
} catch (err) {
|
||||
console.error('Category delete error:', err);
|
||||
req.flash('error_msg', 'Error deleting category');
|
||||
res.redirect('/admin/blog/categories');
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------- Public API Controllers --------------------
|
||||
|
||||
// Get all active categories
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const categories = await BlogCategory.getActive();
|
||||
|
||||
// Update post counts
|
||||
for (const category of categories) {
|
||||
await category.updatePostCount();
|
||||
}
|
||||
|
||||
res.json(categories);
|
||||
} catch (err) {
|
||||
console.error('Categories API error:', err);
|
||||
res.status(500).json({ error: 'Error loading categories' });
|
||||
}
|
||||
};
|
||||
|
||||
// Get category by slug
|
||||
exports.apiShow = async (req, res) => {
|
||||
try {
|
||||
const category = await BlogCategory.findOne({
|
||||
slug: req.params.slug,
|
||||
isActive: true
|
||||
}).lean();
|
||||
|
||||
if (!category) {
|
||||
return res.status(404).json({ error: 'Category not found' });
|
||||
}
|
||||
|
||||
res.json(category);
|
||||
} catch (err) {
|
||||
console.error('Category show API error:', err);
|
||||
res.status(500).json({ error: 'Error loading category' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = exports;
|
||||
554
controllers/blogController.js
Normal file
554
controllers/blogController.js
Normal file
@@ -0,0 +1,554 @@
|
||||
const Blog = require('../models/blog');
|
||||
const BlogCategory = require('../models/blogCategory');
|
||||
const BlogTag = require('../models/blogTag');
|
||||
const BlogComment = require('../models/blogComment');
|
||||
const RecentPost = require('../models/recentPost');
|
||||
const { addBaseUrlToImages } = require('../utils/imageHelper');
|
||||
|
||||
// -------------------- Helper Functions --------------------
|
||||
|
||||
// Generate slug from title
|
||||
const generateSlug = (title) => {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim('-');
|
||||
};
|
||||
|
||||
// Update category post counts
|
||||
const updateCategoryPostCounts = async () => {
|
||||
const categories = await BlogCategory.find();
|
||||
for (const category of categories) {
|
||||
await category.updatePostCount();
|
||||
}
|
||||
};
|
||||
|
||||
// Update tag post counts
|
||||
const updateTagPostCounts = async () => {
|
||||
const tags = await BlogTag.find();
|
||||
for (const tag of tags) {
|
||||
await tag.updatePostCount();
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------- Admin Controllers --------------------
|
||||
|
||||
// Display blog management page
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build filter
|
||||
const filter = {};
|
||||
if (req.query.status) {
|
||||
filter.status = req.query.status;
|
||||
}
|
||||
if (req.query.category) {
|
||||
filter.category = req.query.category;
|
||||
}
|
||||
if (req.query.search) {
|
||||
filter.$or = [
|
||||
{ title: { $regex: req.query.search, $options: 'i' } },
|
||||
{ excerpt: { $regex: req.query.search, $options: 'i' } }
|
||||
];
|
||||
}
|
||||
|
||||
// Get blogs with pagination
|
||||
const blogs = await Blog.find(filter)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
const totalBlogs = await Blog.countDocuments(filter);
|
||||
const totalPages = Math.ceil(totalBlogs / limit);
|
||||
|
||||
// Get categories for filter
|
||||
const categories = await BlogCategory.getActive();
|
||||
|
||||
res.render('admin/blog/index', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Blog Management',
|
||||
blogs,
|
||||
categories,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
limit,
|
||||
totalItems: totalBlogs
|
||||
},
|
||||
query: req.query,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Blog index error:', err);
|
||||
req.flash('error_msg', 'Error loading blogs');
|
||||
res.redirect('/admin/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
// Show create blog form
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
const categories = await BlogCategory.getActive();
|
||||
const tags = await BlogTag.getActive();
|
||||
|
||||
res.render('admin/blog/create', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Create New Blog Post',
|
||||
categories,
|
||||
tags,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Blog create form error:', err);
|
||||
req.flash('error_msg', 'Error loading create form');
|
||||
res.redirect('/admin/blog');
|
||||
}
|
||||
};
|
||||
|
||||
// Store new blog
|
||||
exports.store = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
excerpt,
|
||||
content,
|
||||
category,
|
||||
tags,
|
||||
status,
|
||||
isFeatured,
|
||||
author,
|
||||
galleryImages
|
||||
} = req.body;
|
||||
|
||||
// Generate slug
|
||||
const slug = generateSlug(title);
|
||||
|
||||
// Check if slug exists
|
||||
const existingBlog = await Blog.findOne({ slug });
|
||||
if (existingBlog) {
|
||||
req.flash('error_msg', 'A blog post with this title already exists');
|
||||
return res.redirect('/admin/blog/create');
|
||||
}
|
||||
|
||||
// Create blog data
|
||||
const blogData = {
|
||||
title,
|
||||
slug,
|
||||
excerpt,
|
||||
content,
|
||||
category: category ? (Array.isArray(category) ? category : [category]) : [], // Array categories
|
||||
tags: tags ? (Array.isArray(tags) ? tags : [tags]) : [],
|
||||
status: status || 'published',
|
||||
isFeatured: isFeatured === 'on',
|
||||
author: author || 'Admin',
|
||||
galleryImages: galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : []
|
||||
};
|
||||
|
||||
// Handle featured image
|
||||
if (req.file) {
|
||||
blogData.featuredImage = `/uploads/blog/${req.file.filename}`;
|
||||
}
|
||||
|
||||
// Create blog
|
||||
const blog = new Blog(blogData);
|
||||
await blog.save();
|
||||
|
||||
// Update counts
|
||||
await updateCategoryPostCounts();
|
||||
await updateTagPostCounts();
|
||||
await RecentPost.syncFromBlogs();
|
||||
|
||||
req.flash('success_msg', 'Blog post created successfully');
|
||||
res.redirect('/admin/blog');
|
||||
} catch (err) {
|
||||
console.error('Blog store error:', err);
|
||||
req.flash('error_msg', 'Error creating blog post');
|
||||
res.redirect('/admin/blog/create');
|
||||
}
|
||||
};
|
||||
|
||||
// Show edit blog form
|
||||
exports.edit = async (req, res) => {
|
||||
try {
|
||||
const blog = await Blog.findById(req.params.id);
|
||||
|
||||
if (!blog) {
|
||||
req.flash('error_msg', 'Blog post not found');
|
||||
return res.redirect('/admin/blog');
|
||||
}
|
||||
|
||||
const categories = await BlogCategory.getActive();
|
||||
const tags = await BlogTag.getActive();
|
||||
|
||||
res.render('admin/blog/edit', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Edit Blog Post',
|
||||
blog,
|
||||
categories,
|
||||
tags,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Blog edit form error:', err);
|
||||
req.flash('error_msg', 'Error loading blog post');
|
||||
res.redirect('/admin/blog');
|
||||
}
|
||||
};
|
||||
|
||||
// Update blog
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const blog = await Blog.findById(req.params.id);
|
||||
|
||||
if (!blog) {
|
||||
req.flash('error_msg', 'Blog post not found');
|
||||
return res.redirect('/admin/blog');
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
excerpt,
|
||||
content,
|
||||
category,
|
||||
tags,
|
||||
status,
|
||||
isFeatured,
|
||||
author,
|
||||
galleryImages
|
||||
} = req.body;
|
||||
|
||||
// Update blog data
|
||||
blog.title = title;
|
||||
blog.excerpt = excerpt;
|
||||
blog.content = content;
|
||||
blog.category = category ? (Array.isArray(category) ? category : [category]) : []; // Array categories
|
||||
blog.tags = tags ? (Array.isArray(tags) ? tags : [tags]) : [];
|
||||
blog.status = status || 'published';
|
||||
blog.isFeatured = isFeatured === 'on';
|
||||
blog.author = author || 'Admin';
|
||||
blog.galleryImages = galleryImages ? (Array.isArray(galleryImages) ? galleryImages : [galleryImages]) : [];
|
||||
|
||||
// Handle featured image
|
||||
if (req.file) {
|
||||
blog.featuredImage = `/uploads/blog/${req.file.filename}`;
|
||||
}
|
||||
|
||||
// Generate new slug if title changed
|
||||
const newSlug = generateSlug(title);
|
||||
if (newSlug !== blog.slug) {
|
||||
const existingBlog = await Blog.findOne({ slug: newSlug, _id: { $ne: blog._id } });
|
||||
if (existingBlog) {
|
||||
req.flash('error_msg', 'A blog post with this title already exists');
|
||||
return res.redirect(`/admin/blog/${blog._id}/edit`);
|
||||
}
|
||||
blog.slug = newSlug;
|
||||
}
|
||||
|
||||
await blog.save();
|
||||
|
||||
// Update counts
|
||||
await updateCategoryPostCounts();
|
||||
await updateTagPostCounts();
|
||||
await RecentPost.syncFromBlogs();
|
||||
|
||||
req.flash('success_msg', 'Blog post updated successfully');
|
||||
res.redirect('/admin/blog');
|
||||
} catch (err) {
|
||||
console.error('Blog update error:', err);
|
||||
req.flash('error_msg', 'Error updating blog post');
|
||||
res.redirect(`/admin/blog/${req.params.id}/edit`);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete blog
|
||||
exports.destroy = async (req, res) => {
|
||||
try {
|
||||
const blog = await Blog.findById(req.params.id);
|
||||
|
||||
if (!blog) {
|
||||
req.flash('error_msg', 'Blog post not found');
|
||||
return res.redirect('/admin/blog');
|
||||
}
|
||||
|
||||
await Blog.findByIdAndDelete(req.params.id);
|
||||
|
||||
// Update counts
|
||||
await updateCategoryPostCounts();
|
||||
await updateTagPostCounts();
|
||||
await RecentPost.syncFromBlogs();
|
||||
|
||||
req.flash('success_msg', 'Blog post deleted successfully');
|
||||
res.redirect('/admin/blog');
|
||||
} catch (err) {
|
||||
console.error('Blog delete error:', err);
|
||||
req.flash('error_msg', 'Error deleting blog post');
|
||||
res.redirect('/admin/blog');
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------- Public API Controllers --------------------
|
||||
|
||||
// Get all published blogs for frontend
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build filter
|
||||
const filter = { status: 'published' };
|
||||
|
||||
if (req.query.category) {
|
||||
filter.category = { $in: [req.query.category] }; // Tìm trong array categories
|
||||
}
|
||||
|
||||
if (req.query.tag) {
|
||||
filter.tags = { $in: [req.query.tag] }; // Tìm trong array tags
|
||||
}
|
||||
|
||||
if (req.query.search) {
|
||||
filter.$or = [
|
||||
{ title: { $regex: req.query.search, $options: 'i' } },
|
||||
{ excerpt: { $regex: req.query.search, $options: 'i' } }
|
||||
];
|
||||
}
|
||||
|
||||
// Get blogs
|
||||
const blogs = await Blog.find(filter)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
const totalBlogs = await Blog.countDocuments(filter);
|
||||
|
||||
// Add base URL to images
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedBlogs = blogs.map(blog => addBaseUrlToImages(blog, baseUrl));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Blogs fetched successfully',
|
||||
data: {
|
||||
blogs: processedBlogs,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: Math.ceil(totalBlogs / limit),
|
||||
limit,
|
||||
totalItems: totalBlogs
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Blog API error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading blogs',
|
||||
error: err.message || 'Error loading blogs'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get single blog by slug
|
||||
exports.apiShow = async (req, res) => {
|
||||
try {
|
||||
const blog = await Blog.findOne({
|
||||
slug: req.params.slug,
|
||||
status: 'published'
|
||||
}).lean();
|
||||
|
||||
if (!blog) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Blog post not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Get comments for this post
|
||||
const comments = await BlogComment.getApprovedByPost(blog._id);
|
||||
|
||||
// Add comments to blog
|
||||
blog.comments = comments;
|
||||
|
||||
// Add base URL to images
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedBlog = addBaseUrlToImages(blog, baseUrl);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Blog post fetched successfully',
|
||||
data: processedBlog
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Blog show API error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading blog post',
|
||||
error: err.message || 'Error loading blog post'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get featured blogs
|
||||
exports.apiFeatured = async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 3;
|
||||
|
||||
const blogs = await Blog.getFeatured()
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
// Add base URL to images
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedBlogs = blogs.map(blog => addBaseUrlToImages(blog, baseUrl));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Featured blogs fetched successfully',
|
||||
data: processedBlogs
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Featured blogs API error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading featured blogs',
|
||||
error: err.message || 'Error loading featured blogs'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get recent blogs
|
||||
exports.apiRecent = async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 5;
|
||||
|
||||
// Try to get from RecentPost first
|
||||
let recentPosts = await RecentPost.getRecent(limit);
|
||||
|
||||
// If no recent posts, sync from blogs
|
||||
if (recentPosts.length === 0) {
|
||||
await RecentPost.syncFromBlogs(limit);
|
||||
recentPosts = await RecentPost.getRecent(limit);
|
||||
}
|
||||
|
||||
// Add base URL to images
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
const processedPosts = recentPosts.map(post => addBaseUrlToImages(post, baseUrl));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Recent blogs fetched successfully',
|
||||
data: processedPosts
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Recent blogs API error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading recent blogs',
|
||||
error: err.message || 'Error loading recent blogs'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get categories of a specific blog post
|
||||
exports.apiCategories = async (req, res) => {
|
||||
try {
|
||||
const mongoose = require('mongoose');
|
||||
let query;
|
||||
|
||||
// Check if it's a valid ObjectId
|
||||
if (mongoose.Types.ObjectId.isValid(req.params.id)) {
|
||||
query = { _id: req.params.id };
|
||||
} else {
|
||||
query = { slug: req.params.id };
|
||||
}
|
||||
|
||||
query.status = 'published';
|
||||
|
||||
const blog = await Blog.findOne(query).lean();
|
||||
|
||||
if (!blog) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Blog post not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Get category details
|
||||
const BlogCategory = require('../models/blogCategory');
|
||||
const categories = await BlogCategory.find({
|
||||
name: { $in: blog.category },
|
||||
isActive: true
|
||||
}).lean();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Blog categories fetched successfully',
|
||||
data: categories
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Blog categories API error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading blog categories',
|
||||
error: err.message || 'Error loading blog categories'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get tags of a specific blog post
|
||||
exports.apiTags = async (req, res) => {
|
||||
try {
|
||||
const mongoose = require('mongoose');
|
||||
let query;
|
||||
|
||||
// Check if it's a valid ObjectId
|
||||
if (mongoose.Types.ObjectId.isValid(req.params.id)) {
|
||||
query = { _id: req.params.id };
|
||||
} else {
|
||||
query = { slug: req.params.id };
|
||||
}
|
||||
|
||||
query.status = 'published';
|
||||
|
||||
const blog = await Blog.findOne(query).lean();
|
||||
|
||||
if (!blog) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Blog post not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Get tag details
|
||||
const BlogTag = require('../models/blogTag');
|
||||
const tags = await BlogTag.find({
|
||||
name: { $in: blog.tags },
|
||||
isActive: true
|
||||
}).lean();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Blog tags fetched successfully',
|
||||
data: tags
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Blog tags API error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error loading blog tags',
|
||||
error: err.message || 'Error loading blog tags'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = exports;
|
||||
239
controllers/blogTagController.js
Normal file
239
controllers/blogTagController.js
Normal file
@@ -0,0 +1,239 @@
|
||||
const BlogTag = require('../models/blogTag');
|
||||
|
||||
// -------------------- Admin Controllers --------------------
|
||||
|
||||
// Display tag management page
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
const tags = await BlogTag.find()
|
||||
.sort({ name: 1 })
|
||||
.lean();
|
||||
|
||||
res.render('admin/blog/tags/index', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Blog Tags',
|
||||
tags,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Tag index error:', err);
|
||||
req.flash('error_msg', 'Error loading tags');
|
||||
res.redirect('/admin/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
// Show create tag form
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
res.render('admin/blog/tags/create', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Create New Tag',
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Tag create form error:', err);
|
||||
req.flash('error_msg', 'Error loading create form');
|
||||
res.redirect('/admin/blog/tags');
|
||||
}
|
||||
};
|
||||
|
||||
// Store new tag
|
||||
exports.store = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
isActive
|
||||
} = req.body;
|
||||
|
||||
// Generate slug
|
||||
const slug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim('-');
|
||||
|
||||
// Check if slug exists
|
||||
const existingTag = await BlogTag.findOne({ slug });
|
||||
if (existingTag) {
|
||||
req.flash('error_msg', 'A tag with this name already exists');
|
||||
return res.redirect('/admin/blog/tags/create');
|
||||
}
|
||||
|
||||
// Create tag data
|
||||
const tagData = {
|
||||
name,
|
||||
slug,
|
||||
isActive: isActive === 'on'
|
||||
};
|
||||
|
||||
// Create tag
|
||||
const tag = new BlogTag(tagData);
|
||||
await tag.save();
|
||||
|
||||
req.flash('success_msg', 'Tag created successfully');
|
||||
res.redirect('/admin/blog/tags');
|
||||
} catch (err) {
|
||||
console.error('Tag store error:', err);
|
||||
req.flash('error_msg', 'Error creating tag');
|
||||
res.redirect('/admin/blog/tags/create');
|
||||
}
|
||||
};
|
||||
|
||||
// Show edit tag form
|
||||
exports.edit = async (req, res) => {
|
||||
try {
|
||||
const tag = await BlogTag.findById(req.params.id);
|
||||
|
||||
if (!tag) {
|
||||
req.flash('error_msg', 'Tag not found');
|
||||
return res.redirect('/admin/blog/tags');
|
||||
}
|
||||
|
||||
res.render('admin/blog/tags/edit', {
|
||||
layout: 'layouts/main',
|
||||
title: 'Edit Tag',
|
||||
tag,
|
||||
currentPath: req.path,
|
||||
user: req.session.user
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Tag edit form error:', err);
|
||||
req.flash('error_msg', 'Error loading tag');
|
||||
res.redirect('/admin/blog/tags');
|
||||
}
|
||||
};
|
||||
|
||||
// Update tag
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const tag = await BlogTag.findById(req.params.id);
|
||||
|
||||
if (!tag) {
|
||||
req.flash('error_msg', 'Tag not found');
|
||||
return res.redirect('/admin/blog/tags');
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
isActive
|
||||
} = req.body;
|
||||
|
||||
// Update tag data
|
||||
tag.name = name;
|
||||
tag.isActive = isActive === 'on';
|
||||
|
||||
// Generate new slug if name changed
|
||||
const newSlug = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim('-');
|
||||
|
||||
if (newSlug !== tag.slug) {
|
||||
const existingTag = await BlogTag.findOne({
|
||||
slug: newSlug,
|
||||
_id: { $ne: tag._id }
|
||||
});
|
||||
if (existingTag) {
|
||||
req.flash('error_msg', 'A tag with this name already exists');
|
||||
return res.redirect(`/admin/blog/tags/${tag._id}/edit`);
|
||||
}
|
||||
tag.slug = newSlug;
|
||||
}
|
||||
|
||||
await tag.save();
|
||||
|
||||
req.flash('success_msg', 'Tag updated successfully');
|
||||
res.redirect('/admin/blog/tags');
|
||||
} catch (err) {
|
||||
console.error('Tag update error:', err);
|
||||
req.flash('error_msg', 'Error updating tag');
|
||||
res.redirect(`/admin/blog/tags/${req.params.id}/edit`);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete tag
|
||||
exports.destroy = async (req, res) => {
|
||||
try {
|
||||
const tag = await BlogTag.findById(req.params.id);
|
||||
|
||||
if (!tag) {
|
||||
req.flash('error_msg', 'Tag not found');
|
||||
return res.redirect('/admin/blog/tags');
|
||||
}
|
||||
|
||||
// Check if tag has posts
|
||||
const Blog = require('../models/blog');
|
||||
const postCount = await Blog.countDocuments({ tags: { $in: [tag.name] } });
|
||||
|
||||
if (postCount > 0) {
|
||||
req.flash('error_msg', 'Cannot delete tag that is used in blog posts');
|
||||
return res.redirect('/admin/blog/tags');
|
||||
}
|
||||
|
||||
await BlogTag.findByIdAndDelete(req.params.id);
|
||||
|
||||
req.flash('success_msg', 'Tag deleted successfully');
|
||||
res.redirect('/admin/blog/tags');
|
||||
} catch (err) {
|
||||
console.error('Tag delete error:', err);
|
||||
req.flash('error_msg', 'Error deleting tag');
|
||||
res.redirect('/admin/blog/tags');
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------- Public API Controllers --------------------
|
||||
|
||||
// Get all active tags
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
const tags = await BlogTag.getActive();
|
||||
|
||||
// Update post counts
|
||||
for (const tag of tags) {
|
||||
await tag.updatePostCount();
|
||||
}
|
||||
|
||||
res.json(tags);
|
||||
} catch (err) {
|
||||
console.error('Tags API error:', err);
|
||||
res.status(500).json({ error: 'Error loading tags' });
|
||||
}
|
||||
};
|
||||
|
||||
// Get popular tags
|
||||
exports.apiPopular = async (req, res) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const tags = await BlogTag.getPopular(limit);
|
||||
res.json(tags);
|
||||
} catch (err) {
|
||||
console.error('Popular tags API error:', err);
|
||||
res.status(500).json({ error: 'Error loading popular tags' });
|
||||
}
|
||||
};
|
||||
|
||||
// Get tag by slug
|
||||
exports.apiShow = async (req, res) => {
|
||||
try {
|
||||
const tag = await BlogTag.findOne({
|
||||
slug: req.params.slug,
|
||||
isActive: true
|
||||
}).lean();
|
||||
|
||||
if (!tag) {
|
||||
return res.status(404).json({ error: 'Tag not found' });
|
||||
}
|
||||
|
||||
res.json(tag);
|
||||
} catch (err) {
|
||||
console.error('Tag show API error:', err);
|
||||
res.status(500).json({ error: 'Error loading tag' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = exports;
|
||||
@@ -1,5 +1,6 @@
|
||||
const { addBaseUrlToImages } = require("../utils/imageHelper");
|
||||
const Contact = require("../models/contact");
|
||||
const ContactSubmission = require("../models/contactSubmission");
|
||||
|
||||
// Get contact data from MongoDB
|
||||
const getContactData = async () => {
|
||||
@@ -60,6 +61,7 @@ exports.index = async (req, res) => {
|
||||
zoom: 15,
|
||||
location: "",
|
||||
markerTitle: "",
|
||||
embedUrl: "",
|
||||
tileLayer: {
|
||||
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
attribution: "",
|
||||
@@ -70,16 +72,38 @@ exports.index = async (req, res) => {
|
||||
form: {
|
||||
sectionLabel: "",
|
||||
heading: "",
|
||||
description: "",
|
||||
fields: [],
|
||||
submitButton: { text: "Send Message" },
|
||||
submitButton: { text: "Send Message", icon: "fa-solid fa-arrow-right", buttonClass: "theme-btn style-2" },
|
||||
},
|
||||
};
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
|
||||
|
||||
const { startDate, endDate } = req.query;
|
||||
const query = {};
|
||||
|
||||
if (startDate || endDate) {
|
||||
query.createdAt = {};
|
||||
if (startDate) {
|
||||
query.createdAt.$gte = new Date(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
// Set end date to end of day
|
||||
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,
|
||||
currentPath: req.path,
|
||||
user: req.session.user,
|
||||
@@ -140,6 +164,7 @@ exports.update = async (req, res) => {
|
||||
zoom: 15,
|
||||
location: "",
|
||||
markerTitle: "",
|
||||
embedUrl: "",
|
||||
tileLayer: {
|
||||
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
attribution: "",
|
||||
@@ -150,8 +175,9 @@ exports.update = async (req, res) => {
|
||||
form: formData || {
|
||||
sectionLabel: "",
|
||||
heading: "",
|
||||
description: "",
|
||||
fields: [],
|
||||
submitButton: { text: "Send Message" },
|
||||
submitButton: { text: "Send Message", icon: "fa-solid fa-arrow-right", buttonClass: "theme-btn style-2" },
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -179,3 +205,141 @@ exports.update = async (req, res) => {
|
||||
res.redirect("/admin/contact");
|
||||
}
|
||||
};
|
||||
|
||||
// API để submit contact form (từ frontend)
|
||||
exports.submitForm = async (req, res) => {
|
||||
try {
|
||||
const { name, email, phone, address, date, message } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!name || !email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: "Name and email are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Create new submission
|
||||
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);
|
||||
|
||||
// Handle validation errors
|
||||
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.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để lấy danh sách submissions (cho admin)
|
||||
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// API để cập nhật status của submission
|
||||
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
193
controllers/pricingController.js
Normal file
193
controllers/pricingController.js
Normal file
@@ -0,0 +1,193 @@
|
||||
const Pricing = require("../models/pricing");
|
||||
|
||||
// ==================== CMS ADMIN FUNCTIONS ====================
|
||||
|
||||
// Render admin page for pricing management
|
||||
exports.index = async (req, res) => {
|
||||
try {
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
|
||||
// If no data in DB, try to load from JSON file
|
||||
if (!pricing) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/pricing.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
pricing = await Pricing.migrateFromJson(jsonData);
|
||||
} else {
|
||||
// Create default pricing
|
||||
pricing = await Pricing.create({
|
||||
name: "default",
|
||||
hero: {
|
||||
title: "Pricing Plan",
|
||||
backgroundImage: "/assets/img/inner-page/breadcrumb.jpg",
|
||||
shapeImage: "/assets/img/inner-page/shape.png",
|
||||
breadcrumb: [
|
||||
{ text: "Home", link: "/" },
|
||||
{ text: "Pricing Plan", link: "" },
|
||||
],
|
||||
},
|
||||
pricingSection: {
|
||||
subtitle: "pricing plan",
|
||||
heading: "Flexible Plans to Suit Every Traveler",
|
||||
description: "Choose the plan that fits your visa needs and enjoy expert guidance every step of the way.",
|
||||
},
|
||||
plans: {
|
||||
monthly: [],
|
||||
yearly: [],
|
||||
},
|
||||
testimonials: {
|
||||
subtitle: "What Our Clients Say",
|
||||
heading: "Immigration Success Stories",
|
||||
buttonText: "View All Review",
|
||||
buttonLink: "/contact",
|
||||
buttonIcon: "fa-solid fa-arrow-right",
|
||||
image: "",
|
||||
items: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.render("admin/pricing/index", {
|
||||
layout: "layouts/main",
|
||||
title: "Pricing Management",
|
||||
data: pricing,
|
||||
user: req.session.user,
|
||||
frontendUrl: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading pricing admin page:", err);
|
||||
req.flash("error", "Error loading pricing data");
|
||||
res.redirect("/admin/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
// Update pricing data
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const { hero, pricingSection, plans, testimonials } = req.body;
|
||||
|
||||
// Parse JSON strings if needed
|
||||
const heroData = typeof hero === "string" ? JSON.parse(hero) : hero;
|
||||
const pricingSectionData = typeof pricingSection === "string" ? JSON.parse(pricingSection) : pricingSection;
|
||||
const plansData = typeof plans === "string" ? JSON.parse(plans) : plans;
|
||||
const testimonialsData = typeof testimonials === "string" ? JSON.parse(testimonials) : testimonials;
|
||||
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
|
||||
if (pricing) {
|
||||
pricing.hero = heroData;
|
||||
pricing.pricingSection = pricingSectionData;
|
||||
pricing.plans = plansData;
|
||||
pricing.testimonials = testimonialsData;
|
||||
await pricing.save();
|
||||
} else {
|
||||
pricing = await Pricing.create({
|
||||
name: "default",
|
||||
hero: heroData,
|
||||
pricingSection: pricingSectionData,
|
||||
plans: plansData,
|
||||
testimonials: testimonialsData,
|
||||
});
|
||||
}
|
||||
|
||||
req.flash("success", "Pricing data updated successfully");
|
||||
res.redirect("/admin/pricing");
|
||||
} catch (err) {
|
||||
console.error("Error updating pricing:", err);
|
||||
req.flash("error", "Error updating pricing data");
|
||||
res.redirect("/admin/pricing");
|
||||
}
|
||||
};
|
||||
|
||||
// API to get pricing data (admin)
|
||||
exports.getPricingData = async (req, res) => {
|
||||
try {
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
|
||||
if (!pricing) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/pricing.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
pricing = await Pricing.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: pricing,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting pricing data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading pricing data",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Public API to get pricing page data (for frontend)
|
||||
exports.api = async (req, res) => {
|
||||
try {
|
||||
let pricing = await Pricing.findOne({ name: "default" });
|
||||
|
||||
if (!pricing) {
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsonPath = path.join(__dirname, "../data/pricing.json");
|
||||
|
||||
if (fs.existsSync(jsonPath)) {
|
||||
const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
||||
pricing = await Pricing.migrateFromJson(jsonData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pricing) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "Pricing data not found",
|
||||
});
|
||||
}
|
||||
|
||||
const backendUrl = process.env.BACKEND_URL || "http://localhost:3001";
|
||||
|
||||
const getFullUrl = (path) => {
|
||||
if (!path || path.startsWith("http")) return path;
|
||||
return `${backendUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
||||
};
|
||||
|
||||
// Convert to plain object to modify properties safely
|
||||
const pricingData = pricing.toObject ? pricing.toObject() : pricing;
|
||||
|
||||
if (pricingData.hero) {
|
||||
pricingData.hero.backgroundImage = getFullUrl(pricingData.hero.backgroundImage);
|
||||
pricingData.hero.shapeImage = getFullUrl(pricingData.hero.shapeImage);
|
||||
}
|
||||
|
||||
if (pricingData.testimonials) {
|
||||
pricingData.testimonials.image = getFullUrl(pricingData.testimonials.image);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
hero: pricingData.hero,
|
||||
pricingSection: pricingData.pricingSection,
|
||||
plans: pricingData.plans,
|
||||
testimonials: pricingData.testimonials,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error getting pricing API data:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Error loading pricing data",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -13,7 +13,7 @@ const uploadController = {
|
||||
|
||||
// Lấy loại ảnh từ query params
|
||||
const imageType = req.query.imageType || 'general';
|
||||
|
||||
|
||||
// Tạo đường dẫn tương đối để lưu vào database
|
||||
const relativePath = `/uploads/${imageType}/${req.file.filename}`;
|
||||
const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`;
|
||||
@@ -41,7 +41,7 @@ const uploadController = {
|
||||
if (camp) {
|
||||
// Đồng bộ camps.image và camps.camp-detail.hero.bgImage - 2 trường này luôn giống nhau
|
||||
camp.image = relativePath;
|
||||
|
||||
|
||||
// Đảm bảo camp-detail.hero tồn tại và sync bgImage
|
||||
if (!camp['camp-detail']) camp['camp-detail'] = {};
|
||||
if (!camp['camp-detail'].hero) camp['camp-detail'].hero = {};
|
||||
@@ -73,59 +73,59 @@ const uploadController = {
|
||||
updateImagePath: async (req, res) => {
|
||||
try {
|
||||
const { jsonFile, jsonPath, newImagePath } = req.body;
|
||||
|
||||
|
||||
if (!jsonFile || !jsonPath || !newImagePath) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Missing required information (jsonFile, jsonPath, newImagePath)'
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Missing required information (jsonFile, jsonPath, newImagePath)'
|
||||
});
|
||||
}
|
||||
|
||||
// Đọc file JSON
|
||||
const jsonFilePath = path.join(__dirname, '../data', jsonFile);
|
||||
const jsonData = jsonHelper.readJsonFile(jsonFilePath);
|
||||
|
||||
|
||||
// Cập nhật đường dẫn ảnh theo jsonPath
|
||||
// jsonPath có định dạng như "banner.image" hoặc "partners[0].logo"
|
||||
const pathParts = jsonPath.split('.');
|
||||
let current = jsonData;
|
||||
|
||||
|
||||
// Duyệt qua các phần của path trừ phần cuối
|
||||
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||
const part = pathParts[i];
|
||||
|
||||
|
||||
// Kiểm tra nếu là mảng (ví dụ: partners[0])
|
||||
if (part.includes('[') && part.includes(']')) {
|
||||
const arrName = part.substring(0, part.indexOf('['));
|
||||
const index = parseInt(part.substring(part.indexOf('[') + 1, part.indexOf(']')));
|
||||
|
||||
|
||||
if (!current[arrName] || !Array.isArray(current[arrName])) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Array ${arrName} not found in data`
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Array ${arrName} not found in data`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
current = current[arrName][index];
|
||||
} else {
|
||||
if (!current[part]) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Property ${part} not found in data`
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Property ${part} not found in data`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
current = current[part];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Cập nhật giá trị
|
||||
const lastPart = pathParts[pathParts.length - 1];
|
||||
current[lastPart] = newImagePath;
|
||||
|
||||
|
||||
// Lưu lại file JSON
|
||||
jsonHelper.writeJsonFile(jsonFilePath, jsonData);
|
||||
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Image path updated successfully',
|
||||
@@ -141,22 +141,22 @@ const uploadController = {
|
||||
deleteImage: async (req, res) => {
|
||||
try {
|
||||
const { imagePath } = req.body;
|
||||
|
||||
|
||||
if (!imagePath) {
|
||||
return res.status(400).json({ success: false, message: 'Missing image path to delete' });
|
||||
}
|
||||
|
||||
|
||||
// Chuyển đổi đường dẫn tương đối thành đường dẫn tuyệt đối
|
||||
const fullPath = path.join(__dirname, '../public', imagePath);
|
||||
|
||||
|
||||
// Kiểm tra xem file có tồn tại không
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return res.status(404).json({ success: false, message: 'Image file not found' });
|
||||
}
|
||||
|
||||
|
||||
// Xóa file
|
||||
fs.unlinkSync(fullPath);
|
||||
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Image deleted successfully',
|
||||
|
||||
Reference in New Issue
Block a user