Merge branch 'main' of https://gits.techvanguard.vn/UKSOURCE/cms.hailearning.edu.vn into fea/thanh-02022026-news

This commit is contained in:
Wini_Fy
2026-02-04 09:22:24 +07:00
41 changed files with 7863 additions and 570 deletions

View 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",
});
}
};

View File

@@ -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",
});
}
};

View 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",
});
}
};

View File

@@ -0,0 +1,360 @@
const { getServiceData } = require("../services/service.service");
const Service = require("../models/service");
const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper");
const slugify = require("slugify");
// Admin page - Service list
exports.index = async (req, res) => {
try {
const data = await getServiceData();
console.log(data.services.items.image);
res.render("admin/service/index", {
title: "Service Management",
data,
layout: "layouts/main",
getFullImageUrl, // Truyền helper function vào view
});
} catch (err) {
console.error(err);
req.flash("error_msg", "Error loading service data");
res.redirect("/admin/dashboard");
}
};
// Admin page - Service edit
exports.edit = async (req, res) => {
try {
const { slug } = req.params;
const data = await getServiceData();
const service = data.services?.items?.find((item) => item.slug === slug);
if (!service) {
req.flash("error_msg", "Service not found");
return res.redirect("/admin/service");
}
res.render("admin/service/edit", {
title: `Edit Service - ${service.name}`,
service,
layout: "layouts/main",
getFullImageUrl,
});
} catch (err) {
console.error(err);
req.flash("error_msg", "Error loading service for editing");
res.redirect("/admin/service");
}
};
// Update single service
exports.updateService = async (req, res) => {
try {
const { slug } = req.params;
const currentData = await getServiceData();
const serviceIndex = currentData.services?.items?.findIndex(
(item) => item.slug === slug,
);
if (serviceIndex === -1) {
req.flash("error_msg", "Service not found");
return res.redirect("/admin/service");
}
// Update service data
const updatedData = { ...currentData.toObject?.() };
updatedData.services.items[serviceIndex] = {
...updatedData.services.items[serviceIndex],
name: req.body.name,
slug: req.body.slug,
description: req.body.description,
image: req.body.image,
layout: req.body.layout,
};
if (currentData._id) {
await Service.findByIdAndUpdate(currentData._id, updatedData);
} else {
await Service.create(updatedData);
}
req.flash("success_msg", "Service updated successfully");
res.redirect("/admin/service");
} catch (err) {
console.error(err);
req.flash("error_msg", err.message);
res.redirect("/admin/service");
}
};
// Admin page - Service details
exports.details = async (req, res) => {
try {
const { slug } = req.params;
const data = await getServiceData();
const service = data.services?.items?.find((item) => item.slug === slug);
if (!service) {
req.flash("error_msg", "Service not found");
return res.redirect("/admin/service");
}
res.render("admin/service/details", {
title: `Service Details - ${service.name}`,
service,
layout: "layouts/main",
getFullImageUrl, // Truyền helper function vào view
});
} catch (err) {
console.error(err);
req.flash("error_msg", "Error loading service details");
res.redirect("/admin/service");
}
};
// Update service list
exports.update = async (req, res) => {
try {
const currentData = await getServiceData();
const sections = [
"pageTitle",
"services",
"destinations",
"visas",
"reviews",
];
let updatedData = { ...currentData.toObject?.() };
let hasChanges = false;
sections.forEach((section) => {
if (!req.body[section]) return;
const newData = JSON.parse(req.body[section]);
if (JSON.stringify(newData) !== JSON.stringify(currentData[section])) {
updatedData[section] = newData;
hasChanges = true;
}
});
if (!hasChanges) {
req.flash("info_msg", "No changes were made");
return res.redirect("/admin/service");
}
if (currentData._id) {
await Service.findByIdAndUpdate(currentData._id, updatedData);
} else {
await Service.create(updatedData);
}
req.flash("success_msg", "Service updated successfully");
res.redirect("/admin/service");
} catch (err) {
console.error(err);
req.flash("error_msg", err.message);
res.redirect("/admin/service");
}
};
// Update service details
exports.updateDetails = async (req, res) => {
try {
const { slug } = req.params;
const currentData = await getServiceData();
const serviceIndex = currentData.services?.items?.findIndex(
(item) => item.slug === slug,
);
if (serviceIndex === -1) {
req.flash("error_msg", "Service not found");
return res.redirect("/admin/service");
}
// Parse features and FAQ from JSON strings
const features = req.body.features ? JSON.parse(req.body.features) : [];
const faq = req.body.faq ? JSON.parse(req.body.faq) : [];
// Update service details
const updatedData = { ...currentData.toObject?.() };
updatedData.services.items[serviceIndex].details = {
title: req.body.title,
description: req.body.description,
mainImage: req.body.mainImage,
overviewTitle: req.body.overviewTitle,
overviewDescription: req.body.overviewDescription,
additionalDescription: req.body.additionalDescription,
keyFeaturesTitle: req.body.keyFeaturesTitle,
keyFeaturesImage: req.body.keyFeaturesImage,
features: features,
faqTitle: req.body.faqTitle,
faqImage: req.body.faqImage,
faq: faq,
};
if (currentData._id) {
await Service.findByIdAndUpdate(currentData._id, updatedData);
} else {
await Service.create(updatedData);
}
req.flash("success_msg", "Service details updated successfully");
res.redirect(`/admin/service/${slug}/details`);
} catch (err) {
console.error(err);
req.flash("error_msg", err.message);
res.redirect("/admin/service");
}
};
// API endpoint
exports.api = async (req, res) => {
try {
const serviceData = await getServiceData();
const baseUrl =
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
const processedData = addBaseUrlToImages(serviceData, baseUrl);
res.json(processedData);
} catch (err) {
res.status(500).json({ error: "Error loading service data" });
}
};
/**
* Get service details by slug - API endpoint
*/
exports.getServiceBySlug = async (req, res) => {
try {
const { slug } = req.params;
const serviceDoc = await Service.findOne().lean();
if (!serviceDoc) {
return res.status(404).json({
success: false,
message: "Service data not found",
});
}
// Find service by slug
const service = serviceDoc.services?.items?.find(
(item) => item.slug === slug,
);
if (!service) {
return res.status(404).json({
success: false,
message: `Service with slug '${slug}' not found`,
});
}
const baseUrl =
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
// Return service details in the expected format
const responseData = {
pageTitle: serviceDoc.pageTitle,
breadcrumb: {
...serviceDoc.breadcrumb,
title: "Service Details",
items: [
{ label: "Home", href: "/" },
{ label: "Services", href: "/services" },
{ label: service.name, href: `/services/${slug}` },
],
},
serviceDetails: {
content: service.details,
keyFeatures: {
title: service.details.keyFeaturesTitle || "Key Features",
sideImage: service.details.keyFeaturesImage || "img/default.jpg",
items: service.details.features || [],
},
faq: {
title: service.details.faqTitle || "Frequently Asked Questions",
sideImage: service.details.faqImage || "img/default.jpg",
items: service.details.faq || [],
},
},
};
const processedData = addBaseUrlToImages(responseData, baseUrl);
res.json(processedData);
} catch (error) {
console.error("Error fetching service by slug:", error);
res.status(500).json({
success: false,
message: "Internal server error",
error: error.message,
});
}
};
/**
* Generate slug from text - API endpoint
*/
exports.generateSlug = async (req, res) => {
try {
const { text } = req.body;
if (!text || typeof text !== "string") {
return res.status(400).json({
success: false,
message: "Text is required",
});
}
// Generate slug using slugify library with Vietnamese support
const slug = slugify(text, {
lower: true,
strict: true,
locale: "vi",
});
res.json({
success: true,
slug: slug,
});
} catch (error) {
console.error("Error generating slug:", error);
res.status(500).json({
success: false,
message: "Internal server error",
error: error.message,
});
}
};
/**
* Get all service slugs - API endpoint
*/
exports.getServiceSlugs = async (req, res) => {
try {
const serviceDoc = await Service.findOne().lean();
if (!serviceDoc?.services?.items) {
return res.json({
success: true,
slugs: [],
});
}
const slugs = serviceDoc.services.items.map((item) => ({
slug: item.slug,
name: item.name,
id: item.id,
}));
res.json({
success: true,
slugs,
});
} catch (error) {
console.error("Error fetching service slugs:", error);
res.status(500).json({
success: false,
message: "Internal server error",
error: error.message,
});
}
};

View File

@@ -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',