const { getServiceData } = require("../services/service.service"); const Service = require("../models/service"); const syncServiceMenu = require("../services/syncServiceMenu"); const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper"); const writeAuditLog = require("../audit/writeAuditLog"); const diffObject = require("../audit/diffObject"); const AUDIT_ACTIONS = require("../constants/auditAction"); 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"); } const oldItem = JSON.parse( JSON.stringify(currentData.services.items[serviceIndex]), ); // 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); } const newItem = updatedData.services.items[serviceIndex]; const changes = diffObject(oldItem, newItem); console.log("USER:", req.session?.user || req.user || "No user found"); await writeAuditLog({ model: "Service", documentId: currentData._id, action: AUDIT_ACTIONS.UPDATE_SERVICE, before: oldItem, after: newItem, changes, req, }); // Sync header menu children to reflect updated service name/slug await syncServiceMenu(updatedData.services?.items || []); 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); } // Sync header menu children to reflect current service list await syncServiceMenu(updatedData.services?.items || []); 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"); } const beforeDetails = JSON.parse( JSON.stringify(currentData.services.items[serviceIndex].details || {}), ); // 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?.() }; const updatedDetails = { 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, faqTitle: req.body.faqTitle, faqImage: req.body.faqImage, faq, }; updatedData.services.items[serviceIndex].details = updatedDetails; if (currentData._id) { await Service.findByIdAndUpdate(currentData._id, updatedData); } else { await Service.create(updatedData); } const changes = diffObject(beforeDetails, updatedDetails); if (changes.length > 0) { await writeAuditLog({ model: "Service", documentId: currentData._id, action: AUDIT_ACTIONS.UPDATE_SERVICE_DETAILS, before: beforeDetails, after: updatedDetails, changes, req, }); } 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, }); } };