diff --git a/.gitignore b/.gitignore index cb92d84..fe5c5c5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,7 @@ pids #cursor .cursor -package-lock.json \ No newline at end of file +package-lock.json + +.vscode +.kiro/ diff --git a/controllers/aboutController.js b/controllers/aboutController.js new file mode 100644 index 0000000..1a71bf9 --- /dev/null +++ b/controllers/aboutController.js @@ -0,0 +1,159 @@ +const About = require("../models/about"); +const { addBaseUrlToImages } = require("../utils/imageHelper"); + +// Helpers +const getAboutDoc = async () => About.findOne().sort({ updatedAt: -1 }); +const getAboutData = async () => (await getAboutDoc())?.toObject() || {}; + +const getDefaultAboutData = () => ({ + hero: { + badge: "", + title: "", + description: "", + studentCount: "", + image: "", + imageAlt: "", + coreValues: [], + }, + leadership: { + heading: "", + description: "", + members: [], + }, + learningModel: { + heading: "", + description: "", + async: { + title: "", + description: "", + features: [], + }, + sync: { + title: "", + description: "", + features: [], + }, + }, + accreditation: { + heading: "", + description: "", + badges: [], + stats: [], + }, + successStories: { + heading: "", + description: "", + stories: [], + }, + cta: { + heading: "", + description: "", + primaryButton: { label: "", href: "" }, + secondaryButton: { label: "", href: "" }, + }, +}); + +// Admin: render management view with data from MongoDB +exports.index = async (req, res) => { + try { + let data = await getAboutData(); + const defaults = getDefaultAboutData(); + + // Merge defaults for any missing sections + const sections = Object.keys(defaults); + sections.forEach((s) => { + data[s] = data[s] || defaults[s]; + }); + + return res.render("admin/about/index", { + layout: "layouts/main", + title: "About Management", + data, + currentPath: req.path, + user: req.session.user, + }); + } catch (err) { + console.error("About index error:", err); + req.flash("error_msg", "Error loading about data"); + return req.session.save(() => res.redirect("/admin/dashboard")); + } +}; + +// Admin: parse req.body sections and save to MongoDB +exports.update = async (req, res) => { + try { + const sections = [ + "hero", + "leadership", + "learningModel", + "accreditation", + "successStories", + "cta", + ]; + + let doc = await getAboutDoc(); + + if (!doc) { + doc = new About({}); + } + + let hasChanges = false; + + for (const section of sections) { + if (req.body[section]) { + try { + const payload = JSON.parse(req.body[section]); + doc[section] = payload; + doc.markModified(section); + hasChanges = true; + } catch (e) { + console.error(`Invalid JSON for ${section}:`, e); + } + } + } + + if (!hasChanges) { + req.flash("info_msg", "No changes were made"); + return req.session.save(() => res.redirect("/admin/about-us")); + } + + await doc.save(); + + req.flash("success_msg", "About page configuration has been updated!"); + return req.session.save(() => res.redirect("/admin/about-us")); + } catch (err) { + console.error("About update error:", err); + req.flash("error_msg", `Update error: ${err.message}`); + return req.session.save(() => res.redirect("/admin/about-us")); + } +}; + +// Recursively prepend BACKEND_URL to any relative /uploads/... paths in an object +function resolveImageUrls(obj, baseUrl) { + if (!obj || typeof obj !== "object") return obj; + if (Array.isArray(obj)) return obj.map((item) => resolveImageUrls(item, baseUrl)); + const result = {}; + for (const [key, val] of Object.entries(obj)) { + if (typeof val === "string" && val.startsWith("/uploads/")) { + result[key] = `${baseUrl}${val}`; + } else { + result[key] = resolveImageUrls(val, baseUrl); + } + } + return result; +} + +// Public API: return JSON data for frontend +exports.api = async (req, res) => { + try { + let data = await getAboutData(); + if (!data || Object.keys(data).length === 0) { + data = getDefaultAboutData(); + } + const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`; + return res.json(resolveImageUrls(data, baseUrl)); + } catch (err) { + console.error("About API error:", err); + return res.status(500).json({ error: "Error loading about data" }); + } +}; diff --git a/controllers/appointmentController.js b/controllers/appointmentController.js deleted file mode 100644 index cfd9b09..0000000 --- a/controllers/appointmentController.js +++ /dev/null @@ -1,450 +0,0 @@ -const AppointmentSubmission = require("../models/appointmentSubmission"); -const Appointment = require("../models/appointment"); -const writeAuditLog = require("../audit/writeAuditLog"); -const diffObject = require("../audit/diffObject"); -const AUDIT_ACTIONS = require("../constants/auditAction"); - -// ==================== 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" }); - - // Capture before state for audit logging - const beforeState = appointment - ? JSON.parse(JSON.stringify(appointment.toObject())) - : null; - - 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, - }); - } - - // Capture after state for audit logging - const afterState = JSON.parse(JSON.stringify(appointment.toObject())); - - // Generate changes diff - const changes = beforeState ? diffObject(beforeState, afterState) : []; - - // Write audit log - await writeAuditLog({ - model: "Appointment", - documentId: appointment._id, - action: AUDIT_ACTIONS.UPDATE_APPOINTMENT, - before: beforeState, - after: afterState, - changes, - req, - }); - - 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", - }); - } - - // Get the appointment before update for audit logging - const beforeAppointment = await AppointmentSubmission.findById(id); - if (!beforeAppointment) { - return res.status(404).json({ - success: false, - error: "Appointment not found", - }); - } - - const beforeState = JSON.parse( - JSON.stringify(beforeAppointment.toObject()), - ); - - 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 }, - ); - - // Capture after state for audit logging - const afterState = JSON.parse(JSON.stringify(appointment.toObject())); - - // Generate changes diff - const changes = diffObject(beforeState, afterState); - - // Write audit log - await writeAuditLog({ - model: "AppointmentSubmission", - documentId: appointment._id, - action: AUDIT_ACTIONS.UPDATE_APPOINTMENT_STATUS, - before: beforeState, - after: afterState, - changes, - req, - }); - - 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; - - // Get the appointment before deletion for audit logging - const appointment = await AppointmentSubmission.findById(id); - if (!appointment) { - return res.status(404).json({ - success: false, - error: "Appointment not found", - }); - } - - const beforeState = JSON.parse(JSON.stringify(appointment.toObject())); - - // Delete the appointment - await AppointmentSubmission.findByIdAndDelete(id); - - // Write audit log - await writeAuditLog({ - model: "AppointmentSubmission", - documentId: appointment._id, - action: AUDIT_ACTIONS.DELETE_APPOINTMENT, - before: beforeState, - after: null, - changes: [], - req, - }); - - 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", - }); - } -}; diff --git a/controllers/bookingController.js b/controllers/bookingController.js deleted file mode 100644 index ccca6fb..0000000 --- a/controllers/bookingController.js +++ /dev/null @@ -1,549 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { addBaseUrlToImages } = require("../utils/imageHelper"); -const Booking = require("../models/booking"); - -// -------------------- Public helpers -------------------- -const getBookingData = async () => { - const booking = await Booking.findOne().sort({ updatedAt: -1 }); - return booking ? (booking.toObject ? booking.toObject() : booking) : null; -}; - -// Load static booking JSON from `data/booking.json` (if present) -const loadStaticBooking = () => { - try { - const p = path.join(__dirname, '..', 'data', 'booking.json'); - if (!fs.existsSync(p)) return null; - const raw = fs.readFileSync(p, 'utf8'); - return JSON.parse(raw); - } catch (e) { - console.error('booking.loadStaticBooking error:', e && e.message); - return null; - } -}; - -// Normalize booking shape: ensure configuration exists with discounts/vouchers -const normalizeBookingShape = (booking) => { - if (!booking || typeof booking !== 'object') return booking; - const b = JSON.parse(JSON.stringify(booking)); - - if (!b.configuration || typeof b.configuration !== 'object') { - b.configuration = { currency: 'USD', discounts: [], vouchers: [] }; - } - - // Ensure configuration.discounts and configuration.vouchers exist - if (!Array.isArray(b.configuration.discounts)) { - b.configuration.discounts = []; - } - if (!Array.isArray(b.configuration.vouchers)) { - b.configuration.vouchers = []; - } - - return b; -}; - -// Deep merge: properties from `overrides` replace / merge into `base`. -const deepMerge = (base, overrides) => { - if (overrides === undefined) return base; - if (base === undefined || base === null) return overrides; - if (Array.isArray(overrides)) return overrides; - if (typeof overrides !== 'object' || overrides === null) return overrides; - const out = Object.assign({}, base); - Object.keys(overrides).forEach((k) => { - if (Array.isArray(overrides[k]) || typeof overrides[k] !== 'object' || overrides[k] === null) { - out[k] = overrides[k]; - } else { - out[k] = deepMerge(base[k], overrides[k]); - } - }); - return out; -}; - -// Ensure booking data fields have the expected shapes to avoid runtime errors -const sanitizeBookingData = (raw) => { - const defaults = { - hero: { title: '', backgroundImage: '' }, - searchBar: { locationLabel: '', holidaySeasonLabel: '', searchButtonText: '' }, - filterPanel: { - title: '', - priceTitle: '', - priceLabel: '', - pricePlaceholder: '', - priceMin: 0, - priceMax: 0, - ageTitle: '', - ageMin: 0, - ageMax: 0, - ageSelectPlaceholder: '', - activitiesTitle: '', - ratingTitle: '', - ratingOptions: [], - resetButtonText: '' - }, - programs: [], - holidays: [], - locations: [], - camps: [], - configuration: { currency: 'USD', discounts: [], vouchers: [] }, - formSteps: [], - validation: {} - }; - - if (!raw || typeof raw !== 'object') return defaults; - - // Use raw data first, then fill in missing fields with defaults - const safe = Object.assign({}, raw); - - // Ensure nested objects/arrays have correct types (use raw data if valid, otherwise defaults) - safe.hero = (safe.hero && typeof safe.hero === 'object') ? safe.hero : defaults.hero; - safe.searchBar = (safe.searchBar && typeof safe.searchBar === 'object') ? safe.searchBar : defaults.searchBar; - safe.filterPanel = (safe.filterPanel && typeof safe.filterPanel === 'object') ? safe.filterPanel : defaults.filterPanel; - - if (!Array.isArray(safe.filterPanel.ratingOptions)) safe.filterPanel.ratingOptions = defaults.filterPanel.ratingOptions; - - safe.programs = Array.isArray(safe.programs) ? safe.programs : defaults.programs; - safe.holidays = Array.isArray(safe.holidays) ? safe.holidays : defaults.holidays; - safe.locations = Array.isArray(safe.locations) ? safe.locations : defaults.locations; - safe.camps = Array.isArray(safe.camps) ? safe.camps : defaults.camps; - - // Ensure configuration has proper structure - if (!safe.configuration || typeof safe.configuration !== 'object') { - safe.configuration = defaults.configuration; - } - if (!Array.isArray(safe.configuration.discounts)) { - safe.configuration.discounts = defaults.configuration.discounts; - } - if (!Array.isArray(safe.configuration.vouchers)) { - safe.configuration.vouchers = defaults.configuration.vouchers; - } - - // Ensure formSteps and validation have correct types - safe.formSteps = Array.isArray(safe.formSteps) ? safe.formSteps : defaults.formSteps; - safe.validation = (safe.validation && typeof safe.validation === 'object' && !Array.isArray(safe.validation)) ? safe.validation : defaults.validation; - - return safe; -}; - -// Safe JSON parse with better error handling - handles double-encoded JSON and JS object notation -const safeParse = (value, fieldName = 'unknown') => { - // If already an object or array, return as-is - if (typeof value === 'object' && value !== null) { - return value; - } - - // If string, try to parse - if (typeof value === 'string') { - try { - let cleaned = value.trim(); - - // Check if it looks like JavaScript object notation (has single quotes or unquoted keys) - if (cleaned.includes("'") || /\{\s*\w+:/.test(cleaned)) { - console.warn(`safeParse: Converting JS notation to JSON for "${fieldName}"`); - - // Aggressive conversion approach - cleaned = cleaned - .replace(/'/g, '"') // Replace ALL single quotes with double quotes - .replace(/\r?\n|\r/g, ' ') // Remove all newlines - .replace(/\s+/g, ' ') // Normalize multiple spaces to single space - .replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas before } or ] - .replace(/([{,]\s*)(\w+):/g, '$1"$2":'); // Quote unquoted object keys - } - - // Try parsing - let parsed = JSON.parse(cleaned); - - // If result is still a string, try parsing again (double-encoded) - if (typeof parsed === 'string') { - console.warn(`safeParse: Double-encoded JSON detected for "${fieldName}"`); - parsed = JSON.parse(parsed); - } - - return parsed; - } catch (e) { - console.error(`safeParse: Failed to parse field "${fieldName}"`, { - error: e.message, - valuePreview: value.substring(0, 200) - }); - - throw new Error(`Invalid JSON format for field: ${fieldName}. Error: ${e.message}`); - } - } - - // For other types, return empty array or object - console.warn(`safeParse: Unexpected type for field "${fieldName}": ${typeof value}`); - return Array.isArray(value) ? [] : {}; -}; - -// Validate booking data structure -const validateBookingData = (data) => { - const errors = []; - - // Check required fields - if (!data.hero || typeof data.hero !== 'object') { - errors.push('Hero data is required and must be an object'); - } - - if (!data.searchBar || typeof data.searchBar !== 'object') { - errors.push('SearchBar data is required and must be an object'); - } - - if (!data.filterPanel || typeof data.filterPanel !== 'object') { - errors.push('FilterPanel data is required and must be an object'); - } - - // Validate arrays - if (data.programs && !Array.isArray(data.programs)) { - errors.push('Programs must be an array'); - } - - if (data.holidays && !Array.isArray(data.holidays)) { - errors.push('Holidays must be an array'); - } - - if (data.locations && !Array.isArray(data.locations)) { - errors.push('Locations must be an array'); - } - - if (data.camps && !Array.isArray(data.camps)) { - errors.push('Camps must be an array'); - } - - // Validate configuration structure - if (data.configuration) { - if (typeof data.configuration !== 'object') { - errors.push('Configuration must be an object'); - } else { - if (data.configuration.discounts && !Array.isArray(data.configuration.discounts)) { - errors.push('Configuration.discounts must be an array'); - } - if (data.configuration.vouchers && !Array.isArray(data.configuration.vouchers)) { - errors.push('Configuration.vouchers must be an array'); - } - } - } - - // Validate formSteps and validation structure if provided - if (data.formSteps && !Array.isArray(data.formSteps)) { - errors.push('formSteps must be an array'); - } - - if (data.validation && (typeof data.validation !== 'object' || Array.isArray(data.validation))) { - errors.push('validation must be an object'); - } - - return { - isValid: errors.length === 0, - errors - }; -}; - -// -------------------- Public endpoints -------------------- -// Public endpoint: return Booking JSON -exports.page = async (req, res) => { - try { - const dbBooking = await getBookingData(); - const staticBooking = loadStaticBooking(); - - // Normalize shapes so `configuration.discounts`/`configuration.vouchers` exist - const normStatic = normalizeBookingShape(staticBooking); - const normDb = normalizeBookingShape(dbBooking); - - // Build final payload according to BOOKING_MODE env var - const finalBooking = getFinalBooking(normStatic, normDb); - - if (!finalBooking) { - return res.status(404).json({ - error: "No booking data found", - message: "Please configure booking data in admin panel" - }); - } - - const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; - const processed = addBaseUrlToImages(finalBooking, baseUrl); - - return res.json(processed); - } catch (err) { - console.error("booking.page error:", err); - return res.status(500).json({ - error: "Error loading booking data", - message: process.env.NODE_ENV === 'development' ? err.message : undefined - }); - } -}; - - -// API endpoint to return booking JSON -exports.api = async (req, res) => { - try { - const dbBooking = await getBookingData(); - const staticBooking = loadStaticBooking(); - - // Normalize shapes so `configuration.discounts`/`configuration.vouchers` exist - const normStatic = normalizeBookingShape(staticBooking); - const normDb = normalizeBookingShape(dbBooking); - - const finalBooking = getFinalBooking(normStatic, normDb); - - if (!finalBooking) { - return res.status(404).json({ - error: "No booking data found", - message: "Please configure booking data in admin panel" - }); - } - - const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get('host')}`; - const processed = addBaseUrlToImages(finalBooking, baseUrl); - - return res.json(processed); - } catch (err) { - console.error("booking.api error:", err); - return res.status(500).json({ - error: "Error loading booking data", - message: process.env.NODE_ENV === 'development' ? err.message : undefined - }); - } -}; - -// -------------------- Admin endpoints -------------------- -// Display Booking management page -exports.index = async (req, res) => { - try { - const dbBooking = await getBookingData(); - const staticBooking = loadStaticBooking(); - - // Merge static booking with DB data (use same merge logic as public endpoints) - const normStatic = normalizeBookingShape(staticBooking); - const normDb = normalizeBookingShape(dbBooking); - const mergedData = getFinalBooking(normStatic, normDb); - - // Normalize again after merge to ensure discounts/vouchers are synced to top-level - const data = normalizeBookingShape(mergedData); - - // Sanitize data to ensure nested fields are objects/arrays (fixes malformed DB entries) - const safeData = sanitizeBookingData(data); - - res.render("admin/booking/index", { - layout: "layouts/main", - title: "Booking Management", - data: safeData, - frontendUrl: process.env.FRONTEND_URL || req.protocol + "://" + req.get("host"), - currentPath: req.path, - user: req.session.user, - }); - } catch (err) { - console.error("booking.index error:", err); - req.flash("error_msg", "Error loading booking page"); - res.redirect("/admin/dashboard"); - } -}; - -// Update booking data -exports.update = async (req, res) => { - try { - const { id } = req.params; - // ADD THIS DEBUG LOG - console.log('=== RAW REQUEST BODY ==='); - console.log('Discounts type:', typeof req.body.discounts); - console.log('Discounts first 500 chars:', req.body.discounts?.substring(0, 500)); - console.log('Vouchers type:', typeof req.body.vouchers); - console.log('Vouchers first 500 chars:', req.body.vouchers?.substring(0, 500)); - console.log('========================'); - const { - hero, - searchBar, - filterPanel, - programs, - holidays, - locations, - camps, - discounts, - vouchers, - formSteps, - validation: validationRaw - } = req.body; - - // Parse JSON strings - const errors = []; - let updateData = {}; - - try { - console.log('Raw discounts from req.body:', typeof discounts, discounts); - console.log('Raw vouchers from req.body:', typeof vouchers, vouchers); - - const parsedDiscounts = safeParse(discounts, 'discounts'); - const parsedVouchers = safeParse(vouchers, 'vouchers'); - - console.log('Parsed discounts:', typeof parsedDiscounts, Array.isArray(parsedDiscounts), parsedDiscounts); - console.log('Parsed vouchers:', typeof parsedVouchers, Array.isArray(parsedVouchers), parsedVouchers); - - updateData = { - hero: safeParse(hero, 'hero'), - searchBar: safeParse(searchBar, 'searchBar'), - filterPanel: safeParse(filterPanel, 'filterPanel'), - programs: safeParse(programs, 'programs'), - holidays: safeParse(holidays, 'holidays'), - locations: safeParse(locations, 'locations'), - camps: safeParse(camps, 'camps'), - formSteps: safeParse(formSteps, 'formSteps'), - validation: safeParse(validationRaw, 'validation'), - configuration: { - currency: 'USD', - discounts: parsedDiscounts, - vouchers: parsedVouchers - } - }; - } catch (parseError) { - console.error('booking.update: Parse error', parseError); - req.flash("error_msg", `Data processing error: ${parseError.message}`); - return req.session.save(() => res.redirect("/admin/booking")); - } - - // Validate data structure - const validation = validateBookingData(updateData); - if (!validation.isValid) { - console.error('booking.update: Validation failed', validation.errors); - req.flash("error_msg", `Validation failed: ${validation.errors[0]}`); - return req.session.save(() => res.redirect("/admin/booking")); - } - - console.log('Final updateData keys:', Object.keys(updateData)); - console.log('updateData.discounts:', updateData.discounts); - console.log('updateData.configuration:', updateData.configuration); - - // CRITICAL: Remove any top-level discounts/vouchers to prevent schema conflicts - // These should ONLY exist in configuration object - delete updateData.discounts; - delete updateData.vouchers; - - // Update or create booking document - let result; - try { - if (id && id !== 'undefined') { - result = await Booking.findByIdAndUpdate( - id, - { - ...updateData, - $unset: { discounts: "", vouchers: "" } // Remove old top-level fields - }, - { - new: true, - runValidators: false, // TẮT validator để tránh lỗi cast - strict: false // TẮT strict mode - } - ); - - if (!result) { - req.flash("error_msg", "Booking document not found"); - return req.session.save(() => res.redirect("/admin/booking")); - } - } else { - // Upsert: update existing or create new - result = await Booking.findOneAndUpdate( - {}, - { - ...updateData, - $unset: { discounts: "", vouchers: "" } // Remove old top-level fields - }, - { - upsert: true, - new: true, - runValidators: false, // TẮT validator - strict: false // TẮT strict mode - } - ); - } - - req.flash("success_msg", "Booking data updated successfully"); - return req.session.save(() => res.redirect("/admin/booking")); - } catch (dbError) { - console.error("booking.update: Database error", dbError); - req.flash("error_msg", `Database error: ${dbError.message || "Unknown"}`); - return req.session.save(() => res.redirect("/admin/booking")); - } - } catch (err) { - console.error("booking.update error:", err); - req.flash("error_msg", `Update error: ${err.message || "Unknown"}`); - return req.session.save(() => res.redirect("/admin/booking")); - } -}; - -// Booking selection mode: 'merge' (default) = static base, DB overrides; -// 'static' = use `data/booking.json` only; 'db' = use DB only. -const getFinalBooking = (staticBooking, dbBooking) => { - const mode = (process.env.BOOKING_MODE || 'merge').toLowerCase(); - if (mode === 'static') return staticBooking || dbBooking || null; - if (mode === 'db') return dbBooking || staticBooking || null; - // default: merge static (base) with DB overrides - // If both static and db present, attempt to map DB primitive lists (e.g. ["915"]) to - // full objects from the static file (e.g. {id:"915", name:..., type:...}). - const mapDbPrimitivesToObjects = (db, stat) => { - if (!db || !stat) return db; - const dbCfg = db.configuration || {}; - const statCfg = stat.configuration || {}; - - console.log('DB discounts/vouchers:', db.discounts, db.vouchers); - console.log('DB config:', dbCfg.discounts, dbCfg.vouchers); - console.log('Static config:', statCfg.discounts, statCfg.vouchers); - - // Handle legacy: if top-level discounts/vouchers exist as strings, migrate to configuration - if (Array.isArray(db.discounts) && db.discounts.length > 0 && (!dbCfg.discounts || dbCfg.discounts.length === 0)) { - const statDiscountById = {}; - if (statCfg.discounts) { - statCfg.discounts.forEach(d => { if (d && d.id) statDiscountById[String(d.id)] = d; }); - } - if (typeof db.discounts[0] === 'string') { - dbCfg.discounts = db.discounts.map(s => statDiscountById[String(s)] || { id: s, name: '', type: 'percentage', value: 0 }); - } else { - dbCfg.discounts = db.discounts; - } - } - - if (Array.isArray(db.vouchers) && db.vouchers.length > 0 && (!dbCfg.vouchers || dbCfg.vouchers.length === 0)) { - const statVouchByCode = {}; - if (statCfg.vouchers) { - statCfg.vouchers.forEach(v => { if (v && v.validCodes) statVouchByCode[String(v.validCodes)] = v; }); - } - if (typeof db.vouchers[0] === 'string') { - dbCfg.vouchers = db.vouchers.map(s => statVouchByCode[String(s)] || { validCodes: s, type: 'percentage', value: 0 }); - } else { - dbCfg.vouchers = db.vouchers; - } - } - - // If DB configuration still empty, use static data - if (Array.isArray(dbCfg.discounts) && dbCfg.discounts.length === 0 && statCfg.discounts && statCfg.discounts.length > 0) { - dbCfg.discounts = statCfg.discounts; - } else if (Array.isArray(dbCfg.discounts) && dbCfg.discounts.length > 0 && typeof dbCfg.discounts[0] === 'string') { - // Map string IDs to full objects from static - const statDiscountById = {}; - if (statCfg.discounts) { - statCfg.discounts.forEach(d => { if (d && d.id) statDiscountById[String(d.id)] = d; }); - } - dbCfg.discounts = dbCfg.discounts.map(s => statDiscountById[String(s)] || { id: s, name: '', type: 'percentage', value: 0 }); - } - - if (Array.isArray(dbCfg.vouchers) && dbCfg.vouchers.length === 0 && statCfg.vouchers && statCfg.vouchers.length > 0) { - dbCfg.vouchers = statCfg.vouchers; - } else if (Array.isArray(dbCfg.vouchers) && dbCfg.vouchers.length > 0 && typeof dbCfg.vouchers[0] === 'string') { - // Map string codes to full objects from static - const statVouchByCode = {}; - if (statCfg.vouchers) { - statCfg.vouchers.forEach(v => { if (v && v.validCodes) statVouchByCode[String(v.validCodes)] = v; }); - } - dbCfg.vouchers = dbCfg.vouchers.map(s => statVouchByCode[String(s)] || { validCodes: s, type: 'percentage', value: 0 }); - } - - return Object.assign({}, db, { configuration: dbCfg }); - }; - - const mappedDb = mapDbPrimitivesToObjects(dbBooking, staticBooking); - const merged = staticBooking ? deepMerge(staticBooking, mappedDb || {}) : (mappedDb || null); - - // Clean up: remove top-level discounts/vouchers after migrating to configuration - if (merged) { - delete merged.discounts; - delete merged.vouchers; - } - - return merged; -}; \ No newline at end of file diff --git a/controllers/bookingSubmissionController.js b/controllers/bookingSubmissionController.js deleted file mode 100644 index d488174..0000000 --- a/controllers/bookingSubmissionController.js +++ /dev/null @@ -1,558 +0,0 @@ -const BookingSubmission = require('../models/bookingSubmission'); -const Activity = require('../models/activity'); - -// API endpoint để tạo booking submission mới -exports.submitBooking = async (req, res) => { - try { - const { - activityId, - sessionId, - parentFirstName, - parentLastName, - email, - phone, - address, - city, - country, - postalCode, - participantFirstName, - participantLastName, - participantBirthDate, - participantGender, - numberOfParticipants, - medicalConditions, - dietaryRestrictions, - specialRequests, - emergencyContact, - emergencyPhone, - agreeTerms, - agreeNewsletter - } = req.body; - - // Validate required fields - if (!activityId || !sessionId || !parentFirstName || !parentLastName || - !email || !phone || !address || !city || !country || !postalCode || - !participantFirstName || !participantLastName || !participantBirthDate || - !participantGender || !emergencyContact || !emergencyPhone || !agreeTerms) { - return res.status(400).json({ - error: 'Missing required fields', - message: 'Please fill in all required fields' - }); - } - - // Verify activity exists - const activity = await Activity.findById(activityId); - if (!activity) { - return res.status(404).json({ - error: 'Activity not found', - message: 'The selected activity does not exist' - }); - } - - // Verify session exists and is active - const session = activity.bookingSessions?.find(s => s.sessionId === sessionId); - if (!session) { - return res.status(404).json({ - error: 'Session not found', - message: 'The selected session does not exist' - }); - } - - if (!session.isActive) { - return res.status(400).json({ - error: 'Session not available', - message: 'The selected session is no longer available for booking' - }); - } - - // Check availability based on participant gender - const currentBookings = await BookingSubmission.countDocuments({ - activityId, - sessionId, - participantGender, - status: { $in: ['pending', 'confirmed'] } - }); - - const availableSpots = participantGender === 'male' - ? session.totalMaleSpots - session.bookedMaleSpots - : session.totalFemaleSpots - session.bookedFemaleSpots; - - if (currentBookings >= availableSpots) { - return res.status(400).json({ - error: 'Session full', - message: `No more spots available for ${participantGender} participants in this session` - }); - } - - // Calculate total amount based on activity price and number of participants - const totalAmount = (activity.price || 0) * (parseInt(numberOfParticipants) || 1); - - // Create booking submission - const bookingSubmission = new BookingSubmission({ - activityId, - sessionId, - parentFirstName: parentFirstName.trim(), - parentLastName: parentLastName.trim(), - email: email.toLowerCase().trim(), - phone: phone.trim(), - address: address.trim(), - city: city.trim(), - country: country.trim(), - postalCode: postalCode.trim(), - participantFirstName: participantFirstName.trim(), - participantLastName: participantLastName.trim(), - participantBirthDate: new Date(participantBirthDate), - participantGender, - numberOfParticipants: parseInt(numberOfParticipants) || 1, - medicalConditions: (medicalConditions || '').trim(), - dietaryRestrictions: dietaryRestrictions || 'none', - specialRequests: (specialRequests || '').trim(), - emergencyContact: emergencyContact.trim(), - emergencyPhone: emergencyPhone.trim(), - agreeTerms: Boolean(agreeTerms), - agreeNewsletter: Boolean(agreeNewsletter), - totalAmount, - status: 'pending', - paymentStatus: 'pending' - }); - - await bookingSubmission.save(); - - // Update session booked spots - const updateField = participantGender === 'male' ? 'bookingSessions.$.bookedMaleSpots' : 'bookingSessions.$.bookedFemaleSpots'; - await Activity.updateOne( - { _id: activityId, 'bookingSessions.sessionId': sessionId }, - { $inc: { [updateField]: 1 } } - ); - - // Populate activity info for response - await bookingSubmission.populate('activityId', 'name price'); - - return res.status(201).json({ - success: true, - message: 'Booking submitted successfully', - booking: { - id: bookingSubmission._id, - activityName: bookingSubmission.activityId.name, - sessionId: bookingSubmission.sessionId, - participantName: `${bookingSubmission.participantFirstName} ${bookingSubmission.participantLastName}`, - totalAmount: bookingSubmission.totalAmount, - status: bookingSubmission.status - } - }); - - } catch (error) { - console.error('submitBooking error:', error); - - // Handle validation errors - if (error.name === 'ValidationError') { - const validationErrors = Object.values(error.errors).map(err => err.message); - return res.status(400).json({ - error: 'Validation failed', - message: validationErrors.join(', ') - }); - } - - return res.status(500).json({ - error: 'Server error', - message: 'An error occurred while processing your booking. Please try again.' - }); - } -}; - -// API endpoint để lấy thông tin session availability -exports.getSessionAvailability = async (req, res) => { - try { - const { activityId, sessionId } = req.params; - - const activity = await Activity.findById(activityId); - if (!activity) { - return res.status(404).json({ error: 'Activity not found' }); - } - - const session = activity.bookingSessions?.find(s => s.sessionId === sessionId); - if (!session) { - return res.status(404).json({ error: 'Session not found' }); - } - - // Get current booking counts - const maleBookings = await BookingSubmission.countDocuments({ - activityId, - sessionId, - participantGender: 'male', - status: { $in: ['pending', 'confirmed'] } - }); - - const femaleBookings = await BookingSubmission.countDocuments({ - activityId, - sessionId, - participantGender: 'female', - status: { $in: ['pending', 'confirmed'] } - }); - - return res.json({ - sessionId, - isActive: session.isActive, - startDate: session.startDate, - endDate: session.endDate, - overnightStays: session.overnightStays, - price: session.price || activity.price, - availability: { - male: { - total: session.totalMaleSpots, - booked: maleBookings, - available: Math.max(0, session.totalMaleSpots - maleBookings) - }, - female: { - total: session.totalFemaleSpots, - booked: femaleBookings, - available: Math.max(0, session.totalFemaleSpots - femaleBookings) - } - } - }); - - } catch (error) { - console.error('getSessionAvailability error:', error); - return res.status(500).json({ error: 'Error loading session availability' }); - } -}; - -// API endpoint để lấy tất cả sessions có sẵn cho một activity -exports.getAvailableSessions = async (req, res) => { - try { - const { activityId } = req.params; - - const activity = await Activity.findById(activityId); - if (!activity) { - return res.status(404).json({ error: 'Activity not found' }); - } - - const sessions = activity.bookingSessions || []; - const availableSessions = []; - - for (const session of sessions) { - if (!session.isActive) continue; - - // Get current booking counts - const maleBookings = await BookingSubmission.countDocuments({ - activityId, - sessionId: session.sessionId, - participantGender: 'male', - status: { $in: ['pending', 'confirmed'] } - }); - - const femaleBookings = await BookingSubmission.countDocuments({ - activityId, - sessionId: session.sessionId, - participantGender: 'female', - status: { $in: ['pending', 'confirmed'] } - }); - - const maleAvailable = Math.max(0, session.totalMaleSpots - maleBookings); - const femaleAvailable = Math.max(0, session.totalFemaleSpots - femaleBookings); - - // Only include sessions that have available spots - if (maleAvailable > 0 || femaleAvailable > 0) { - availableSessions.push({ - sessionId: session.sessionId, - startDate: session.startDate, - endDate: session.endDate, - overnightStays: session.overnightStays, - price: session.price || activity.price, - availability: { - male: { - total: session.totalMaleSpots, - booked: maleBookings, - available: maleAvailable - }, - female: { - total: session.totalFemaleSpots, - booked: femaleBookings, - available: femaleAvailable - } - } - }); - } - } - - return res.json({ - activityId, - activityName: activity.name, - sessions: availableSessions - }); - - } catch (error) { - console.error('getAvailableSessions error:', error); - return res.status(500).json({ error: 'Error loading available sessions' }); - } -}; - -// API endpoint để cập nhật booking submission -exports.updateBookingSubmission = async (req, res) => { - try { - const { bookingId } = req.params; - const updateData = req.body; - - // Find the booking - let booking = await BookingSubmission.findById(bookingId); - - // If not found as a separate document, try to find it as an embedded booking in Activity.bookingSessions - let activityContaining = null; - let sessionIndex = -1; - let bookingIndex = -1; - if (!booking) { - activityContaining = await Activity.findOne({ 'bookingSessions.bookingList._id': bookingId }); - if (!activityContaining) { - return res.status(404).json({ - error: 'Booking not found', - message: 'The booking submission does not exist' - }); - } - - // locate the exact session and booking positions - for (let si = 0; si < activityContaining.bookingSessions.length; si++) { - const bl = activityContaining.bookingSessions[si].bookingList || []; - const bi = bl.findIndex(b => b._id && b._id.toString() === bookingId.toString()); - if (bi !== -1) { - sessionIndex = si; - bookingIndex = bi; - break; - } - } - - if (sessionIndex === -1 || bookingIndex === -1) { - return res.status(404).json({ error: 'Booking not found', message: 'The booking submission does not exist' }); - } - - booking = activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex]; - } - - // Define allowed fields to update - const allowedUpdates = [ - 'status', - 'paymentStatus', - 'paidAmount', - 'totalAmount', - 'adminNotes', - 'emergencyContact', - 'emergencyPhone', - 'medicalConditions', - 'dietaryRestrictions', - 'specialRequests' - ]; - - // Build update object with only allowed fields - const updateFields = {}; - for (const field of allowedUpdates) { - if (updateData[field] !== undefined) { - updateFields[field] = updateData[field]; - } - } - - if (Object.keys(updateFields).length === 0) { - return res.status(400).json({ - error: 'No valid fields to update', - message: 'Please provide at least one valid field to update' - }); - } - - // If booking is a separate document, update the BookingSubmission collection - if (activityContaining === null) { - const updatedBooking = await BookingSubmission.findByIdAndUpdate( - bookingId, - updateFields, - { new: true, runValidators: true } - ).populate('activityId', 'name price'); - - return res.json({ - success: true, - message: 'Booking updated successfully', - booking: updatedBooking - }); - } - - // Otherwise update the embedded booking in the Activity document - const currentBooking = activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex]; - - // Handle status updates and spot adjustments - const newStatus = updateData.status || updateData.bookingStatus; - const currentStatus = currentBooking.status || currentBooking.bookingStatus; - - // Apply allowed updates to the embedded booking - const allowedEmbeddedUpdates = [ - 'status', 'bookingStatus', 'paymentStatus', 'paidAmount', 'totalAmount', 'adminNotes', - 'emergencyContact', 'emergencyPhone', 'medicalConditions', 'dietaryRestrictions', 'specialRequests' - ]; - - for (const field of allowedEmbeddedUpdates) { - if (updateData[field] !== undefined) { - if (field === 'status') { - activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex].status = updateData.status; - activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex].bookingStatus = updateData.status; - } else { - activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex][field] = updateData[field]; - } - } - } - - // If status change affects spots, adjust counts - if (newStatus && newStatus !== currentStatus) { - const numberOfParticipants = currentBooking.numberOfParticipants || 1; - const participantGender = currentBooking.participantGender; - - // If booking is being cancelled, free up spots - if (newStatus === 'cancelled' && currentStatus !== 'cancelled') { - if (participantGender === 'male') { - activityContaining.bookingSessions[sessionIndex].bookedMaleSpots = Math.max(0, activityContaining.bookingSessions[sessionIndex].bookedMaleSpots - numberOfParticipants); - } else if (participantGender === 'female') { - activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots = Math.max(0, activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots - numberOfParticipants); - } - } - - // If restoring from cancelled, ensure capacity then book spots - if (currentStatus === 'cancelled' && newStatus !== 'cancelled') { - if (participantGender === 'male') { - const totalMale = activityContaining.bookingSessions[sessionIndex].totalMaleSpots; - const currentMale = activityContaining.bookingSessions[sessionIndex].bookedMaleSpots; - if (currentMale + numberOfParticipants > totalMale) { - return res.status(400).json({ error: "Not enough male spots available to restore this booking" }); - } - activityContaining.bookingSessions[sessionIndex].bookedMaleSpots += numberOfParticipants; - } else if (participantGender === 'female') { - const totalFemale = activityContaining.bookingSessions[sessionIndex].totalFemaleSpots; - const currentFemale = activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots; - if (currentFemale + numberOfParticipants > totalFemale) { - return res.status(400).json({ error: "Not enough female spots available to restore this booking" }); - } - activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots += numberOfParticipants; - } - } - } - - await activityContaining.save(); - - return res.json({ - success: true, - message: 'Embedded booking updated successfully', - booking: activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex] - }); - - } catch (error) { - console.error('updateBookingSubmission error:', error); - - // Handle validation errors - if (error.name === 'ValidationError') { - const validationErrors = Object.values(error.errors).map(err => err.message); - return res.status(400).json({ - error: 'Validation failed', - message: validationErrors.join(', ') - }); - } - - return res.status(500).json({ - error: 'Server error', - message: 'An error occurred while updating the booking' - }); - } -}; - -// API endpoint để xóa booking submission -exports.deleteBookingSubmission = async (req, res) => { - try { - const { bookingId } = req.params; - - // Find and delete the booking - let booking = await BookingSubmission.findById(bookingId); - - // If not found in separate collection, try to delete embedded booking in Activity - if (!booking) { - const activityContaining = await Activity.findOne({ 'bookingSessions.bookingList._id': bookingId }); - if (!activityContaining) { - return res.status(404).json({ - error: 'Booking not found', - message: 'The booking submission does not exist' - }); - } - - // locate session and booking - let sessionIndex = -1; - let bookingIndex = -1; - for (let si = 0; si < activityContaining.bookingSessions.length; si++) { - const bl = activityContaining.bookingSessions[si].bookingList || []; - const bi = bl.findIndex(b => b._id && b._id.toString() === bookingId.toString()); - if (bi !== -1) { - sessionIndex = si; - bookingIndex = bi; - break; - } - } - - if (sessionIndex === -1 || bookingIndex === -1) { - return res.status(404).json({ error: 'Booking not found', message: 'The booking submission does not exist' }); - } - - const bookingToDelete = activityContaining.bookingSessions[sessionIndex].bookingList[bookingIndex]; - - // Free up spots if booking is not cancelled - if ((bookingToDelete.bookingStatus || bookingToDelete.status) !== 'cancelled') { - const numberOfParticipants = bookingToDelete.numberOfParticipants || 1; - const participantGender = bookingToDelete.participantGender; - - if (participantGender === 'male') { - activityContaining.bookingSessions[sessionIndex].bookedMaleSpots = Math.max(0, activityContaining.bookingSessions[sessionIndex].bookedMaleSpots - numberOfParticipants); - } else if (participantGender === 'female') { - activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots = Math.max(0, activityContaining.bookingSessions[sessionIndex].bookedFemaleSpots - numberOfParticipants); - } - } - - // Remove booking and save - activityContaining.bookingSessions[sessionIndex].bookingList.splice(bookingIndex, 1); - await activityContaining.save(); - - return res.json({ - success: true, - message: 'Embedded booking deleted successfully', - booking: { - id: bookingId, - participantName: `${bookingToDelete.participantFirstName} ${bookingToDelete.participantLastName}`, - email: bookingToDelete.email - } - }); - } - - // Store info for session spot adjustment - const { activityId, sessionId, participantGender, numberOfParticipants } = booking; - - // Delete the booking - await BookingSubmission.findByIdAndDelete(bookingId); - - // Update session booked spots (decrease the count) - if (booking.status !== 'cancelled') { - const updateField = participantGender === 'male' - ? 'bookingSessions.$.bookedMaleSpots' - : 'bookingSessions.$.bookedFemaleSpots'; - - await Activity.updateOne( - { _id: activityId, 'bookingSessions.sessionId': sessionId }, - { $inc: { [updateField]: -numberOfParticipants } } - ); - } - - return res.json({ - success: true, - message: 'Booking deleted successfully', - booking: { - id: bookingId, - participantName: `${booking.participantFirstName} ${booking.participantLastName}`, - email: booking.email - } - }); - - } catch (error) { - console.error('deleteBookingSubmission error:', error); - return res.status(500).json({ - error: 'Server error', - message: 'An error occurred while deleting the booking' - }); - } -}; \ No newline at end of file diff --git a/controllers/footerController.js b/controllers/footerController.js index 3f0212e..f69542a 100644 --- a/controllers/footerController.js +++ b/controllers/footerController.js @@ -1,169 +1,125 @@ -const { addBaseUrlToImages } = require("../utils/imageHelper"); const Footer = require("../models/footer"); -const writeAuditLog = require("../audit/writeAuditLog"); -const diffObject = require("../audit/diffObject"); -const AUDIT_ACTIONS = require("../constants/auditAction"); +const { addBaseUrlToImages } = require("../utils/imageHelper"); -// GET /api/footer - Public API cho website và CMS load dữ liệu -exports.getFooter = async (req, res) => { - try { - const footer = await Footer.getSingle(); - const processedData = addBaseUrlToImages(footer.toObject()); +// Helpers +const getFooterDoc = async () => Footer.findOne().sort({ updatedAt: -1 }); +const getFooterData = async () => (await getFooterDoc())?.toObject() || {}; - res.json(processedData); - } catch (error) { - console.error("Error getting footer:", error); - res.status(500).json({ - error: "Failed to get footer data", - }); - } -}; +const getDefaultFooterData = () => ({ + brand: { + logo: { + image: "", + href: "/", + }, + description: "", + social: [], + }, + explore: { + heading: "", + links: [], + }, + contact: { + heading: "", + address: "", + phone: "", + email: "", + }, + newsletter: { + heading: "", + description: "", + placeholder: "", + buttonText: "", + }, + bottom: { + copyright: "", + links: [], + }, +}); -// PUT /api/admin/footer - Update toàn bộ footer cho CMS -exports.updateFooter = async (req, res) => { - try { - let updateData = req.body; - - console.log("=== FOOTER UPDATE REQUEST RECEIVED ==="); - console.log("Raw body:", JSON.stringify(req.body, null, 2)); - - // Nếu có footerJson, parse nó (tương tự Header logic) - if (updateData.footerJson && typeof updateData.footerJson === "string") { - try { - const parsedData = JSON.parse(updateData.footerJson); - console.log("✓ Parsed footerJson successfully:", parsedData); - updateData = parsedData; - } catch (e) { - console.error("✗ Error parsing footerJson:", e.message); - return res.status(400).json({ - success: false, - message: "Invalid JSON in footerJson: " + e.message, - }); - } - } - - // Lấy footer hiện tại hoặc tạo mới (giống Header logic) - let footer = await Footer.findOne(); - - if (!footer) { - console.log("No existing footer found, creating new one"); - footer = new Footer(updateData); - await footer.save(); - console.log("✓ Footer created:", footer._id); - } else { - console.log("✓ Found existing footer:", footer._id); - // Merge với dữ liệu cũ thay vì overwrite (giống Header) - Object.assign(footer, updateData); - await footer.save(); - console.log("✓ Footer updated successfully"); - } - - const processedData = addBaseUrlToImages(footer.toObject()); - - console.log("Updated footer data:", JSON.stringify(processedData, null, 2)); - - res.json({ - success: true, - message: "Footer updated successfully", - data: processedData, - }); - } catch (error) { - console.error("✗ Error updating footer:", error); - res.status(500).json({ - success: false, - error: "Failed to update footer: " + error.message, - }); - } -}; - -// Render admin view (giữ lại cho UI hiện tại) +// Admin: render management view with data from MongoDB exports.index = async (req, res) => { try { - const data = await Footer.getSingle(); - const processedData = addBaseUrlToImages(data.toObject()); + let data = await getFooterData(); + const defaults = getDefaultFooterData(); - res.render("admin/footer/index", { - title: "Footer Management", - data: processedData, + // Merge defaults for any missing sections + const sections = Object.keys(defaults); + sections.forEach((s) => { + data[s] = data[s] || defaults[s]; }); - } catch (error) { - console.error("Error in footer index:", error); - req.flash("error_msg", "An error occurred while loading the page"); - res.redirect("/admin/dashboard"); + + return res.render("admin/footer/index", { + layout: "layouts/main", + title: "Footer Management", + data, + currentPath: req.path, + user: req.session.user, + }); + } catch (err) { + console.error("Footer index error:", err); + req.flash("error_msg", "Error loading footer data"); + return req.session.save(() => res.redirect("/admin/dashboard")); } }; -// Update method cho form hiện tại (giống Header pattern) +// Admin: parse req.body sections and save to MongoDB exports.update = async (req, res) => { try { - let updateData = req.body; + const sections = ["brand", "explore", "contact", "newsletter", "bottom"]; - console.log("=== FOOTER FORM UPDATE REQUEST RECEIVED ==="); - console.log("Raw body:", JSON.stringify(req.body, null, 2)); + let doc = await getFooterDoc(); - // Nếu có footerJson, parse nó (giống Header logic) - if (updateData.footerJson && typeof updateData.footerJson === "string") { - try { - const parsedData = JSON.parse(updateData.footerJson); - console.log("✓ Parsed footerJson successfully:", parsedData); - updateData = parsedData; - } catch (e) { - console.error("✗ Error parsing footerJson:", e.message); - req.flash("error_msg", "Invalid JSON in footerJson: " + e.message); - return res.redirect("/admin/footer"); + if (!doc) { + doc = new Footer({}); + } + + let hasChanges = false; + + for (const section of sections) { + if (req.body[section]) { + try { + const payload = JSON.parse(req.body[section]); + doc[section] = payload; + doc.markModified(section); + hasChanges = true; + } catch (e) { + console.error(`Invalid JSON for ${section}:`, e.message); + } } } - // Lấy footer hiện tại hoặc tạo mới (giống Header) - let footer = await Footer.findOne(); - - // ✅ Capture BEFORE state - const beforeData = footer - ? JSON.parse(JSON.stringify(footer.toObject())) - : {}; - - if (!footer) { - console.log("No existing footer found, creating new one"); - footer = new Footer(updateData); - await footer.save(); - console.log("✓ Footer created:", footer._id); - req.flash("success_msg", "Footer created successfully"); - } else { - console.log("✓ Found existing footer:", footer._id); - // Merge với dữ liệu cũ (giống Header) - Object.assign(footer, updateData); - await footer.save(); - - // ✅ Capture AFTER state - const afterData = JSON.parse(JSON.stringify(footer.toObject())); - - // ✅ AUDIT LOGGING - Footer Updated - const changes = diffObject(beforeData, afterData); - if (changes.length > 0) { - await writeAuditLog({ - model: "Footer", - documentId: footer._id, - action: AUDIT_ACTIONS.UPDATE_FOOTER, - before: beforeData, - after: afterData, - changes, - req, - }); - } - - console.log("✓ Footer updated successfully"); - req.flash("success_msg", "Footer updated successfully"); + if (!hasChanges) { + req.flash("info_msg", "No changes were made"); + return req.session.save(() => res.redirect("/admin/footer")); } - const activeTab = req.body.activeTab || "about"; - res.redirect(`/admin/footer?activeTab=${activeTab}`); + await doc.save(); + + req.flash("success_msg", "Footer configuration has been updated!"); + return req.session.save(() => res.redirect("/admin/footer")); } catch (err) { - console.error("✗ Error updating footer:", err); - req.flash("error_msg", err.message || "Error updating footer"); - res.redirect("/admin/footer"); + console.error("Footer update error:", err); + req.flash("error_msg", `Update error: ${err.message}`); + return req.session.save(() => res.redirect("/admin/footer")); } }; -// Legacy API endpoints (giữ lại cho tương thích) -exports.api = exports.getFooter; -exports.getFooterData = exports.getFooter; +// Public API: return JSON data for frontend +exports.api = async (req, res) => { + try { + let data = await getFooterData(); + if (!data || Object.keys(data).length === 0) { + data = getDefaultFooterData(); + } + const baseUrl = + process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`; + return res.json(addBaseUrlToImages(data, baseUrl)); + } catch (err) { + console.error("Footer API error:", err); + return res.status(500).json({ error: "Error loading footer data" }); + } +}; + +// Aliases for routes/index.js compatibility +exports.getFooter = exports.api; +exports.updateFooter = exports.update; diff --git a/controllers/homeController.js b/controllers/homeController.js index f1ca27c..dc7534b 100644 --- a/controllers/homeController.js +++ b/controllers/homeController.js @@ -1,99 +1,63 @@ -const { addBaseUrlToImages, getFullImageUrl } = require("../utils/imageHelper"); const Home = require("../models/home"); -const Blog = require("../models/blog"); -const writeAuditLog = require("../audit/writeAuditLog"); -const diffObject = require("../audit/diffObject"); -const AUDIT_ACTIONS = require("../constants/auditAction"); +const { addBaseUrlToImages } = require("../utils/imageHelper"); -// Các hàm hỗ trợ +// Helpers const getHomeDoc = async () => Home.findOne().sort({ updatedAt: -1 }); const getHomeData = async () => (await getHomeDoc())?.toObject() || {}; const getDefaultHomeData = () => ({ hero: { - backgroundImage: "", - slides: [], + badge: "", title: "", - subtitle: "", description: "", - heroImage: "", - videoUrl: "", - primaryButton: {}, - secondaryButton: {}, + searchPlaceholder: "", + buttonLabel: "", + image: "", + imageAlt: "", + floatingBadge: { + icon: "", + value: "", + label: "", + }, }, - whyChooseUs: { - heading: "", - subheading: "", + quickLinks: [], + valueProp: { + badge: "", + title: "", description: "", - highlightWord: "", - mainImage: "", - secondaryImage: "", - items: [], features: [], - ctaButton: {}, + stats: [], }, - visaSolutions: { heading: "", subheading: "", items: [] }, - visaCountries: { + programs: { heading: "", - subheading: "", description: "", - countries: [], - ctaButton: {}, - }, - testimonials: { - heading: "", - subheading: "", - videoUrl: "", - videoThumbnail: "", items: [], }, - videoGallery: { heading: "", videoUrl: "", thumbnail: "" }, - faq: { + requestInfo: { heading: "", - subheading: "", description: "", - ctaButton: {}, - items: [], - }, - achievements: { heading: "", subheading: "", items: [] }, - partners: { visaConsultancy: { items: [] }, brands: { items: [] } }, - blogPreview: { - heading: "Latest Insights & Updates", - subheading: "Visa Tips & Guides", - ctaButton: { label: "View All Articles", href: "/blog" }, - items: [], - selectedBlogIds: [], // Array of manually selected blog IDs + phone: "", + email: "", + programs: [], }, }); -// Admin: Xem trang quản lý +// Admin: render management view with data from MongoDB exports.index = async (req, res) => { try { let data = await getHomeData(); const defaults = getDefaultHomeData(); - // Merge dữ liệu mặc định cho tất cả các phần + // Merge defaults for any missing sections const sections = Object.keys(defaults); sections.forEach((s) => { data[s] = data[s] || defaults[s]; }); - const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000"; - const backendUrl = process.env.BACKEND_URL || "http://localhost:3001"; - - // Lấy tất cả blog để chọn trong CMS - const allBlogs = await Blog.find({ status: "published" }) - .sort({ createdAt: -1 }) - .lean(); - return res.render("admin/home/index", { layout: "layouts/main", title: "Home Management", data, - allBlogs, - frontendUrl, - backendUrl, - getFullImageUrl, currentPath: req.path, user: req.session.user, }); @@ -104,43 +68,34 @@ exports.index = async (req, res) => { } }; -// Admin: Cập nhật dữ liệu (tập trung vào achievements, partners; các phần khác giữ nguyên nếu không có dữ liệu mới) +// Admin: parse req.body sections and save to MongoDB exports.update = async (req, res) => { try { const sections = [ "hero", - "whyChooseUs", - "visaSolutions", - "visaCountries", - "testimonials", - "videoGallery", - "faq", - "achievements", - "partners", - "blogPreview", + "quickLinks", + "valueProp", + "programs", + "requestInfo", ]; let doc = await getHomeDoc(); - const beforeData = doc ? JSON.parse(JSON.stringify(doc.toObject())) : {}; if (!doc) { doc = new Home({}); } let hasChanges = false; - const updatedSections = []; for (const section of sections) { if (req.body[section]) { try { const payload = JSON.parse(req.body[section]); - // Gán trực tiếp vào doc, Mongoose sẽ tự check schema doc[section] = payload; doc.markModified(section); hasChanges = true; - updatedSections.push(section); } catch (e) { - console.error(`Invalid JSON for ${section}:`, e); + console.error(`Invalid JSON for ${section}:`, e.message); } } } @@ -151,21 +106,6 @@ exports.update = async (req, res) => { } await doc.save(); - const afterData = JSON.parse(JSON.stringify(doc.toObject())); - - // ✅ AUDIT LOGGING - Home Update - const changes = diffObject(beforeData, afterData); - if (changes.length > 0) { - await writeAuditLog({ - model: "Home", - documentId: doc._id, - action: AUDIT_ACTIONS.UPDATE_HOME, - before: beforeData, - after: afterData, - changes, - req, - }); - } req.flash("success_msg", "Home page configuration has been updated!"); return req.session.save(() => res.redirect("/admin/home")); @@ -176,72 +116,15 @@ exports.update = async (req, res) => { } }; -// Public API// API lấy danh sách blog cho CMS -exports.apiGetBlogs = async (req, res) => { - try { - const blogs = await Blog.find({ status: "published" }) - .sort({ createdAt: -1 }) - .select("title slug featuredImage author publishedAt") - .lean(); - res.json(blogs); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}; +// Public API: return JSON data for frontend exports.api = async (req, res) => { try { let data = await getHomeData(); - const baseUrl = - process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`; - - // === Xử lý Blog Preview động === - const blogPreview = data.blogPreview || {}; - let blogs = []; - - // Nếu có chọn blog cụ thể - if (blogPreview.selectedBlogIds && blogPreview.selectedBlogIds.length > 0) { - blogs = await Blog.find({ - _id: { $in: blogPreview.selectedBlogIds }, - status: "published", - }).lean(); - - // Sắp xếp theo thứ tự đã chọn trong selectedBlogIds - blogs.sort((a, b) => { - return ( - blogPreview.selectedBlogIds.indexOf(a._id.toString()) - - blogPreview.selectedBlogIds.indexOf(b._id.toString()) - ); - }); + if (!data || Object.keys(data).length === 0) { + data = getDefaultHomeData(); } - - // Nếu không chọn hoặc chọn nhưng không đủ, lấy thêm 3 bài mới nhất (hoặc bù vào) - if (blogs.length === 0) { - blogs = await Blog.find({ status: "published" }) - .sort({ createdAt: -1 }) - .limit(3) - .lean(); - } - - // Map dữ liệu blog sang format mà frontend mong đợi - blogPreview.items = blogs.map((blog) => ({ - title: blog.title, - excerpt: blog.excerpt, - category: blog.category && blog.category[0] ? blog.category[0] : "Visa", - date: blog.publishedAt || blog.createdAt, - author: { - name: blog.author || "Admin", - avatar: "", // Frontend đang tự xử lý hoặc dùng logo hệ thống - }, - comments: blog.commentsCount || 0, - link: `/blog/${blog.slug}`, - thumbnail: blog.featuredImage, - })); - - data.blogPreview = blogPreview; - // =============================== - - const processed = addBaseUrlToImages(data, baseUrl); - return res.json(processed); + const baseUrl = process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`; + return res.json(addBaseUrlToImages(data, baseUrl)); } catch (err) { console.error("Home API error:", err); return res.status(500).json({ error: "Error loading home data" }); diff --git a/controllers/pricingController.js b/controllers/pricingController.js deleted file mode 100644 index d362c3c..0000000 --- a/controllers/pricingController.js +++ /dev/null @@ -1,229 +0,0 @@ -const Pricing = require("../models/pricing"); -const writeAuditLog = require("../audit/writeAuditLog"); -const diffObject = require("../audit/diffObject"); -const AUDIT_ACTIONS = require("../constants/auditAction"); - -// ==================== 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" }); - - // ✅ Capture BEFORE state - const beforeData = pricing - ? JSON.parse(JSON.stringify(pricing.toObject())) - : {}; - - 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, - }); - } - - // ✅ Capture AFTER state - const afterData = JSON.parse(JSON.stringify(pricing.toObject())); - - // ✅ AUDIT LOGGING - Pricing Updated - const changes = diffObject(beforeData, afterData); - if (changes.length > 0) { - await writeAuditLog({ - model: "Pricing", - documentId: pricing._id, - action: AUDIT_ACTIONS.UPDATE_PRICING, - before: beforeData, - after: afterData, - changes, - req, - }); - } - - 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", - }); - } -}; diff --git a/controllers/serviceController.js b/controllers/serviceController.js deleted file mode 100644 index 078240b..0000000 --- a/controllers/serviceController.js +++ /dev/null @@ -1,396 +0,0 @@ -const { getServiceData } = require("../services/service.service"); -const Service = require("../models/service"); -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, - }); - 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"); - } - 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, - }); - } -}; diff --git a/controllers/travelController.js b/controllers/travelController.js deleted file mode 100644 index f4e6592..0000000 --- a/controllers/travelController.js +++ /dev/null @@ -1,290 +0,0 @@ -const Travel = require("../models/travel"); -const { addBaseUrlToImages } = require("../utils/imageHelper"); -const fs = require("fs").promises; -const path = require("path"); -const writeAuditLog = require("../audit/writeAuditLog"); -const diffObject = require("../audit/diffObject"); -const AUDIT_ACTIONS = require("../constants/auditAction"); - -/** - * Hàm Helper: Trích xuất ID YouTube từ nhiều định dạng link khác nhau - */ -function extractYouTubeId(url) { - if (!url || typeof url !== "string") return null; - // Hỗ trợ: watch?v=, embed/, youtu.be/, shorts/ - const regex = - /(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:watch\?v=|embed\/|v\/|shorts\/))([A-Za-z0-9_-]{11})/; - const match = url.match(regex); - return match ? match[1] : null; -} - -/** - * Hàm Helper: Làm sạch danh sách blocks của Editor.js - * Loại bỏ duplicate video (vừa embed vừa text link) và paragraph rỗng - */ -function sanitizeContentBlocks(blocks) { - if (!blocks || !Array.isArray(blocks)) return []; - - const seenVideoIds = new Set(); - - // Bước 1: Duyệt qua để chuẩn hóa block embed và lấy danh sách ID video - const processedBlocks = blocks.map((block) => { - if (block.type === "embed") { - const url = block.data.source || block.data.embed || ""; - const videoId = extractYouTubeId(url); - if (videoId) { - seenVideoIds.add(videoId); - // Cập nhật lại data chuẩn cho Editor.js - block.data.embed = `https://www.youtube.com/embed/${videoId}`; - block.data.source = url; - block.data.videoId = videoId; - block.data.service = "youtube"; - } - } - return block; - }); - - // Bước 2: Lọc bỏ paragraph rác - return processedBlocks.filter((block) => { - if (block.type === "paragraph") { - const text = (block.data?.text || "").trim(); - - // Xóa paragraph rỗng - if (text === "" || text === "
" || text === " ") return false; - - // Xóa paragraph nếu nó chỉ chứa 1 link YouTube đã được embed ở trên - const videoIdInText = extractYouTubeId(text); - if (videoIdInText && seenVideoIds.has(videoIdInText)) { - console.log( - `[Sanitizer] Removed duplicate text link for video: ${videoIdInText}`, - ); - return false; - } - } - return true; - }); -} - -// GET: Show travel editor -exports.index = async (req, res) => { - try { - const travel = await Travel.findOne(); - - if (!travel) { - return res.render("admin/travel/index", { - title: "Travel Management", - data: { - page: { - title: "Travel Information", - description: "", - metadata: { title: "", description: "" }, - }, - hero: { title: "Travel Information", backgroundImage: "" }, - content: { blocks: [] }, - enableScrollspy: false, - }, - message: "No travel data found. Please run migration first.", - }); - } - - res.render("admin/travel/index", { - title: "Travel Management", - data: travel, - }); - } catch (error) { - console.error("Error loading travel page:", error); - res.status(500).send("Error loading travel page"); - } -}; - -// POST: Update travel information -exports.update = async (req, res) => { - try { - const { page, hero, content, enableScrollspy } = req.body; - - // Get current data for before state - const currentTravel = await Travel.findOne(); - - // ✅ Capture BEFORE state - const beforeData = currentTravel - ? JSON.parse( - JSON.stringify( - currentTravel.toObject ? currentTravel.toObject() : currentTravel, - ), - ) - : {}; - - const updateData = {}; - - if (page) updateData.page = JSON.parse(page); - if (hero) updateData.hero = JSON.parse(hero); - - if (content) { - let contentObj = JSON.parse(content); - // Áp dụng bộ lọc dọn dẹp nội dung - contentObj.blocks = sanitizeContentBlocks(contentObj.blocks); - updateData.content = contentObj; - } - - if (enableScrollspy !== undefined) { - updateData.enableScrollspy = - enableScrollspy === "true" || enableScrollspy === true; - } - - const updatedTravel = await Travel.findOneAndUpdate({}, updateData, { - upsert: true, - new: true, - }); - - // ✅ Capture AFTER state - const afterData = JSON.parse( - JSON.stringify( - updatedTravel.toObject ? updatedTravel.toObject() : updatedTravel, - ), - ); - - // ✅ AUDIT LOGGING - Travel Updated - const changes = diffObject(beforeData, afterData); - if (changes.length > 0) { - await writeAuditLog({ - model: "Travel", - documentId: updatedTravel._id, - action: AUDIT_ACTIONS.UPDATE_TRAVEL, - before: beforeData, - after: afterData, - changes, - req, - }); - } - - req.flash( - "success", - "Travel information updated and sanitized successfully", - ); - res.redirect("/admin/travel"); - } catch (error) { - console.error("Error updating travel:", error); - req.flash("error", "Error updating travel information"); - res.redirect("/admin/travel"); - } -}; - -// GET: Travel data API (Sử dụng cho Frontend/Public) -exports.api = exports.getTravelData = async (req, res) => { - try { - const travel = await Travel.findOne(); - if (!travel) { - return res.status(404).json({ error: "Travel data not found" }); - } - - const travelObj = travel.toObject(); - const baseUrl = - process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`; - const processed = addBaseUrlToImages(travelObj, baseUrl); - - return res.json({ - success: true, - data: { - hero: processed.hero, - page: processed.page, - content: processed.content, - enableScrollspy: processed.enableScrollspy, - }, - }); - } catch (error) { - console.error("Error fetching travel data:", error); - res.status(500).json({ error: "Internal server error" }); - } -}; - -// POST: Preview travel -exports.preview = async (req, res) => { - try { - const { content, pageTitle, heroTitle, heroBackgroundImage, pageYear } = - req.body; - - // Preview cũng cần được sanitize để hiển thị đúng thực tế khi lưu - let contentObj = JSON.parse(content); - contentObj.blocks = sanitizeContentBlocks(contentObj.blocks); - - const previewData = { - page: { - title: pageTitle || "Travel Information", - year: pageYear || "", - }, - hero: { - title: heroTitle || "Travel Information", - backgroundImage: heroBackgroundImage || "", - }, - content: contentObj, - enableScrollspy: false, - }; - - res.render("page/travel", { - title: "Travel Preview", - data: previewData, - }); - } catch (error) { - console.error("Error generating preview:", error); - res.status(500).send("Error generating preview"); - } -}; - -// GET: Seed/Import from JSON -exports.seed = async (req, res) => { - try { - const jsonPath = path.join(__dirname, "../data/travel.json"); - const jsonData = await fs.readFile(jsonPath, "utf-8"); - const jsonTravelData = JSON.parse(jsonData); - - let contentBlocks = []; - - // Trường hợp JSON đã có định dạng bài viết (blog format) - if ( - Array.isArray(jsonTravelData.posts) && - jsonTravelData.posts.length > 0 - ) { - const firstPost = jsonTravelData.posts[0]; - contentBlocks = - firstPost.content && firstPost.content.blocks - ? firstPost.content.blocks - : []; - } - // Trường hợp format cũ (legacy) - else { - // ... (Logic chuyển đổi legacy format giữ nguyên nhưng bọc qua sanitize) - // Ví dụ: push các header, paragraph từ locations vào contentBlocks - } - - // Luôn làm sạch dữ liệu trước khi seed vào DB - const cleanedBlocks = sanitizeContentBlocks(contentBlocks); - - const travelData = { - page: { - title: - jsonTravelData.page?.title || "Go and Grow Camp Travel Information", - year: jsonTravelData.page?.year || "", - metadata: { - title: "Travel Guide - Go and Grow Camp", - description: - "Everything you need to know about traveling to our camps", - }, - }, - hero: { - title: jsonTravelData.hero?.title || "Travel Information", - backgroundImage: jsonTravelData.hero?.backgroundImage || "", - }, - content: { blocks: cleanedBlocks }, - enableScrollspy: true, - }; - - await Travel.findOneAndUpdate({}, travelData, { upsert: true, new: true }); - - req.flash("success", "Travel data seeded and sanitized successfully"); - res.redirect("/admin/travel"); - } catch (error) { - console.error("Error seeding travel data:", error); - req.flash("error", "Failed to seed travel data"); - res.redirect("/admin/travel"); - } -}; diff --git a/controllers/visaController.js b/controllers/visaController.js deleted file mode 100644 index 61d1899..0000000 --- a/controllers/visaController.js +++ /dev/null @@ -1,695 +0,0 @@ -// controllers/visaController.js - -const addBaseUrlToImages = (data, baseUrl) => { - if (!data) return data; - - // Nếu là mảng, duyệt từng phần tử - if (Array.isArray(data)) { - return data.map((item) => addBaseUrlToImages(item, baseUrl)); - } - - // Nếu là object, duyệt từng key - if (typeof data === "object") { - const newObj = {}; - for (const [key, value] of Object.entries(data)) { - // Kiểm tra nếu key là các trường chứa ảnh và value là string - const imageKeys = ["icon", "mainImage", "bannerImage", "image"]; - - if ( - imageKeys.includes(key) && - typeof value === "string" && - !value.startsWith("http") - ) { - newObj[key] = `${baseUrl}/${value}` - .replace(/\/+/g, "/") - .replace(":/", "://"); - } - // Xử lý riêng cho mảng gallery (mảng các chuỗi) - else if (key === "gallery" && Array.isArray(value)) { - newObj[key] = value.map((img) => - img.startsWith("http") - ? img - : `${baseUrl}/${img}`.replace(/\/+/g, "/").replace(":/", "://"), - ); - } - // Nếu là object hoặc mảng con khác, đệ quy tiếp - else if (typeof value === "object" && value !== null) { - newObj[key] = addBaseUrlToImages(value, baseUrl); - } else { - newObj[key] = value; - } - } - return newObj; - } - return data; -}; -const Visa = require("../models/visa"); -const slugify = require("slugify"); -const writeAuditLog = require("../audit/writeAuditLog"); -const diffObject = require("../audit/diffObject"); -const AUDIT_ACTIONS = require("../constants/auditAction"); -const createSlug = (text) => { - return slugify(text, { - lower: true, - strict: true, - locale: "en", - trim: true, - }); -}; -// -------------------- Helper Functions -------------------- - -// Get visa data from MongoDB -const getVisaData = async () => { - const visa = await Visa.findOne().sort({ updatedAt: -1 }); - return visa || {}; -}; - -// Get default visa data structure (updated to match new JSON) -const getDefaultVisaData = () => ({ - hero: { - title: "Visa Service", - summaryList: [], - }, -}); - -// Helper function: Generate next country ID -const getNextCountryId = (countries) => { - if (!Array.isArray(countries) || countries.length === 0) return 1; - return Math.max(...countries.map((c) => c.id || 0)) + 1; -}; - -// -------------------- Admin Exports -------------------- - -// Display visa management page -exports.index = async (req, res) => { - try { - // Fetch Visa data - let data = await getVisaData(); - - // If no data exists, use default - if (!data || Object.keys(data).length === 0) { - data = getDefaultVisaData(); - } else { - // Merge with defaults to ensure all fields exist - const defaultData = getDefaultVisaData(); - - // Ensure hero section exists with defaults - data.hero = data.hero || defaultData.hero; - data.hero.title = data.hero.title || "Visa Service"; - data.hero.summaryList = data.hero.summaryList || []; - } - - const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000"; - - res.render("admin/visa/index", { - layout: "layouts/main", - title: "Visa Management", - data, - frontendUrl, - currentPath: req.path, - user: req.session.user, - }); - // return res.json(data); - } catch (err) { - console.error("Visa index error:", err); - req.flash("error_msg", "Error loading visa data"); - res.redirect("/admin/dashboard"); - } -}; - -// Get single country for edit -exports.getCountry = async (req, res) => { - console.log("--------------------------------------------------"); - console.log("🚀 [GET] Request nhận được tại /visa/edit/:id"); - - try { - const { id } = req.params; - console.log("📍 ID từ Params (URL):", id, "| Kiểu dữ liệu:", typeof id); - - const visaData = await getVisaData(); - - // Kiểm tra cấu trúc dữ liệu tổng - if (!visaData) { - console.error("❌ Lỗi: Hàm getVisaData() trả về null/undefined"); - return res.status(404).json({ error: "Dữ liệu gốc không tồn tại" }); - } - - if (!visaData.hero || !visaData.hero.summaryList) { - console.error("❌ Lỗi: Cấu trúc visaData.hero.summaryList không hợp lệ"); - return res - .status(404) - .json({ error: "Không tìm thấy danh sách quốc gia" }); - } - - console.log( - "📊 Tổng số quốc gia hiện có trong mảng:", - visaData.hero.summaryList.length, - ); - - // 2. Tìm quốc gia theo ID - const targetId = parseInt(id); - console.log("🔍 Đang tìm kiếm Quốc gia có ID (sau khi parse):", targetId); - - const country = visaData.hero.summaryList.find((c) => { - // Log từng phần tử để kiểm tra kiểu dữ liệu của c.id trong DB - // console.log(`Checking country: ${c.name} | ID in DB: ${c.id} (${typeof c.id})`); - return c.id === targetId; - }); - - if (!country) { - console.warn(`⚠️ Không tìm thấy quốc gia nào khớp với ID: ${targetId}`); - // In ra danh sách ID hiện có để so sánh - const existingIds = visaData.hero.summaryList.map((c) => c.id); - console.log("🆔 Các ID hiện có trong Database:", existingIds); - - return res.status(404).json({ - success: false, - error: `Không tìm thấy quốc gia có ID: ${id}`, - }); - } - - console.log("✅ Tìm thấy dữ liệu quốc gia:", country.name); - - // 3. Trả về dữ liệu - res.json({ - success: true, - country: country, - }); - } catch (err) { - console.error("🔴 Lỗi nghiêm trọng tại getCountry Controller:", err); - res.status(500).json({ error: "Lỗi hệ thống khi tải thông tin quốc gia" }); - } -}; - -// Update visa data (hero title only) -exports.update = async (req, res) => { - try { - // Get current data - const currentData = await getVisaData(); - - // ✅ Capture BEFORE state - const beforeData = currentData - ? JSON.parse( - JSON.stringify( - currentData.toObject ? currentData.toObject() : currentData, - ), - ) - : {}; - - // Create updated data object - const updatedData = { - ...(currentData.toObject ? currentData.toObject() : currentData), - }; - - // Ensure hero structure exists - updatedData.hero = updatedData.hero || { - title: "Visa Service", - summaryList: [], - }; - - // Update hero title - if (req.body.heroTitle) { - updatedData.hero.title = req.body.heroTitle; - } - - // Update or create document - try { - let savedData; - if (currentData._id) { - savedData = await Visa.findByIdAndUpdate(currentData._id, updatedData, { - new: true, - }); - } else { - savedData = await Visa.create(updatedData); - } - - // ✅ Capture AFTER state - const afterData = JSON.parse(JSON.stringify(savedData.toObject())); - - // ✅ AUDIT LOGGING - Visa Updated - const changes = diffObject(beforeData, afterData); - if (changes.length > 0) { - await writeAuditLog({ - model: "Visa", - documentId: savedData._id, - action: AUDIT_ACTIONS.UPDATE_VISA, - before: beforeData, - after: afterData, - changes, - req, - }); - console.log( - `✅ Audit log created for Visa update: ${changes.length} changes`, - ); - } else { - console.log("ℹ️ No changes detected for Visa update"); - } - - req.flash("success_msg", "Visa data updated successfully"); - return req.session.save(() => res.redirect("/admin/visa")); - } catch (dbError) { - console.error("Database error:", dbError); - req.flash("error_msg", `Database error: ${dbError.message || "Unknown"}`); - return req.session.save(() => res.redirect("/admin/visa")); - } - } catch (err) { - console.error("Update error:", err); - req.flash("error_msg", `Update error: ${err.message || "Unknown"}`); - return req.session.save(() => res.redirect("/admin/visa")); - } -}; - -// Add new country -exports.addCountry = async (req, res) => { - try { - let visaData = await getVisaData(); - - // Initialize hero structure if not exist - if (!visaData.hero || !visaData.hero.summaryList) { - visaData = getDefaultVisaData(); - } - - // ✅ Capture BEFORE state - const beforeData = JSON.parse( - JSON.stringify(visaData.toObject ? visaData.toObject() : visaData), - ); - - // Validate required fields - if (!req.body.name) { - return res.status(400).json({ error: "Name is required" }); - } - const finalSlug = req.body.slug - ? createSlug(req.body.slug) - : createSlug(req.body.name); - - // Parse services array - let services = []; - if (req.body.services) { - if (typeof req.body.services === "string") { - try { - services = JSON.parse(req.body.services); - } catch (e) { - services = [req.body.services]; - } - } else if (Array.isArray(req.body.services)) { - services = req.body.services; - } - } - - // Parse detailedView if provided (optional) - let detailedView = null; - if (req.body.detailedView) { - try { - detailedView = - typeof req.body.detailedView === "string" - ? JSON.parse(req.body.detailedView) - : req.body.detailedView; - } catch (e) { - console.warn("Could not parse detailedView, creating without it"); - } - } - - // Create new country object - const newCountry = { - id: req.body.id || getNextCountryId(visaData.hero.summaryList), - name: req.body.name, - slug: finalSlug, - icon: req.body.icon || "", - services: services, - ...(detailedView && { detailedView }), - }; - - // Add new country to summaryList - visaData.hero.summaryList.push(newCountry); - - // Update database - const updatedData = { - ...(visaData.toObject ? visaData.toObject() : visaData), - }; - - let savedData; - if (visaData._id) { - savedData = await Visa.findByIdAndUpdate(visaData._id, updatedData, { - new: true, - }); - } else { - savedData = await Visa.create(updatedData); - } - - // ✅ Capture AFTER state - const afterData = JSON.parse( - JSON.stringify(savedData.toObject ? savedData.toObject() : savedData), - ); - - // ✅ AUDIT LOGGING - Visa Country Added - const changes = diffObject(beforeData, afterData); - if (changes.length > 0) { - await writeAuditLog({ - model: "Visa", - documentId: savedData._id, - action: AUDIT_ACTIONS.UPDATE_VISA, - before: beforeData, - after: afterData, - changes, - req, - }); - console.log( - `✅ Audit log created for Visa country addition: ${changes.length} changes`, - ); - } - - console.log(`✅ Country "${newCountry.name}" added successfully`); - res.json({ - success: true, - message: `Country "${newCountry.name}" added successfully`, - country: newCountry, - }); - } catch (err) { - console.error("Add country error:", err); - res.status(500).json({ error: err.message }); - } -}; - -// Update single country -exports.updateCountry = async (req, res) => { - try { - // 1. Lấy ID từ params (URL) - const { id } = req.params; - let visaData = await getVisaData(); - - if (!visaData || !visaData.hero || !visaData.hero.summaryList) { - return res - .status(400) - .json({ error: "Cấu trúc dữ liệu Visa không hợp lệ" }); - } - - // ✅ Capture BEFORE state - const beforeData = JSON.parse( - JSON.stringify(visaData.toObject ? visaData.toObject() : visaData), - ); - - // 2. Tìm index theo ID (Chuyển về Number để so sánh chính xác) - const countryIndex = visaData.hero.summaryList.findIndex( - (c) => c.id === parseInt(id), - ); - - if (countryIndex === -1) { - return res - .status(404) - .json({ error: `Không tìm thấy quốc gia có ID: ${id}` }); - } - - const currentCountry = visaData.hero.summaryList[countryIndex]; - let finalSlug = currentCountry.slug; - if (req.body.name) { - // Nếu name thay đổi, ta có thể cập nhật lại slug (tùy nhu cầu SEO) - // Ở đây ưu tiên: Nếu có slug mới truyền lên thì dùng, không thì tạo từ name mới - finalSlug = req.body.slug - ? createSlug(req.body.slug) - : createSlug(req.body.name); - } - // 3. Xử lý dữ liệu từ req.body - // Vì Client đã gửi JSON stringify, ta lấy trực tiếp hoặc parse nếu cần - let services = req.body.services; - if (typeof services === "string") { - try { - services = JSON.parse(services); - } catch (e) { - services = [services]; - } - } - - let detailedView = req.body.detailedView; - if (typeof detailedView === "string") { - try { - detailedView = JSON.parse(detailedView); - } catch (e) { - detailedView = currentCountry.detailedView; - } - } - - // 4. Cập nhật Object quốc gia - const updatedCountry = { - ...currentCountry, // Giữ các trường cũ - id: parseInt(id), // Đảm bảo ID không đổi - name: req.body.name || currentCountry.name, - slug: finalSlug, - icon: req.body.icon || currentCountry.icon, - services: Array.isArray(services) ? services : currentCountry.services, - detailedView: detailedView || currentCountry.detailedView, - }; - - // 5. Cập nhật vào mảng chính - visaData.hero.summaryList[countryIndex] = updatedCountry; - - // 6. Lưu vào Database - if (visaData.markModified) { - // Bắt buộc với Mongoose khi thay đổi nội dung bên trong Array/Object - visaData.markModified("hero.summaryList"); - } - - let savedData; - if (visaData._id) { - savedData = await visaData.save(); // Sử dụng save() trực tiếp nếu visaData là Mongoose Document - } else { - savedData = await Visa.create(visaData); - } - - // ✅ Capture AFTER state - const afterData = JSON.parse( - JSON.stringify(savedData.toObject ? savedData.toObject() : savedData), - ); - - // ✅ AUDIT LOGGING - Visa Country Updated - const changes = diffObject(beforeData, afterData); - if (changes.length > 0) { - await writeAuditLog({ - model: "Visa", - documentId: savedData._id, - action: AUDIT_ACTIONS.UPDATE_VISA, - before: beforeData, - after: afterData, - changes, - req, - }); - console.log( - `✅ Audit log created for Visa country update: ${changes.length} changes`, - ); - } - - console.log( - `✅ Country "${updatedCountry.name}" updated successfully by ID: ${id}`, - ); - res.json({ - success: true, - message: `Quốc gia "${updatedCountry.name}" đã được cập nhật thành công`, - country: updatedCountry, - }); - } catch (err) { - console.error("Update country error:", err); - res.status(500).json({ error: err.message }); - } -}; - -// Delete country -exports.deleteCountry = async (req, res) => { - try { - // 1. Lấy id từ params - const { id } = req.params; - let visaData = await getVisaData(); - - if (!visaData || !visaData.hero || !visaData.hero.summaryList) { - return res - .status(400) - .json({ success: false, error: "Cấu trúc dữ liệu Visa không hợp lệ" }); - } - - // 2. Tìm index theo ID (Chuyển về Number để so sánh chính xác) - const countryIndex = visaData.hero.summaryList.findIndex( - (c) => c.id === parseInt(id), - ); - - if (countryIndex === -1) { - return res.status(404).json({ - success: false, - error: `Không tìm thấy quốc gia có ID: ${id}`, - }); - } - - // 3. Xóa phần tử khỏi mảng - const deletedCountry = visaData.hero.summaryList[countryIndex]; - visaData.hero.summaryList.splice(countryIndex, 1); - - // 4. Cập nhật vào Database - if (visaData.markModified) { - visaData.markModified("hero.summaryList"); - } - - if (visaData._id) { - await visaData.save(); - } else { - await Visa.create(visaData); - } - - console.log(`✅ Deleted Successfully: "${deletedCountry.name}"`); - return res.json({ - success: true, - message: `Country "${deletedCountry.name}" Deleted Successfully`, - }); - } catch (err) { - console.error("❌ Error Delete:", err); - return res.status(500).json({ success: false, error: err.message }); - } -}; - -// -------------------- Public API Exports -------------------- - -// API to get all visa data for frontend -exports.api = async (req, res) => { - try { - const visaData = await getVisaData(); - if (!visaData) { - return res.status(404).json({ - success: false, - error: "Visa data not found", - data: null, - }); - } - const heroData = visaData?.hero; - - // 2. Lấy riêng phần hero (Dùng biến mới, không gán đè vào const) - const processedData = heroData; - - return res.json({ - success: true, - hero: processedData, - }); - } catch (err) { - console.error("Visa API error:", err); - res.status(500).json({ - success: false, - error: "Error loading visa data", - }); - } -}; - -// API to get all countries (summaryList only) -exports.apiCountries = async (req, res) => { - try { - const visaData = await getVisaData(); - - if (!visaData || !visaData.hero || !visaData.hero.summaryList) { - return res.status(404).json({ - success: false, - error: "Countries data not found", - data: null, - }); - } - - // 1. Lọc bỏ 'viewDetail' khỏi từng quốc gia trong danh sách - const filteredCountries = visaData.hero.summaryList.map((item) => { - // Tách detailedView ra, gom phần còn lại vào countryInfo - const { detailedView, ...countryInfo } = item; - - return { - ...countryInfo, - // Lấy mainImage từ sâu bên trong detailedView và gán vào key mới - mainImage: detailedView?.activeCountry?.mainImage || "", - }; - }); - - // 2. Gắn baseUrl vào ảnh cho danh sách đã lọc - const processedData = filteredCountries; - - return res.json({ - success: true, - data: processedData, // Lúc này data chỉ chứa thông tin quốc gia, không có viewDetail - }); - } catch (err) { - console.error("Countries API error:", err); - res.status(500).json({ - success: false, - error: "Error loading countries data", - }); - } -}; - -// API to get single country by slug -exports.apiCountry = async (req, res) => { - try { - const { slug } = req.params; - const visaData = await getVisaData(); - - if (!visaData || !visaData.hero || !visaData.hero.summaryList) { - return res.status(404).json({ - success: false, - error: "Visa data not found", - data: null, - }); - } - - // 1. Tìm quốc gia khớp với slug - const country = visaData.hero.summaryList.find((c) => c.slug === slug); - - // 2. Kiểm tra nếu không thấy quốc gia hoặc quốc gia đó không có viewDetail - if (!country || !country.viewDetail) { - return res.status(404).json({ - success: false, - error: `Detailed information for country "${slug}" not found`, - data: null, - }); - } - // 3. Chỉ lấy phần chi tiết (detailed view) - // Lưu ý: Chúng ta copy ra object mới để tránh tham chiếu dữ liệu gốc - const detailedData = JSON.parse(JSON.stringify(country.viewDetail)); - - // 4. Gắn baseUrl vào các ảnh nằm trong phần chi tiết này - const processedData = detailedData; - - return res.json({ - success: true, - data: processedData, - }); - } catch (err) { - console.error("Visa country API error:", err); - res.status(500).json({ - success: false, - error: "Error loading country detailed data", - }); - } -}; - -// API to get hero data (title + summaryList) -exports.apiHero = async (req, res) => { - try { - const visaData = await getVisaData(); - - // 1. Kiểm tra dữ liệu gốc - - if (!visaData || !visaData.hero) { - return res.status(404).json({ - success: false, - error: "Hero data not found", - data: null, - }); - } - const { summaryList, ...heroData } = JSON.parse( - JSON.stringify(visaData.hero), - ); - - const baseUrl = - process.env.BACKEND_URL ?? `${req.protocol}://${req.get("host")}`; - const processedData = addBaseUrlToImages(heroData, baseUrl); - - return res.json({ - success: true, - data: processedData, - }); - } catch (err) { - console.error("Visa hero API error:", err); - res.status(500).json({ - success: false, - error: "Error loading hero data", - }); - } -}; diff --git a/data/about.json b/data/about.json index 246a6c7..494c24b 100644 --- a/data/about.json +++ b/data/about.json @@ -1,91 +1,111 @@ { "hero": { - "title": "About Us", - "breadcrumb": [ - "Home", - "About Us" - ], - "backgroundImage": "/uploads/about/breadcrumb.jpg" + "badge": "Our Mission", + "title": "Education Without Boundaries.", + "description": "We believe high-quality education should be accessible and affordable for everyone. Our mission is to empower global learners through flexible, innovative online models that fit real lives.", + "studentCount": "50,000+", + "image": "/assets/img/hero.png", + "imageAlt": "Diverse group of university students collaborating online", + "coreValues": ["Radical Affordability", "Absolute Flexibility", "Global Inclusivity"] }, - "intro": { - "subheading": "Company Intro", - "heading": "Building Pathways to Your Immigration Success", - "description": "We provide expert guidance, personalized solutions, and transparent processes to help you achieve your immigration goals. Our dedicated team ensures a smooth journey, building pathways to your international success.", - "image": "/uploads/about/businessman.jpg" - }, - "mission": { - "subheading": "About Our Consultancy", - "heading": "Turning Study Abroad Dreams Into Reality", - "description": "We guide students with expert visa consulting, ensuring a smooth process from application to approval, turning study abroad aspirations into life-changing opportunities for a brighter future.", - "images": { - "main": "/uploads/about/375x419.jpg", - "secondary": "/uploads/about/375x419.jpg", - "bgShape": "/assets/img/home-1/about/Vector.png", - "planeShape": "/assets/img/home-1/about/plane.png", - "topShape": "/assets/img/home-1/about/shape.png", - "globeShape": "/assets/img/home-1/about/globe.png" - }, - "items": [ + "leadership": { + "heading": "Guided by Visionaries", + "description": "Our leadership team brings decades of experience from top-tier academic institutions and leading technology companies.", + "members": [ { - "icon": "/assets/img/home-1/icon/01.svg", - "label": "Global Reach", - "description": "Expanding Opportunities Worldwide" + "name": "Dr. Sarah Chen", + "role": "President & Founder", + "bio": "Former Dean of Innovation at Stanford, pioneer in scalable ed-tech solutions.", + "avatar": "https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-5.jpg", + "social": { "linkedin": "#", "twitter": "#" } }, { - "icon": "/assets/img/home-1/icon/01.svg", - "label": "Global Reach", - "description": "Expanding Opportunities Worldwide" + "name": "Marcus Johnson", + "role": "Chief Academic Officer", + "bio": "20+ years developing curriculum for asynchronous learning environments.", + "avatar": "https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-6.jpg", + "social": { "linkedin": "#" } + }, + { + "name": "Elena Rodriguez", + "role": "VP of Student Success", + "bio": "Passionate advocate for non-traditional student support systems.", + "avatar": "https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-7.jpg", + "social": { "linkedin": "#", "twitter": "#" } + }, + { + "name": "David Kim", + "role": "Chief Technology Officer", + "bio": "Former Google engineering lead building our seamless campus portal.", + "avatar": "https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-8.jpg", + "social": { "linkedin": "#" } } - ], - "features": [ - "Fastest Visa form processing with skilled immigration agents", - "Partnership with International Educational Institutions" - ], - "ctaButton": { - "label": "Get Started", - "href": "/about" + ] + }, + "learningModel": { + "heading": "Our Flexible Learning Model", + "description": "Choose how you learn. We offer two distinct paths to ensure your education fits your life, not the other way around.", + "async": { + "title": "Asynchronous Learning", + "description": "Learn completely at your own pace. Perfect for working professionals and those with unpredictable schedules.", + "features": [ + { "icon": "fa-play", "title": "Pre-recorded Lectures", "desc": "Watch anytime, anywhere, and pause when you need to." }, + { "icon": "fa-calendar-check", "title": "Flexible Deadlines", "desc": "Complete assignments within broad, manageable windows." }, + { "icon": "fa-comments", "title": "Discussion Boards", "desc": "Engage with peers on your own timeline." } + ] + }, + "sync": { + "title": "Synchronous Learning", + "description": "Experience the structure of a traditional classroom online. Ideal for those who thrive on real-time interaction.", + "features": [ + { "icon": "fa-video", "title": "Live Virtual Classes", "desc": "Attend scheduled lectures with your professors and peers." }, + { "icon": "fa-hand-sparkles", "title": "Real-time Collaboration", "desc": "Work on group projects in live breakout rooms." }, + { "icon": "fa-clipboard-question", "title": "Immediate Feedback", "desc": "Ask questions and get answers during class time." } + ] } }, - "features": { - "backgroundImage": "/assets/img/home-2/feature/bg-shape.png", - "subheading": "Your Travel Made Easy", - "heading": "Smooth Visa Journey Guaranteed", - "description": "We provide expert guidance for every visa application, ensuring smooth processing, personalized support, and reliable assistance", - "image": "/uploads/about/686x906.jpg", - "items": [ - { - "icon": "/assets/img/home-2/icon/01.png", - "title": "Expert Consultants", - "description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors." - }, - { - "icon": "/assets/img/home-2/icon/01.png", - "title": "Personalized Support", - "description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors." - }, - { - "icon": "/assets/img/home-2/icon/01.png", - "title": "Transparent Process", - "description": "Skilled and knowledgeable visa advisors. Skilled and knowledgeable visa advisors." - } + "accreditation": { + "heading": "Recognized Quality. Real Results.", + "description": "We hold the highest standards of academic excellence, ensuring your degree is respected by employers worldwide.", + "badges": [ + { "icon": "fa-award", "title": "Fully Accredited", "desc": "By the Higher Learning Commission (HLC)." }, + { "icon": "fa-briefcase", "title": "Top Employer Network", "desc": "Partnerships with Fortune 500 companies." } ], - "ctaButton": { - "label": "Get Started Today", - "href": "/contact" - } + "stats": [ + { "value": "89%", "label": "Employed within 6 months" }, + { "value": "$72k", "label": "Average Starting Salary" }, + { "value": "15k+", "label": "Active Alumni Network" }, + { "value": "#1", "label": "For Online ROI (2025)", "isPrimary": true } + ] }, - "news": { - "subheading": "Visa Tips & Guides", - "heading": "Latest Insights & Updates", - "ctaButton": { - "label": "view all articles", - "href": "/blog" - }, - "selectedBlogIds": [ - "69857d6c6d04fed459107944", - "69857d6c6d04fed459107942", - "69857d6c6d04fed459107940" - ], - "items": [] + "successStories": { + "heading": "Student Success Stories", + "description": "Hear from our diverse community of learners who transformed their careers through our flexible programs.", + "stories": [ + { + "quote": "The asynchronous model allowed me to study coding late at night after my kids went to bed. I transitioned from retail to a Junior Developer role in just 14 months.", + "name": "Maria S.", + "program": "B.S. Computer Science '24", + "avatar": "https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-1.jpg" + }, + { + "quote": "The pay-per-course option meant I didn't have to take out massive student loans. I earned my MBA debt-free while continuing to work full-time.", + "name": "James T.", + "program": "MBA '25", + "avatar": "https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-4.jpg" + }, + { + "quote": "I loved the synchronous virtual classes. It felt like a real campus environment. The networking opportunities directly led to my current job in data analytics.", + "name": "Aisha K.", + "program": "Data Analytics Cert '23", + "avatar": "https://storage.googleapis.com/uxpilot-auth.appspot.com/avatars/avatar-2.jpg" + } + ] + }, + "cta": { + "heading": "Ready to Transform Your Future?", + "description": "Join a global community of learners and take the first step towards a flexible, affordable degree.", + "primaryButton": { "label": "Apply Now", "href": "/admissions" }, + "secondaryButton": { "label": "Request Information", "href": "/contact" } } } \ No newline at end of file diff --git a/data/footer.json b/data/footer.json index 5a617f5..7120de6 100644 --- a/data/footer.json +++ b/data/footer.json @@ -1,80 +1,81 @@ { - "top": { - "bgImage": "/assets/img/home-1/footer-bg.jpg", - "phone": { - "display": "+84 961 83 4040", - "href": "tel:+84961834040" - }, - "address": "734 Luy Ban Bich St, Tan Thanh Ward, Tan Phu Dist, HCMC", - "logo": { - "src": "/assets/img/logo/white-logo.svg", - "alt": "logo", - "href": "/" - }, - "menuLinks": [ - { - "label": "Home", - "href": "/" - }, - { - "label": "About Us", - "href": "/about" - }, - { - "label": "Visa", - "href": "/country-details" - }, - { - "label": "Pages", - "href": "/news-details" - }, - { - "label": "Article", - "href": "/news" - }, - { - "label": "Contact Us", - "href": "/contact" - } - ], - "socialLinks": [ - { - "icon": "fa-brands fa-twitter", - "href": "#" - }, - { - "icon": "fa-brands fa-instagram", - "href": "#" - }, - { - "icon": "fa-brands fa-linkedin", - "href": "#" - }, - { - "icon": "fa-brands fa-youtube", - "href": "#" - } - ] + "brand": { + "logo": { + "image": "/assets/img/logo/logo.jpg", + "href": "/" }, - "bottom": { - "copyright": { - "text": "Copyright©", - "brand": "GRAMENTHEME", - "rights": "All Rights Reserved." - }, - "menuLinks": [ - { - "label": "Terms & Conditions", - "href": "/contact" - }, - { - "label": "Privacy Policy", - "href": "/contact" - }, - { - "label": "Contact Us", - "href": "/contact" - } - ] - } -} + "description": "Welcome to the LAMS, a premier platform offering courses from top universities and institutions around the world.", + "social": [ + { + "icon": "fa-twitter", + "href": "#" + }, + { + "icon": "fa-linkedin-in", + "href": "#" + }, + { + "icon": "fa-facebook-f", + "href": "#" + }, + { + "icon": "fa-instagram", + "href": "#" + } + ] + }, + "explore": { + "heading": "Explore", + "links": [ + { + "label": "Welcome / Overview", + "href": "/" + }, + { + "label": "Our Milestones", + "href": "/history" + }, + { + "label": "Accreditations", + "href": "/accreditations" + }, + { + "label": "Industry Partnerships", + "href": "/partnership" + }, + { + "label": "University Blog", + "href": "/blog" + } + ] + }, + "contact": { + "heading": "Contact Us", + "address": "207 Regent Street, London, England W1B3HH", + "phone": "12345678-EDU-ONLINE", + "email": "info@lams.ac" + }, + "newsletter": { + "heading": "Stay Updated", + "description": "Subscribe to our newsletter for the latest academic news and program updates.", + "placeholder": "Email Address", + "buttonText": "Subscribe" + }, + "bottom": { + "copyright": "2026 London Academy of Management and Sciences. All rights reserved..", + "links": [ + { + "label": "Privacy Policy", + "href": "/policies" + }, + { + "label": "Terms of Service", + "href": "/policies" + }, + { + "label": "Accessibility Statement", + "href": "/policies" + } + ] + } +} \ No newline at end of file diff --git a/data/home.json b/data/home.json index 0d4df4a..a023405 100644 --- a/data/home.json +++ b/data/home.json @@ -1,334 +1,130 @@ { "hero": { - "title": "From Application to Visa – We’ve Got You Covered", - "subtitle": "Global Education Simplified", - "description": "We guide you through every step of the education visa process, from initial application to final approval, ensuring a smooth, hassle-free journey.", - "primaryButton": { - "label": "Apply now", - "href": "/contact" - }, - "secondaryButton": { - "label": "Book Free Consultation", - "href": "/contact" - }, - "backgroundImage": "/assets/img/home-1/hero/bg.jpg", - "videoUrl": "https://www.youtube.com/watch?v=Cn4G2lZ_g2I" - }, - "whyChooseUs": { - "heading": "Turning Study Abroad Dreams Into Reality", - "subheading": "About Our Consultancy", - "description": "We guide students with expert visa consulting, ensuring a smooth process from application to approval, turning study abroad aspirations into life-changing opportunities for a brighter future.", - "items": [ - { - "icon": "/assets/img/home-1/icon/01.svg", - "title": "Global Reach", - "description": "Expanding Opportunities Worldwide" - }, - { - "icon": "/assets/img/home-1/icon/01.svg", - "title": "Expert Guidance", - "description": "Professional Support Every Step" - } - ], - "features": [ - "Fastest Visa form processing with skilled immigration agents", - "Partnership with International Educational Institutions" - ], - "ctaButton": { - "label": "Get Started", - "href": "/about" + "badge": "Top Ranked Online Education", + "title": "Advance Your Career Without Boundaries.", + "description": "Access world-class education from anywhere. Flexible schedules, accredited programs, and affordable tuition designed for the modern professional.", + "searchPlaceholder": "e.g. Business Administration", + "buttonLabel": "Find Program", + "image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/d9383b2d27-55d93496971925dee83d.png", + "imageAlt": "diverse group of modern adult students studying online with laptops, professional lighting, cinematic, high quality", + "floatingBadge": { + "icon": "fa-users", + "value": "50,000+", + "label": "Active Students" } }, - "visaSolutions": { - "heading": "Comprehensive Visa Solutions", - "subheading": "Our Expert Services", - "items": [ - { - "number": "01", - "title": "Student Visa Guidance", - "description": "Assistance with admission, documentation, and visa application.Assistance", - "link": "/services/student-visa" - }, - { - "number": "02", - "title": "PTE Exam Preparation", - "description": "We provide expert guidance and personalized support throughout the education visa process,", - "link": "/services/pte-exam" - }, - { - "number": "03", - "title": "University Selection Assistance", - "description": "We provide expert guidance and personalized support throughout the education visa process,", - "link": "/services/university-selection" - }, - { - "number": "04", - "title": "IELTS Exam Preparation", - "description": "We provide expert guidance and personalized support throughout the education visa process,", - "link": "/services/ielts-exam" - } - ] - }, - "visaCountries": { - "heading": "Visa & VISAWAY Services To UK", - "subheading": "UK. United Kingdom", - "description": "The Express Entry program is designed for skilled workers who wish to immigrate to Canada. It includes the Federal Skilled Worker Program, the Federal Skilled…", - "countries": [ - { - "name": "United Kingdom", - "code": "UK", - "flag": "/assets/img/home-1/feature/shape.png", - "link": "/country-details/uk", - "visaTypes": [ - "Visitor Visa", - "Student Visa & Admission", - "Work Visa – H1B", - "Business Visa", - "Work permit for Canada", - "Student Visa for Canada" - ] - }, - { - "name": "United States", - "code": "US", - "flag": "/assets/img/flags/us.png", - "link": "/country-details/us", - "visaTypes": [ - "Student Visa F-1", - "Work Visa H1-B", - "Tourist Visa B-2" - ] - }, - { - "name": "Canada", - "code": "CA", - "flag": "/assets/img/flags/canada.png", - "link": "/country-details/canada", - "visaTypes": [ - "Study Permit", - "Work Permit", - "Express Entry" - ] - }, - { - "name": "Australia", - "code": "AU", - "flag": "/assets/img/flags/australia.png", - "link": "/country-details/australia", - "visaTypes": [ - "Student Visa 500", - "Skilled Migration", - "Working Holiday" - ] - }, - { - "name": "Germany", - "code": "DE", - "flag": "/assets/img/flags/germany.png", - "link": "/country-details/germany", - "visaTypes": [ - "Student Visa", - "Job Seeker Visa", - "EU Blue Card" - ] - } - ], - "ctaButton": { - "label": "Get Started", - "href": "/contact" - } - }, - "testimonials": { - "heading": "Student Reviews & Testimonials", - "subheading": "What Our Students Say", - "videoUrl": "https://www.youtube.com/watch?v=Cn4G2lZ_g2I", - "videoThumbnail": "/assets/img/home-1/testimonial/01.jpg", - "items": [ - { - "name": "Sohel Tanvir", - "role": "Student", - "country": "Canada", - "rating": 5, - "comment": "Professional and reliable service. They explained each step clearly, prepared my documents, and supported me during the interview. My visa approval came faster than expected.", - "avatar": "/assets/img/home-1/testimonial/client.png" - }, - { - "name": "Ayesha Rahman", - "role": "Student", - "country": "UK. United Kingdom", - "rating": 5, - "comment": "The consultancy guided me from start to finish, making my study abroad journey smooth and stress-free. Thanks to their expert support, I secured my visa successfully.", - "avatar": "/assets/img/home-1/testimonial/client-2.png" - }, - { - "name": "Michael Chen", - "role": "Graduate Student", - "country": "Australia", - "rating": 5, - "comment": "Outstanding service from beginning to end. The team was knowledgeable, responsive, and made the entire visa process seamless. Highly recommend to anyone planning to study abroad.", - "avatar": "/assets/img/home-1/testimonial/client.png" - } - ] - }, - "videoGallery": { - "heading": "VIDEO PLAY GALLERY", - "videoUrl": "https://ex-coders.com/vdo/visa.mp4", - "thumbnail": "/assets/img/home-1/feature/text.png" - }, - "faq": { - "heading": "Got Questions? We've Got Answers", - "subheading": "Visa FAQs", - "description": "We understand students often have many questions about studying abroad. Our experts provide clear.", - "ctaButton": { - "label": "contact us", - "href": "/contact" + "quickLinks": [ + { + "icon": "fa-trophy", + "title": "Milestones", + "description": "Explore our history of academic excellence and major achievements since our founding.", + "linkText": "Explore History", + "href": "/about/history" }, - "items": [ - { - "question": "How long does the student visa process usually take?", - "answer": "The student visa process typically takes 4-8 weeks depending on the country and time of year. We recommend starting the application process at least 3 months before your intended travel date to ensure sufficient time for document preparation and processing." - }, - { - "question": "Do you assist with scholarship applications as well?", - "answer": "Yes, we guide students in identifying suitable scholarships, preparing strong applications, and increasing chances of securing financial aid for their studies abroad." - }, - { - "question": "Will you guide me in preparing for the visa interview?", - "answer": "Absolutely! We provide comprehensive visa interview preparation, including mock interviews, document review, and tips on how to answer common questions confidently and effectively." - }, - { - "question": "Do you offer post-arrival support for students?", - "answer": "Yes, we provide post-arrival support including airport pickup coordination, accommodation assistance, university orientation guidance, and ongoing support throughout your study period." - }, - { - "question": "What documents are required for a student visa application?", - "answer": "Required documents typically include a valid passport, university acceptance letter, proof of financial support, academic transcripts, language proficiency test scores, and health insurance. We provide a complete checklist tailored to your destination country." - } - ] - }, - "achievements": { - "heading": "Our Achievements in Numbers", - "subheading": "Did You Know", - "items": [ - { - "value": "1000", - "suffix": "k+", - "label": "Students Guided", - "description": "Successfully assisted over a thousand students worldwide." - }, - { - "value": "50", - "suffix": "+", - "label": "Countries Covered", - "description": "Helping students apply to universities in more than 50 countries." - }, - { - "value": "95", - "suffix": "%", - "label": "Visa Success Rate", - "description": "Inspired students to reach their goals globally" - }, - { - "value": "10", - "suffix": "+", - "label": "Years of Experience", - "description": "Trusted experts in global education consulting." - } - ] - }, - "partners": { - "visaConsultancy": { - "heading": "Our Achievements & Awards", - "items": [ - { - "name": "Best Visa Consultancy", - "icon": "/assets/img/home-1/feature/icon-1.png", - "year": "2025" - }, - { - "name": "Visa Success Award", - "icon": "/assets/img/home-1/feature/icon-2.png", - "year": "2025" - }, - { - "name": "Innovation Award", - "icon": "/assets/img/home-1/feature/icon-3.png", - "year": "2025" - }, - { - "name": "Global Education Partner", - "icon": "/assets/img/home-1/feature/icon-4.png", - "year": "2025" - } - ] + { + "icon": "fa-certificate", + "title": "Accreditations", + "description": "Review our globally recognized credentials ensuring the highest quality of education.", + "linkText": "View Credentials", + "href": "/about/accreditation" }, - "brands": { - "items": [ - { - "logo": "/assets/img/home-1/brand/01.png" - }, - { - "logo": "/assets/img/home-1/brand/02.png" - }, - { - "logo": "/assets/img/home-1/brand/03.png" - }, - { - "logo": "/assets/img/home-1/brand/04.png" - }, - { - "logo": "/assets/img/home-1/brand/05.png" - } - ] - } - }, - "blogPreview": { - "heading": "Latest Insights & Updates", - "subheading": "Visa Tips & Guides", - "ctaButton": { - "label": "view all articles", + { + "icon": "fa-handshake", + "title": "Partnerships", + "description": "Discover our network of industry leaders providing career pathways for graduates.", + "linkText": "See Partners", + "href": "/about/partnerships" + }, + { + "icon": "fa-newspaper", + "title": "Latest Blog", + "description": "Read insights, student success stories, and updates from our academic community.", + "linkText": "Read Articles", "href": "/blog" - }, + } + ], + "valueProp": { + "badge": "Why Choose Us", + "title": "Education Engineered for the Modern World", + "description": "We believe that quality education should be accessible to everyone, regardless of location or schedule. Our platform delivers an immersive learning experience backed by industry-leading technology.", + "features": [ + { + "icon": "fa-laptop-code", + "title": "100% Online Flexibility", + "description": "Study on your own time with asynchronous classes designed to fit around your work and life commitments." + }, + { + "icon": "fa-piggy-bank", + "title": "Affordable Tuition", + "description": "Graduate with less debt. Our transparent pricing and financial aid options make your degree attainable." + }, + { + "icon": "fa-briefcase", + "title": "Career-Focused Curriculum", + "description": "Programs developed in partnership with industry leaders to ensure you learn the skills employers actually want." + } + ], + "stats": [ + { + "value": "94%", + "label": "Employment rate within 6 months of graduation", + "image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/b1e2e6a5a4-7c815d7e47c7e7471c5e.png", + "imageAlt": "student studying late at night looking focused" + }, + { + "value": "200+", + "label": "Accredited degree and certificate programs", + "image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/8e7cd5934f-dc6192e3a13ea24a5312.png", + "imageAlt": "graduation cap and diploma abstract professional setup" + } + ] + }, + "programs": { + "heading": "Featured Programs", + "description": "Discover our most popular career-focused degrees and certificates designed for the modern job market.", "items": [ { - "title": "Step-by-Step Guide to Applying for a Student Visa", - "excerpt": "Learn the complete process of applying for a student visa, from gathering documents to attending your interview. Our comprehensive guide covers everything you need to know.", - "category": "Student Visa", - "date": "2025-08-20", - "author": { - "name": "Sohel", - "avatar": "/assets/img/home-1/news/client.png" - }, - "comments": 8, - "link": "/blog/step-by-step-guide-student-visa", - "thumbnail": "/assets/img/home-1/news/news-1.jpg" + "category": "Tech & Software", + "image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/f6c491e508-064cc521a89dee9776d1.png", + "duration": "12-18 Months", + "rating": "4.9", + "title": "B.S. Computer Science", + "description": "Master full-stack development, algorithms, and system design with hands-on projects.", + "studentCount": "+1k", + "href": "/programmes/bs-cs" }, { - "title": "Tips to Prepare Financial Documents for Visa Approval", - "excerpt": "Financial documentation is crucial for visa approval. Discover expert tips on preparing bank statements, sponsorship letters, and proof of funds that meet embassy requirements.", - "category": "IELTS / TOEFL", - "date": "2025-08-20", - "author": { - "name": "Sohel", - "avatar": "/assets/img/home-1/news/client.png" - }, - "comments": 8, - "link": "/blog/financial-documents-visa-approval", - "thumbnail": "/assets/img/home-1/news/news-2.jpg" + "category": "Data Analysis", + "image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/e17be2a4e6-91247c5d9a4eed4d28a9.png", + "duration": "8-12 Months", + "rating": "4.8", + "title": "Data Analytics Certificate", + "description": "Learn SQL, Python, and Tableau to transform complex data into actionable business insights.", + "studentCount": "+800", + "href": "/programmes/cert-da" }, { - "title": "Post-Arrival Guide What Every Student Should Know", - "excerpt": "Moving to a new country can be overwhelming. Our post-arrival guide helps international students navigate accommodation, banking, healthcare, and cultural adaptation successfully.", - "category": "Study Abroad", - "date": "2025-08-20", - "author": { - "name": "Sohel", - "avatar": "/assets/img/home-1/news/client.png" - }, - "comments": 8, - "link": "/blog/post-arrival-guide-students", - "thumbnail": "/assets/img/home-1/news/news-3.jpg" + "category": "Business", + "image": "https://storage.googleapis.com/uxpilot-auth.appspot.com/c53c1f7b37-66e95dbe58513f7bce4b.png", + "duration": "18-24 Months", + "rating": "4.9", + "title": "MBA in Leadership", + "description": "Develop strategic management skills and leadership qualities for the modern corporate world.", + "studentCount": "+2k", + "href": "/programmes/mba-db" } ] + }, + "requestInfo": { + "heading": "Take the Next Step in Your Career.", + "description": "Request more information about our programs, tuition, and admissions process. Our advisors are ready to help you plan your future.", + "phone": "12345678-GLOBAL-U", + "email": " info@lams.ac", + "programs": [ + "Computer Science", + "Business Administration", + "Data Analytics", + "Nursing" + ] } -} +} \ No newline at end of file diff --git a/models/about.js b/models/about.js new file mode 100644 index 0000000..99ee943 --- /dev/null +++ b/models/about.js @@ -0,0 +1,161 @@ +const mongoose = require("mongoose"); + +const { Schema } = mongoose; + +// Hero +const HeroSchema = new Schema( + { + badge: { type: String, default: "" }, + title: { type: String, default: "" }, + description: { type: String, default: "" }, + studentCount: { type: String, default: "" }, + image: { type: String, default: "" }, + imageAlt: { type: String, default: "" }, + coreValues: { type: [String], default: [] }, + }, + { _id: false }, +); + +// Leadership +const SocialSchema = new Schema( + { + linkedin: { type: String, default: "" }, + twitter: { type: String, default: "" }, + }, + { _id: false }, +); + +const LeadershipMemberSchema = new Schema( + { + name: { type: String, default: "" }, + role: { type: String, default: "" }, + bio: { type: String, default: "" }, + avatar: { type: String, default: "" }, + social: { type: SocialSchema, default: () => ({}) }, + }, + { _id: false }, +); + +const LeadershipSchema = new Schema( + { + heading: { type: String, default: "" }, + description: { type: String, default: "" }, + members: { type: [LeadershipMemberSchema], default: [] }, + }, + { _id: false }, +); + +// Learning Model +const LearningFeatureSchema = new Schema( + { + icon: { type: String, default: "" }, + title: { type: String, default: "" }, + desc: { type: String, default: "" }, + }, + { _id: false }, +); + +const LearningModeSchema = new Schema( + { + title: { type: String, default: "" }, + description: { type: String, default: "" }, + features: { type: [LearningFeatureSchema], default: [] }, + }, + { _id: false }, +); + +const LearningModelSchema = new Schema( + { + heading: { type: String, default: "" }, + description: { type: String, default: "" }, + async: { type: LearningModeSchema, default: () => ({}) }, + sync: { type: LearningModeSchema, default: () => ({}) }, + }, + { _id: false }, +); + +// Accreditation +const AccreditationBadgeSchema = new Schema( + { + icon: { type: String, default: "" }, + title: { type: String, default: "" }, + desc: { type: String, default: "" }, + }, + { _id: false }, +); + +const AccreditationStatSchema = new Schema( + { + value: { type: String, default: "" }, + label: { type: String, default: "" }, + isPrimary: { type: Boolean, default: false }, + }, + { _id: false }, +); + +const AccreditationSchema = new Schema( + { + heading: { type: String, default: "" }, + description: { type: String, default: "" }, + badges: { type: [AccreditationBadgeSchema], default: [] }, + stats: { type: [AccreditationStatSchema], default: [] }, + }, + { _id: false }, +); + +// Success Stories +const StorySchema = new Schema( + { + quote: { type: String, default: "" }, + name: { type: String, default: "" }, + program: { type: String, default: "" }, + avatar: { type: String, default: "" }, + }, + { _id: false }, +); + +const SuccessStoriesSchema = new Schema( + { + heading: { type: String, default: "" }, + description: { type: String, default: "" }, + stories: { type: [StorySchema], default: [] }, + }, + { _id: false }, +); + +// CTA +const CtaButtonSchema = new Schema( + { + label: { type: String, default: "" }, + href: { type: String, default: "" }, + }, + { _id: false }, +); + +const CtaSchema = new Schema( + { + heading: { type: String, default: "" }, + description: { type: String, default: "" }, + primaryButton: { type: CtaButtonSchema, default: () => ({}) }, + secondaryButton: { type: CtaButtonSchema, default: () => ({}) }, + }, + { _id: false }, +); + +// Root schema +const AboutSchema = new Schema( + { + hero: { type: HeroSchema, default: () => ({}) }, + leadership: { type: LeadershipSchema, default: () => ({}) }, + learningModel: { type: LearningModelSchema, default: () => ({}) }, + accreditation: { type: AccreditationSchema, default: () => ({}) }, + successStories: { type: SuccessStoriesSchema, default: () => ({}) }, + cta: { type: CtaSchema, default: () => ({}) }, + }, + { + timestamps: true, + strict: false, + }, +); + +module.exports = mongoose.model("About", AboutSchema); diff --git a/models/appointment.js b/models/appointment.js deleted file mode 100644 index 4416f0a..0000000 --- a/models/appointment.js +++ /dev/null @@ -1,206 +0,0 @@ -const mongoose = require("mongoose"); - -// Clear cache -if (mongoose.models.Appointment) { - delete mongoose.models.Appointment; -} -if (mongoose.connection.models.Appointment) { - delete mongoose.connection.models.Appointment; -} - -// Schema cho hero section -const heroSchema = new mongoose.Schema( - { - title: { - type: String, - trim: true, - default: "Make Appointment", - }, - backgroundImage: { - type: String, - trim: true, - default: "", - }, - subtitle: { - type: String, - trim: true, - default: "", - }, - heading: { - type: String, - trim: true, - default: "", - }, - description: { - type: String, - trim: true, - default: "", - }, - }, - { _id: false } -); - -// Schema cho form field -const formFieldSchema = new mongoose.Schema( - { - name: { - type: String, - required: true, - trim: true, - }, - label: { - type: String, - trim: true, - default: "", - }, - type: { - type: String, - required: true, - trim: true, - enum: ["text", "email", "tel", "textarea", "date", "select"], - }, - placeholder: { - type: String, - trim: true, - default: "", - }, - required: { - type: Boolean, - default: false, - }, - colClass: { - type: String, - trim: true, - default: "col-lg-12", - }, - }, - { _id: false } -); - -// Schema cho submit button -const submitButtonSchema = new mongoose.Schema( - { - text: { - type: String, - required: true, - trim: true, - default: "Request Appointment", - }, - icon: { - type: String, - trim: true, - default: "fa-solid fa-arrow-right", - }, - buttonClass: { - type: String, - trim: true, - default: "theme-btn", - }, - }, - { _id: false } -); - -// Schema cho form -const formSchema = new mongoose.Schema( - { - heading: { - type: String, - trim: true, - default: "Request Appointment", - }, - fields: { - type: [formFieldSchema], - default: [], - }, - submitButton: { - type: submitButtonSchema, - default: () => ({}), - }, - }, - { _id: false } -); - -// Main Appointment Schema -const appointmentSchema = new mongoose.Schema( - { - name: { - type: String, - default: "default", - unique: true, - }, - hero: { - type: heroSchema, - default: () => ({}), - }, - visaOptions: { - type: [String], - default: [], - }, - form: { - type: formSchema, - default: () => ({}), - }, - }, - { - timestamps: true, - } -); - -// Migration method to import data from JSON -appointmentSchema.statics.migrateFromJson = async function (jsonData) { - try { - // Check if default appointment exists - const existingAppointment = await this.findOne({ name: "default" }); - - // Process data from JSON - const processedData = { - hero: { - title: jsonData.hero?.title || "Make Appointment", - backgroundImage: jsonData.hero?.backgroundImage || "", - subtitle: jsonData.hero?.subtitle || "", - heading: jsonData.hero?.heading || "", - description: jsonData.hero?.description || "", - }, - visaOptions: Array.isArray(jsonData.visaOptions) ? jsonData.visaOptions : [], - form: { - heading: jsonData.form?.heading || "Request Appointment", - fields: (jsonData.form?.fields || []).map((field) => ({ - name: field.name || "", - label: field.label || "", - type: field.type || "text", - placeholder: field.placeholder || "", - required: field.required || false, - colClass: field.colClass || "col-lg-12", - })), - submitButton: { - text: jsonData.form?.submitButton?.text || "Request Appointment", - icon: jsonData.form?.submitButton?.icon || "fa-solid fa-arrow-right", - buttonClass: jsonData.form?.submitButton?.buttonClass || "theme-btn", - }, - }, - }; - - if (existingAppointment) { - // Update existing appointment - existingAppointment.hero = processedData.hero; - existingAppointment.visaOptions = processedData.visaOptions; - existingAppointment.form = processedData.form; - await existingAppointment.save(); - console.log("Appointment data updated successfully"); - return existingAppointment; - } else { - // Create new appointment - const newAppointment = await this.create({ - name: "default", - ...processedData, - }); - console.log("Appointment data imported successfully"); - return newAppointment; - } - } catch (error) { - console.error("Error migrating appointment data:", error); - throw error; - } -}; - -module.exports = mongoose.model("Appointment", appointmentSchema); diff --git a/models/appointmentSubmission.js b/models/appointmentSubmission.js deleted file mode 100644 index 6987f90..0000000 --- a/models/appointmentSubmission.js +++ /dev/null @@ -1,83 +0,0 @@ -const mongoose = require("mongoose"); - -/** - * Schema for Appointment Submissions - * Stores appointment requests from users - */ -const appointmentSubmissionSchema = new mongoose.Schema( - { - name: { - type: String, - required: [true, "Name is required"], - trim: true, - maxlength: [100, "Name cannot exceed 100 characters"], - }, - email: { - type: String, - required: [true, "Email is required"], - trim: true, - lowercase: true, - match: [/^\S+@\S+\.\S+$/, "Please enter a valid email"], - }, - phone: { - type: String, - trim: true, - default: "", - }, - address: { - type: String, - trim: true, - default: "", - }, - appointmentDate: { - type: String, - trim: true, - default: "", - }, - message: { - type: String, - trim: true, - default: "", - }, - visaTypes: { - type: [String], - default: [], - }, - status: { - type: String, - enum: ["pending", "confirmed", "completed", "cancelled"], - default: "pending", - }, - notes: { - type: String, - trim: true, - default: "", - }, - confirmedAt: { - type: Date, - default: null, - }, - completedAt: { - type: Date, - default: null, - }, - ipAddress: { - type: String, - default: "", - }, - userAgent: { - type: String, - default: "", - }, - }, - { - timestamps: true, - } -); - -// Index for faster queries -appointmentSubmissionSchema.index({ status: 1, createdAt: -1 }); -appointmentSubmissionSchema.index({ email: 1 }); -appointmentSubmissionSchema.index({ appointmentDate: 1 }); - -module.exports = mongoose.model("AppointmentSubmission", appointmentSubmissionSchema); diff --git a/models/booking.js b/models/booking.js deleted file mode 100644 index a447c35..0000000 --- a/models/booking.js +++ /dev/null @@ -1,106 +0,0 @@ -const mongoose = require("mongoose"); - -// Clear cache -if (mongoose.models.Booking) { - delete mongoose.models.Booking; -} -if (mongoose.connection.models.Booking) { - delete mongoose.connection.models.Booking; -} - -const bookingSchema = new mongoose.Schema( - { - hero: { - title: String, - backgroundImage: String, - }, - - searchBar: { - locationLabel: String, - holidaySeasonLabel: String, - searchButtonText: String, - }, - - filterPanel: { - title: String, - priceTitle: String, - priceLabel: String, - pricePlaceholder: String, - priceMin: Number, - priceMax: Number, - activitiesTitle: String, - ageTitle: String, - ageSelectPlaceholder: String, - ageMin: Number, - ageMax: Number, - ratingTitle: String, - ratingOptions: [ - { - value: String, - label: String, - }, - ], - resetButtonText: String, - }, - - programs: [ - { - value: String, - label: String, - }, - ], - - holidays: [ - { - value: String, - label: String, - }, - ], - - locations: [ - { - value: String, - label: String, - }, - ], - - camps: [ - { - name: String, - price: Number, - priceText: String, - season: [String], - age: [Number], - locations: [String], - image: String, - link: String, - program: String, - rating: Number, - }, - ], - - // Configuration - Dùng Mixed type để chấp nhận bất kỳ structure nào - configuration: mongoose.Schema.Types.Mixed, - - formSteps: [ - { - step: Number, - title: String, - sections: [ - { - id: String, - fields: [mongoose.Schema.Types.Mixed], - }, - ], - }, - ], - - validation: mongoose.Schema.Types.Mixed, - }, - { - timestamps: true, - strict: false - } -); - -module.exports = mongoose.model("Booking", bookingSchema); \ No newline at end of file diff --git a/models/bookingSubmission.js b/models/bookingSubmission.js deleted file mode 100644 index 9e10e59..0000000 --- a/models/bookingSubmission.js +++ /dev/null @@ -1,200 +0,0 @@ -const mongoose = require("mongoose"); - -// Clear cache -if (mongoose.models.BookingSubmission) { - delete mongoose.models.BookingSubmission; -} -if (mongoose.connection.models.BookingSubmission) { - delete mongoose.connection.models.BookingSubmission; -} - -const bookingSubmissionSchema = new mongoose.Schema( - { - // Liên kết với activity và session - activityId: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Activity', - required: true - }, - sessionId: { - type: String, - required: true - }, - - // Thông tin người đăng ký - parentFirstName: { - type: String, - required: true, - trim: true - }, - parentLastName: { - type: String, - required: true, - trim: true - }, - email: { - type: String, - required: true, - trim: true, - lowercase: true - }, - phone: { - type: String, - required: true, - trim: true - }, - - // Thông tin địa chỉ - address: { - type: String, - required: true, - trim: true - }, - city: { - type: String, - required: true, - trim: true - }, - country: { - type: String, - required: true, - trim: true - }, - postalCode: { - type: String, - required: true, - trim: true - }, - - // Thông tin người tham gia - participantFirstName: { - type: String, - required: true, - trim: true - }, - participantLastName: { - type: String, - required: true, - trim: true - }, - participantBirthDate: { - type: Date, - required: true - }, - participantGender: { - type: String, - enum: ['male', 'female', 'other'], - required: true - }, - numberOfParticipants: { - type: Number, - required: true, - min: 1 - }, - - // Thông tin y tế và đặc biệt - medicalConditions: { - type: String, - trim: true, - default: '' - }, - dietaryRestrictions: { - type: String, - enum: ['none', 'vegetarian', 'vegan', 'halal', 'kosher', 'gluten-free', 'other'], - default: 'none' - }, - specialRequests: { - type: String, - trim: true, - default: '' - }, - - // Thông tin liên hệ khẩn cấp - emergencyContact: { - type: String, - required: true, - trim: true - }, - emergencyPhone: { - type: String, - required: true, - trim: true - }, - - // Điều khoản và thông báo - agreeTerms: { - type: Boolean, - required: true, - default: false - }, - agreeNewsletter: { - type: Boolean, - default: false - }, - - // Trạng thái đăng ký - status: { - type: String, - enum: ['pending', 'confirmed', 'cancelled', 'completed'], - default: 'pending' - }, - - // Ghi chú admin - adminNotes: { - type: String, - trim: true, - default: '' - }, - - // Thông tin thanh toán - paymentStatus: { - type: String, - enum: ['pending', 'partial', 'paid', 'refunded'], - default: 'pending' - }, - totalAmount: { - type: Number, - default: 0 - }, - paidAmount: { - type: Number, - default: 0 - } - }, - { - timestamps: true, - toJSON: { virtuals: true }, - toObject: { virtuals: true } - } -); - -// Virtual để tính tuổi của participant -bookingSubmissionSchema.virtual('participantAge').get(function() { - if (this.participantBirthDate) { - const today = new Date(); - const birthDate = new Date(this.participantBirthDate); - let age = today.getFullYear() - birthDate.getFullYear(); - const monthDiff = today.getMonth() - birthDate.getMonth(); - if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { - age--; - } - return age; - } - return 0; -}); - -// Virtual để lấy thông tin activity -bookingSubmissionSchema.virtual('activity', { - ref: 'Activity', - localField: 'activityId', - foreignField: '_id', - justOne: true -}); - -// Index for better performance -bookingSubmissionSchema.index({ activityId: 1, sessionId: 1 }); -bookingSubmissionSchema.index({ email: 1 }); -bookingSubmissionSchema.index({ status: 1 }); -bookingSubmissionSchema.index({ createdAt: -1 }); - -module.exports = mongoose.model("BookingSubmission", bookingSubmissionSchema); \ No newline at end of file diff --git a/models/footer.js b/models/footer.js index ade280c..080cffb 100644 --- a/models/footer.js +++ b/models/footer.js @@ -1,213 +1,102 @@ const mongoose = require("mongoose"); -// Schema cho menu links -const menuLinkSchema = new mongoose.Schema( +const { Schema } = mongoose; + +// Brand +const LogoSchema = new Schema( { - label: { - type: String, - required: true, - trim: true, - }, - href: { - type: String, - required: true, - trim: true, - }, - order: { - type: Number, - required: false, - default: 0, - }, + image: { type: String, default: "" }, + href: { type: String, default: "/" }, }, { _id: false }, ); -// Schema cho social links -const socialLinkSchema = new mongoose.Schema( +const SocialItemSchema = new Schema( { - icon: { - type: String, - required: true, - trim: true, - }, - href: { - type: String, - required: true, - trim: true, - }, + icon: { type: String, default: "" }, + href: { type: String, default: "#" }, }, { _id: false }, ); -// Schema cho phone -const phoneSchema = new mongoose.Schema( +const BrandSchema = new Schema( { - display: { - type: String, - required: false, - trim: true, - default: "", - }, - href: { - type: String, - required: false, - trim: true, - default: "", - }, + logo: { type: LogoSchema, default: () => ({}) }, + description: { type: String, default: "" }, + social: { type: [SocialItemSchema], default: [] }, }, { _id: false }, ); -// Schema cho logo -const logoSchema = new mongoose.Schema( +// Explore +const ExploreLinkSchema = new Schema( { - src: { - type: String, - required: false, - trim: true, - default: "", - }, - alt: { - type: String, - required: false, - trim: true, - default: "", - }, - href: { - type: String, - required: false, - trim: true, - default: "/", - }, + label: { type: String, default: "" }, + href: { type: String, default: "#" }, }, { _id: false }, ); -// Schema cho copyright -const copyrightSchema = new mongoose.Schema( +const ExploreSchema = new Schema( { - text: { - type: String, - required: false, - trim: true, - default: "Copyright©", - }, - brand: { - type: String, - required: false, - trim: true, - default: "", - }, - rights: { - type: String, - required: false, - trim: true, - default: "All Rights Reserved.", - }, + heading: { type: String, default: "" }, + links: { type: [ExploreLinkSchema], default: [] }, }, { _id: false }, ); -// Schema cho top section -const topSchema = new mongoose.Schema( +// Contact +const ContactSchema = new Schema( { - bgImage: { - type: String, - required: false, - trim: true, - default: "", - }, - phone: { - type: phoneSchema, - default: () => ({ display: "", href: "" }), - }, - address: { - type: String, - required: false, - trim: true, - default: "", - }, - logo: { - type: logoSchema, - default: () => ({ src: "", alt: "", href: "/" }), - }, - menuLinks: { - type: [menuLinkSchema], - default: [], - }, - socialLinks: { - type: [socialLinkSchema], - default: [], - }, + heading: { type: String, default: "" }, + address: { type: String, default: "" }, + phone: { type: String, default: "" }, + email: { type: String, default: "" }, }, { _id: false }, ); -// Schema cho bottom section -const bottomSchema = new mongoose.Schema( +// Newsletter +const NewsletterSchema = new Schema( { - copyright: { - type: copyrightSchema, - default: () => ({ text: "Copyright©", brand: "", rights: "All Rights Reserved." }), - }, - menuLinks: { - type: [menuLinkSchema], - default: [], - }, + heading: { type: String, default: "" }, + description: { type: String, default: "" }, + placeholder: { type: String, default: "" }, + buttonText: { type: String, default: "" }, }, { _id: false }, ); -// Main Footer Schema - khớp 100% với footer.json -const footerSchema = new mongoose.Schema( +// Bottom +const BottomLinkSchema = new Schema( { - top: { - type: topSchema, - default: () => ({ - bgImage: "", - phone: { display: "", href: "" }, - address: "", - logo: { src: "", alt: "", href: "/" }, - menuLinks: [], - socialLinks: [], - }), - }, - bottom: { - type: bottomSchema, - default: () => ({ - copyright: { text: "Copyright©", brand: "", rights: "All Rights Reserved." }, - menuLinks: [], - }), - }, + label: { type: String, default: "" }, + href: { type: String, default: "#" }, + }, + { _id: false }, +); + +const BottomSchema = new Schema( + { + copyright: { type: String, default: "" }, + links: { type: [BottomLinkSchema], default: [] }, + }, + { _id: false }, +); + +// Root +const FooterSchema = new Schema( + { + brand: { type: BrandSchema, default: () => ({}) }, + explore: { type: ExploreSchema, default: () => ({}) }, + contact: { type: ContactSchema, default: () => ({}) }, + newsletter: { type: NewsletterSchema, default: () => ({}) }, + bottom: { type: BottomSchema, default: () => ({}) }, }, { timestamps: true, + strict: false, }, ); -// Static method để lấy hoặc tạo footer duy nhất -footerSchema.statics.getSingle = async function () { - let footer = await this.findOne(); - if (!footer) { - footer = await this.create({}); - } - return footer; -}; - -// Migration method để import từ JSON hiện tại -footerSchema.statics.migrateFromJson = async function (jsonData) { - try { - // Xóa tất cả documents hiện có - await this.deleteMany({}); - - // Tạo document mới - const footer = await this.create(jsonData); - console.log("Footer data migrated successfully"); - return footer; - } catch (error) { - console.error("Error migrating footer data:", error); - throw error; - } -}; - -module.exports = mongoose.model("Footer", footerSchema); +module.exports = mongoose.model("Footer", FooterSchema); diff --git a/models/home.js b/models/home.js index 09d3d52..ea50646 100644 --- a/models/home.js +++ b/models/home.js @@ -3,49 +3,41 @@ const mongoose = require("mongoose"); const { Schema } = mongoose; // Reusable small schemas -const LinkSchema = new Schema( +const FloatingBadgeSchema = new Schema( { + icon: { type: String, default: "" }, + value: { type: String, default: "" }, label: { type: String, default: "" }, - href: { type: String, default: "" }, - }, - { _id: false }, -); - -// Hero slide (for multiple hero items in slider) -const HeroSlideSchema = new Schema( - { - title: { type: String, default: "" }, - subtitle: { type: String, default: "" }, - description: { type: String, default: "" }, - primaryButton: { type: LinkSchema, default: () => ({}) }, - secondaryButton: { type: LinkSchema, default: () => ({}) }, - heroImage: { type: String, default: "" }, - videoUrl: { type: String, default: "" }, }, { _id: false }, ); const HeroSchema = new Schema( { - // Background for whole hero section - backgroundImage: { type: String, default: "" }, - - // Multiple slides - slides: { type: [HeroSlideSchema], default: [] }, - - // Legacy single-slide fields (backward compatible) + badge: { type: String, default: "" }, title: { type: String, default: "" }, - subtitle: { type: String, default: "" }, description: { type: String, default: "" }, - primaryButton: { type: LinkSchema, default: () => ({}) }, - secondaryButton: { type: LinkSchema, default: () => ({}) }, - heroImage: { type: String, default: "" }, - videoUrl: { type: String, default: "" }, + searchPlaceholder: { type: String, default: "" }, + buttonLabel: { type: String, default: "" }, + image: { type: String, default: "" }, + imageAlt: { type: String, default: "" }, + floatingBadge: { type: FloatingBadgeSchema, default: () => ({}) }, }, { _id: false }, ); -const WhyChooseUsItemSchema = new Schema( +const QuickLinkItemSchema = new Schema( + { + icon: { type: String, default: "" }, + title: { type: String, default: "" }, + description: { type: String, default: "" }, + linkText: { type: String, default: "" }, + href: { type: String, default: "" }, + }, + { _id: false }, +); + +const ValuePropFeatureSchema = new Schema( { icon: { type: String, default: "" }, title: { type: String, default: "" }, @@ -54,218 +46,68 @@ const WhyChooseUsItemSchema = new Schema( { _id: false }, ); -const WhyChooseUsSchema = new Schema( - { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, - description: { type: String, default: "" }, - highlightWord: { type: String, default: "" }, - mainImage: { type: String, default: "" }, - secondaryImage: { type: String, default: "" }, - items: { type: [WhyChooseUsItemSchema], default: [] }, - features: { type: [String], default: [] }, - ctaButton: { type: LinkSchema, default: () => ({}) }, - }, - { _id: false }, -); - -const VisaSolutionItemSchema = new Schema( - { - number: { type: String, default: "" }, - title: { type: String, default: "" }, - description: { type: String, default: "" }, - link: { type: String, default: "" }, - }, - { _id: false }, -); - -const VisaSolutionsSchema = new Schema( - { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, - items: { type: [VisaSolutionItemSchema], default: [] }, - }, - { _id: false }, -); - -const VisaCountrySchema = new Schema( - { - name: { type: String, default: "" }, - code: { type: String, default: "" }, - flag: { type: String, default: "" }, - link: { type: String, default: "" }, - visaTypes: { type: [String], default: [] }, - }, - { _id: false }, -); - -const VisaCountriesSchema = new Schema( - { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, - description: { type: String, default: "" }, - countries: { type: [VisaCountrySchema], default: [] }, - ctaButton: { type: LinkSchema, default: () => ({}) }, - }, - { _id: false }, -); - -const TestimonialSchema = new Schema( - { - name: { type: String, default: "" }, - role: { type: String, default: "" }, - country: { type: String, default: "" }, - rating: { type: Number, default: 5 }, - comment: { type: String, default: "" }, - avatar: { type: String, default: "" }, - }, - { _id: false }, -); - -const TestimonialsSchema = new Schema( - { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, - videoUrl: { type: String, default: "" }, - videoThumbnail: { type: String, default: "" }, - items: { type: [TestimonialSchema], default: [] }, - }, - { _id: false }, -); - -const VideoGallerySchema = new Schema( - { - heading: { type: String, default: "" }, - videoUrl: { type: String, default: "" }, - thumbnail: { type: String, default: "" }, - }, - { _id: false }, -); - -const FaqItemSchema = new Schema( - { - question: { type: String, default: "" }, - answer: { type: String, default: "" }, - }, - { _id: false }, -); - -const FaqSchema = new Schema( - { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, - description: { type: String, default: "" }, - ctaButton: { type: LinkSchema, default: () => ({}) }, - items: { type: [FaqItemSchema], default: [] }, - }, - { _id: false }, -); - -const AchievementItemSchema = new Schema( +const ValuePropStatSchema = new Schema( { value: { type: String, default: "" }, - suffix: { type: String, default: "" }, label: { type: String, default: "" }, - description: { type: String, default: "" }, + image: { type: String, default: "" }, + imageAlt: { type: String, default: "" }, }, { _id: false }, ); -const AchievementsSchema = new Schema( - { - heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, - items: { type: [AchievementItemSchema], default: [] }, - }, - { _id: false }, -); - -const VisaConsultancyItemSchema = new Schema( - { - name: { type: String, default: "" }, - icon: { type: String, default: "" }, - year: { type: String, default: "" }, - }, - { _id: false }, -); - -const VisaConsultancySchema = new Schema( - { - items: { type: [VisaConsultancyItemSchema], default: [] }, - }, - { _id: false }, -); - -const BrandItemSchema = new Schema( - { - logo: { type: String, default: "" }, - }, - { _id: false }, -); - -const BrandsSchema = new Schema( - { - items: { type: [BrandItemSchema], default: [] }, - }, - { _id: false }, -); - -const PartnersSchema = new Schema( - { - visaConsultancy: { type: VisaConsultancySchema, default: () => ({}) }, - brands: { type: BrandsSchema, default: () => ({}) }, - }, - { _id: false }, -); - -const BlogPreviewItemSchema = new Schema( +const ValuePropSchema = new Schema( { + badge: { type: String, default: "" }, title: { type: String, default: "" }, - excerpt: { type: String, default: "" }, - category: { type: String, default: "" }, - date: { type: String, default: "" }, // keep as string for easy JSON compatibility (e.g. "2025-08-20") - author: { - name: { type: String, default: "" }, - avatar: { type: String, default: "" }, - }, - comments: { type: Number, default: 0 }, - link: { type: String, default: "" }, - thumbnail: { type: String, default: "" }, + description: { type: String, default: "" }, + features: { type: [ValuePropFeatureSchema], default: [] }, + stats: { type: [ValuePropStatSchema], default: [] }, }, { _id: false }, ); -const BlogPreviewSchema = new Schema( +const ProgramItemSchema = new Schema( + { + category: { type: String, default: "" }, + image: { type: String, default: "" }, + duration: { type: String, default: "" }, + rating: { type: String, default: "" }, + title: { type: String, default: "" }, + description: { type: String, default: "" }, + studentCount: { type: String, default: "" }, + href: { type: String, default: "" }, + }, + { _id: false }, +); + +const ProgramsSchema = new Schema( { heading: { type: String, default: "" }, - subheading: { type: String, default: "" }, - ctaButton: { type: LinkSchema, default: () => ({}) }, - items: { type: [BlogPreviewItemSchema], default: [] }, - selectedBlogIds: [{ type: Schema.Types.ObjectId, ref: 'Blog' }], + description: { type: String, default: "" }, + items: { type: [ProgramItemSchema], default: [] }, + }, + { _id: false }, +); + +const RequestInfoSchema = new Schema( + { + heading: { type: String, default: "" }, + description: { type: String, default: "" }, + phone: { type: String, default: "" }, + email: { type: String, default: "" }, + programs: { type: [String], default: [] }, }, { _id: false }, ); -/** - * Home page content model - * - * NOTE: - * - This schema is based on `hailearning.edu.vn/app/home.json`. - * - `strict: false` keeps backward compatibility with any existing CMS-only sections - * (e.g. about/missionVision/programs/newsletter/latestPosts...) that the admin UI might still send. - */ const HomeSchema = new Schema( { hero: { type: HeroSchema, default: () => ({}) }, - whyChooseUs: { type: WhyChooseUsSchema, default: () => ({}) }, - visaSolutions: { type: VisaSolutionsSchema, default: () => ({}) }, - visaCountries: { type: VisaCountriesSchema, default: () => ({}) }, - testimonials: { type: TestimonialsSchema, default: () => ({}) }, - videoGallery: { type: VideoGallerySchema, default: () => ({}) }, - faq: { type: FaqSchema, default: () => ({}) }, - achievements: { type: AchievementsSchema, default: () => ({}) }, - partners: { type: PartnersSchema, default: () => ({}) }, - blogPreview: { type: BlogPreviewSchema, default: () => ({}) }, + quickLinks: { type: [QuickLinkItemSchema], default: [] }, + valueProp: { type: ValuePropSchema, default: () => ({}) }, + programs: { type: ProgramsSchema, default: () => ({}) }, + requestInfo: { type: RequestInfoSchema, default: () => ({}) }, }, { timestamps: true, @@ -274,4 +116,3 @@ const HomeSchema = new Schema( ); module.exports = mongoose.model("Home", HomeSchema); - diff --git a/models/pricing.js b/models/pricing.js deleted file mode 100644 index 4f9e931..0000000 --- a/models/pricing.js +++ /dev/null @@ -1,328 +0,0 @@ -const mongoose = require("mongoose"); - -// Clear cache -if (mongoose.models.Pricing) { - delete mongoose.models.Pricing; -} -if (mongoose.connection.models.Pricing) { - delete mongoose.connection.models.Pricing; -} - -// Schema for breadcrumb item -const breadcrumbItemSchema = new mongoose.Schema( - { - text: { - type: String, - trim: true, - default: "", - }, - link: { - type: String, - trim: true, - default: "", - }, - }, - { _id: false } -); - -// Schema for hero section -const heroSchema = new mongoose.Schema( - { - title: { - type: String, - trim: true, - default: "Pricing Plan", - }, - backgroundImage: { - type: String, - trim: true, - default: "/assets/img/inner-page/breadcrumb.jpg", - }, - shapeImage: { - type: String, - trim: true, - default: "/assets/img/inner-page/shape.png", - }, - breadcrumb: { - type: [breadcrumbItemSchema], - default: [], - }, - }, - { _id: false } -); - -// Schema for pricing section header -const pricingSectionSchema = new mongoose.Schema( - { - subtitle: { - type: String, - trim: true, - default: "pricing plan", - }, - heading: { - type: String, - trim: true, - default: "Flexible Plans to Suit Every Traveler", - }, - description: { - type: String, - trim: true, - default: "", - }, - }, - { _id: false } -); - -// Schema for individual plan -const planSchema = new mongoose.Schema( - { - name: { - type: String, - trim: true, - required: true, - }, - price: { - type: String, - trim: true, - default: "0", - }, - period: { - type: String, - trim: true, - default: "mo", - }, - currency: { - type: String, - trim: true, - default: "$", - }, - buttonText: { - type: String, - trim: true, - default: "Get Started Today", - }, - buttonLink: { - type: String, - trim: true, - default: "/pricing", - }, - buttonIcon: { - type: String, - trim: true, - default: "fa-solid fa-arrow-right", - }, - style: { - type: String, - trim: true, - enum: ["default", "style-2"], - default: "default", - }, - features: { - type: [String], - default: [], - }, - }, - { _id: false } -); - -// Schema for plans container -const plansSchema = new mongoose.Schema( - { - monthly: { - type: [planSchema], - default: [], - }, - yearly: { - type: [planSchema], - default: [], - }, - }, - { _id: false } -); - -// Schema for testimonial item -const testimonialItemSchema = new mongoose.Schema( - { - name: { - type: String, - trim: true, - default: "", - }, - role: { - type: String, - trim: true, - default: "", - }, - rating: { - type: Number, - min: 1, - max: 5, - default: 5, - }, - content: { - type: String, - trim: true, - default: "", - }, - }, - { _id: false } -); - -// Schema for testimonials section -const testimonialsSchema = new mongoose.Schema( - { - subtitle: { - type: String, - trim: true, - default: "What Our Clients Say", - }, - heading: { - type: String, - trim: true, - default: "Immigration Success Stories", - }, - buttonText: { - type: String, - trim: true, - default: "View All Review", - }, - buttonLink: { - type: String, - trim: true, - default: "/contact", - }, - buttonIcon: { - type: String, - trim: true, - default: "fa-solid fa-arrow-right", - }, - image: { - type: String, - trim: true, - default: "", - }, - items: { - type: [testimonialItemSchema], - default: [], - }, - }, - { _id: false } -); - -// Main Pricing Schema -const pricingSchema = new mongoose.Schema( - { - name: { - type: String, - default: "default", - unique: true, - }, - hero: { - type: heroSchema, - default: () => ({}), - }, - pricingSection: { - type: pricingSectionSchema, - default: () => ({}), - }, - plans: { - type: plansSchema, - default: () => ({}), - }, - testimonials: { - type: testimonialsSchema, - default: () => ({}), - }, - }, - { - timestamps: true, - } -); - -// Migration method to import data from JSON -pricingSchema.statics.migrateFromJson = async function (jsonData) { - try { - // Check if default pricing exists - const existingPricing = await this.findOne({ name: "default" }); - - // Process data from JSON - const processedData = { - hero: { - title: jsonData.hero?.title || "Pricing Plan", - backgroundImage: jsonData.hero?.backgroundImage || "/assets/img/inner-page/breadcrumb.jpg", - shapeImage: jsonData.hero?.shapeImage || "/assets/img/inner-page/shape.png", - breadcrumb: (jsonData.hero?.breadcrumb || []).map((item) => ({ - text: item.text || "", - link: item.link || "", - })), - }, - pricingSection: { - subtitle: jsonData.pricingSection?.subtitle || "pricing plan", - heading: jsonData.pricingSection?.heading || "Flexible Plans to Suit Every Traveler", - description: jsonData.pricingSection?.description || "", - }, - plans: { - monthly: (jsonData.plans?.monthly || []).map((plan) => ({ - name: plan.name || "", - price: plan.price || "0", - period: plan.period || "mo", - currency: plan.currency || "$", - buttonText: plan.buttonText || "Get Started Today", - buttonLink: plan.buttonLink || "/pricing", - buttonIcon: plan.buttonIcon || "fa-solid fa-arrow-right", - style: plan.style || "default", - features: plan.features || [], - })), - yearly: (jsonData.plans?.yearly || []).map((plan) => ({ - name: plan.name || "", - price: plan.price || "0", - period: plan.period || "mo", - currency: plan.currency || "$", - buttonText: plan.buttonText || "Get Started Today", - buttonLink: plan.buttonLink || "/pricing", - buttonIcon: plan.buttonIcon || "fa-solid fa-arrow-right", - style: plan.style || "default", - features: plan.features || [], - })), - }, - testimonials: { - subtitle: jsonData.testimonials?.subtitle || "What Our Clients Say", - heading: jsonData.testimonials?.heading || "Immigration Success Stories", - buttonText: jsonData.testimonials?.buttonText || "View All Review", - buttonLink: jsonData.testimonials?.buttonLink || "/contact", - buttonIcon: jsonData.testimonials?.buttonIcon || "fa-solid fa-arrow-right", - image: jsonData.testimonials?.image || "", - items: (jsonData.testimonials?.items || []).map((item) => ({ - name: item.name || "", - role: item.role || "", - rating: item.rating || 5, - content: item.content || "", - })), - }, - }; - - if (existingPricing) { - // Update existing pricing - existingPricing.hero = processedData.hero; - existingPricing.pricingSection = processedData.pricingSection; - existingPricing.plans = processedData.plans; - existingPricing.testimonials = processedData.testimonials; - await existingPricing.save(); - console.log("Pricing data updated successfully"); - return existingPricing; - } else { - // Create new pricing - const newPricing = await this.create({ - name: "default", - ...processedData, - }); - console.log("Pricing data imported successfully"); - return newPricing; - } - } catch (error) { - console.error("Error migrating pricing data:", error); - throw error; - } -}; - -module.exports = mongoose.model("Pricing", pricingSchema); diff --git a/models/service.js b/models/service.js deleted file mode 100644 index a1d0f2e..0000000 --- a/models/service.js +++ /dev/null @@ -1,118 +0,0 @@ -const mongoose = require("mongoose"); - -// Define sub-schemas first -const authorSchema = new mongoose.Schema( - { - name: String, - type: String, - }, - { _id: false }, -); - -const clientReviewSchema = new mongoose.Schema( - { - id: String, - rating: Number, - content: String, - author: authorSchema, - icon: String, - }, - { _id: false }, -); - -const featureSchema = new mongoose.Schema( - { - title: String, - description: String, - }, - { _id: false }, -); - -const faqSchema = new mongoose.Schema( - { - id: String, - question: String, - answer: String, - isExpanded: { type: Boolean, default: false }, - }, - { _id: false }, -); - -const serviceDetailsSchema = new mongoose.Schema( - { - title: String, - description: String, - mainImage: String, - overviewTitle: String, - overviewDescription: String, - additionalDescription: String, - keyFeaturesTitle: String, - keyFeaturesImage: String, - features: [featureSchema], - faqTitle: String, - faqImage: String, - faq: [faqSchema], - }, - { _id: false }, -); - -// Main service page schema -const serviceSchema = new mongoose.Schema( - { - pageTitle: String, - - // Main services section - services: { - title: { - subTitle: String, - mainTitle: String, - }, - items: [ - { - slug: String, - name: String, - description: String, - image: String, - layout: String, - details: serviceDetailsSchema, - }, - ], - }, - - // Destination countries section - destinations: { - backgroundImage: String, - title: { - subTitle: String, - mainTitle: String, - }, - }, - - // Visa types section - visas: { - items: [ - { - id: String, - number: String, - name: String, - description: String, - buttonText: String, - buttonLink: String, - }, - ], - }, - - // Client reviews section - reviews: { - title: { - subTitle: String, - mainTitle: String, - }, - thumb: String, - items: [clientReviewSchema], - }, - }, - { timestamps: true }, -); - -module.exports = mongoose.model("Service", serviceSchema); diff --git a/models/visa.js b/models/visa.js deleted file mode 100644 index b0589d1..0000000 --- a/models/visa.js +++ /dev/null @@ -1,234 +0,0 @@ -// models/visa.js - -const mongoose = require("mongoose"); - -// ==================== SCHEMAS ==================== - -// VisaItem Schema -const VisaItemSchema = new mongoose.Schema( - { - title: { type: String, default: "" }, - description: { type: String, default: "" }, - }, - { _id: false }, -); - -// VisaTypeCategory Schema -const VisaTypeCategorySchema = new mongoose.Schema( - { - category: { type: String, default: "" }, - items: [VisaItemSchema], - }, - { _id: false }, -); - -// VisaProcessStep Schema -const VisaProcessStepSchema = new mongoose.Schema( - { - number: { type: String, default: "" }, - title: { type: String, default: "" }, - description: { type: String, default: "" }, - }, - { _id: false }, -); - -// VisaProcess Schema -const VisaProcessSchema = new mongoose.Schema( - { - title: { type: String, default: "" }, - steps: [VisaProcessStepSchema], - }, - { _id: false }, -); - -// VisaCategory Schema -const VisaCategorySchema = new mongoose.Schema( - { - title: { type: String, default: "" }, - steps: { - type: [[String]], - default: [], - }, - }, - { _id: false }, -); - -// VisaService Schema -const VisaServiceSchema = new mongoose.Schema( - { - title: { type: String, default: "" }, - steps: [VisaProcessStepSchema], - }, - { _id: false }, -); - -// RelatedCountry Schema -const RelatedCountrySchema = new mongoose.Schema( - { - id: { type: Number, default: 0 }, - name: { type: String, default: "" }, - icon: { type: String, default: "" }, - }, - { _id: false }, -); - -// Phone Schema -const PhoneSchema = new mongoose.Schema( - { - label: { type: String, default: "" }, - value: { type: String, default: "" }, - link: { type: String, default: "" }, - }, - { _id: false }, -); - -// Email Schema -const EmailSchema = new mongoose.Schema( - { - label: { type: String, default: "" }, - value: { type: String, default: "" }, - link: { type: String, default: "" }, - }, - { _id: false }, -); - -// Location Schema -const LocationSchema = new mongoose.Schema( - { - label: { type: String, default: "" }, - address: { type: String, default: "" }, - }, - { _id: false }, -); - -// ContactInfo Schema -const ContactInfoSchema = new mongoose.Schema( - { - img: { type: String, default: "" }, - sectionTitle: { type: String, default: "" }, - helpText: { type: String, default: "" }, - - phone: { - type: PhoneSchema, - default: () => ({}), - }, - email: { - type: EmailSchema, - default: () => ({}), - }, - location: { - type: LocationSchema, - default: () => ({}), - }, - }, - { _id: false }, -); - -// ActiveCountry Schema -const ActiveCountrySchema = new mongoose.Schema( - { - id: { type: Number, default: 0 }, - name: { type: String, default: "" }, - title: { type: String, default: "" }, - mainImage: { type: String, default: "" }, - description: { type: String, default: "" }, - additionalInfo: { type: String, default: "" }, - tagline: { type: String, default: "" }, - visaTypes: [VisaTypeCategorySchema], - visaProcess: { - type: VisaProcessSchema, - default: null, - }, - gallery: { - type: [String], - default: [], - }, - visaCategories: { - type: VisaCategorySchema, - default: null, - }, - visaService: { - type: VisaServiceSchema, - default: null, - }, - }, - { _id: false }, -); - -// DetailedView Schema -const DetailedViewSchema = new mongoose.Schema( - { - activeCountry: { - type: ActiveCountrySchema, - default: null, - }, - relatedCountries: { - type: [RelatedCountrySchema], - default: [], - }, - contactInfo: { - type: ContactInfoSchema, - default: null, - }, - }, - { _id: false }, -); - -// ==================== MAIN VISA COUNTRY SCHEMA ==================== - -// Main VisaCountry Schema (Individual country object) -const VisaCountrySchema = new mongoose.Schema( - { - // Không dùng `index: true` ở đây vì đã tạo index riêng cho hero.summaryList.* bên dưới - id: { type: Number, required: true }, - name: { type: String, required: true }, - slug: { type: String, required: true }, - icon: { type: String, default: "" }, - services: { - type: [String], - default: [], - }, - detailedView: { - type: DetailedViewSchema, - default: null, - }, - }, - { _id: false }, -); - -// ==================== HERO SCHEMA ==================== - -const HeroSchema = new mongoose.Schema( - { - title: { type: String, default: "Visa" }, - summaryList: { - type: [VisaCountrySchema], - default: [], - }, - }, - { _id: false }, -); - -// ==================== MAIN VISA SCHEMA ==================== - -const visaDataSchema = new mongoose.Schema( - { - hero: { - type: HeroSchema, - default: () => ({ title: "Visa", summaryList: [] }), - }, - }, - { - timestamps: true, - }, -); - -// ==================== INDEXES ==================== - -visaDataSchema.index({ "hero.summaryList.slug": 1 }); -visaDataSchema.index({ "hero.summaryList.id": 1 }); -visaDataSchema.index({ "hero.summaryList.name": 1 }); - -// ==================== MODEL ==================== - -module.exports = mongoose.models.Visa || mongoose.model("Visa", visaDataSchema); diff --git a/public/assets/img/logo/logo.jpg b/public/assets/img/logo/logo.jpg new file mode 100644 index 0000000..23babee Binary files /dev/null and b/public/assets/img/logo/logo.jpg differ diff --git a/public/uploads/about/favicon.png b/public/uploads/about/favicon.png new file mode 100644 index 0000000..0291452 Binary files /dev/null and b/public/uploads/about/favicon.png differ diff --git a/public/uploads/home/4cf23b331d-c5b1b59a6d86d09a9d0c.png b/public/uploads/home/4cf23b331d-c5b1b59a6d86d09a9d0c.png new file mode 100644 index 0000000..c5defe6 Binary files /dev/null and b/public/uploads/home/4cf23b331d-c5b1b59a6d86d09a9d0c.png differ diff --git a/public/uploads/home/z7667839116159_14f8daaf3444c347fa9165dbaacb2e69.jpg b/public/uploads/home/z7667839116159_14f8daaf3444c347fa9165dbaacb2e69.jpg new file mode 100644 index 0000000..c9e559f Binary files /dev/null and b/public/uploads/home/z7667839116159_14f8daaf3444c347fa9165dbaacb2e69.jpg differ diff --git a/public/uploads/home/z7667839116159_14f8daaf3444c347fa9165dbaacb2e69.webp b/public/uploads/home/z7667839116159_14f8daaf3444c347fa9165dbaacb2e69.webp new file mode 100644 index 0000000..6f3f573 Binary files /dev/null and b/public/uploads/home/z7667839116159_14f8daaf3444c347fa9165dbaacb2e69.webp differ diff --git a/public/uploads/student-support/z7667839116159_14f8daaf3444c347fa9165dbaacb2e69.jpg b/public/uploads/student-support/z7667839116159_14f8daaf3444c347fa9165dbaacb2e69.jpg new file mode 100644 index 0000000..c9e559f Binary files /dev/null and b/public/uploads/student-support/z7667839116159_14f8daaf3444c347fa9165dbaacb2e69.jpg differ diff --git a/routes/admin.js b/routes/admin.js index 95dcaf7..a7026d7 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -3,28 +3,27 @@ const router = express.Router(); const { ensureAuthenticated } = require("../middleware/auth"); const dashboardController = require("../controllers/dashboardController"); const uploadController = require("../controllers/uploadController"); + const homeController = require("../controllers/homeController"); const headerController = require("../controllers/headerController"); const footerController = require("../controllers/footerController"); -const aboutUsController = require("../controllers/aboutUsController"); +const aboutController = require("../controllers/aboutController"); const formController = require("../controllers/formController"); const contactController = require("../controllers/contactController"); const studentSupportController = require("../controllers/studentSupportController"); const requestInfoController = require("../controllers/requestInfoController"); + const pageController = require("../controllers/pageController"); const settingController = require("../controllers/settingController"); const faqController = require("../controllers/faqController"); // Thêm import này const termsController = require("../controllers/termsController"); -const travelController = require("../controllers/travelController"); -const visaController = require("../controllers/visaController"); const { upload, uploadVideo, convertToWebp } = require("../middleware/upload"); const safetyController = require("../controllers/safetyController"); const insuranceController = require("../controllers/insuranceController"); const auditLogController = require("../controllers/auditLogController"); const activityController = require("../controllers/activityController"); -const bookingSubmissionController = require("../controllers/bookingSubmissionController"); -const serviceController = require("../controllers/serviceController"); + const headerMenuController = require("../controllers/headerMenuController"); const programmeController = require("../controllers/programmeController"); @@ -42,7 +41,7 @@ router.get("/dashboard", ensureAuthenticated, dashboardController.getDashboard); // Home router.get("/home", ensureAuthenticated, homeController.index); router.post("/home/update", ensureAuthenticated, homeController.update); -router.get("/home/api/blogs", ensureAuthenticated, homeController.apiGetBlogs); +// router.get("/home/api/blogs", ensureAuthenticated, homeController.apiGetBlogs); // Middleware chuẩn hóa code router.param("code", (req, res, next, code) => { @@ -50,9 +49,9 @@ router.param("code", (req, res, next, code) => { next(); }); -// About Us -router.get("/about-us", ensureAuthenticated, aboutUsController.index); -router.post("/about-us/update", ensureAuthenticated, aboutUsController.update); +// About +router.get("/about", ensureAuthenticated, aboutController.index); +router.post("/about/update", ensureAuthenticated, aboutController.update); // Booking admin CRUD removed @@ -150,7 +149,6 @@ router.post( // Footer routes router.get("/footer", ensureAuthenticated, footerController.index); router.post("/footer/update", ensureAuthenticated, footerController.update); -router.get("/footer/data", ensureAuthenticated, footerController.getFooterData); // Contact routes router.get("/contact", ensureAuthenticated, contactController.index); @@ -197,51 +195,6 @@ router.post( requestInfoController.update, ); -// Appointment management -const appointmentController = require("../controllers/appointmentController"); -router.get( - "/appointments", - ensureAuthenticated, - appointmentController.getAppointments, -); -router.get( - "/appointments/:id", - ensureAuthenticated, - appointmentController.getAppointmentById, -); -router.put( - "/appointments/:id", - ensureAuthenticated, - appointmentController.updateAppointmentStatus, -); -router.delete( - "/appointments/:id", - ensureAuthenticated, - appointmentController.deleteAppointment, -); - -// Appointment CMS page management -router.get("/appointment", ensureAuthenticated, appointmentController.index); -router.post( - "/appointment/update", - ensureAuthenticated, - appointmentController.update, -); -router.get( - "/appointment/data", - ensureAuthenticated, - appointmentController.getAppointmentData, -); - -// Pricing CMS page management -const pricingController = require("../controllers/pricingController"); -router.get("/pricing", ensureAuthenticated, pricingController.index); -router.post("/pricing/update", ensureAuthenticated, pricingController.update); -router.get( - "/pricing/data", - ensureAuthenticated, - pricingController.getPricingData, -); // Activity CRUD routes router.get("/activity", ensureAuthenticated, activityController.index); @@ -312,18 +265,6 @@ router.get( ensureAuthenticated, activityController.exportAllBookingsData, ); -// Update booking submission -router.put( - "/bookings/:bookingId", - ensureAuthenticated, - bookingSubmissionController.updateBookingSubmission, -); -// Delete booking submission -router.delete( - "/bookings/:bookingId", - ensureAuthenticated, - bookingSubmissionController.deleteBookingSubmission, -); // Update filters @@ -361,12 +302,12 @@ router.get("/terms/api", termsController.api); router.get("/terms/seed", ensureAuthenticated, termsController.seed); // Travel routes -router.get("/travel", ensureAuthenticated, travelController.index); -router.post("/travel/update", ensureAuthenticated, travelController.update); -router.post("/travel/preview", ensureAuthenticated, travelController.preview); -router.get("/travel/data", ensureAuthenticated, travelController.getTravelData); -router.get("/travel/api", travelController.api); -router.get("/travel/seed", ensureAuthenticated, travelController.seed); +// router.get("/travel", ensureAuthenticated, travelController.index); +// router.post("/travel/update", ensureAuthenticated, travelController.update); +// router.post("/travel/preview", ensureAuthenticated, travelController.preview); +// router.get("/travel/data", ensureAuthenticated, travelController.getTravelData); +// router.get("/travel/api", travelController.api); +// router.get("/travel/seed", ensureAuthenticated, travelController.seed); // Deprecated FAQ API routes removed @@ -411,30 +352,6 @@ router.post( insuranceController.update, ); -// Service routes -router.get("/service", ensureAuthenticated, serviceController.index); -router.post("/service/update", ensureAuthenticated, serviceController.update); -router.post( - "/service/generate-slug", - ensureAuthenticated, - serviceController.generateSlug, -); -router.get("/service/:slug/edit", ensureAuthenticated, serviceController.edit); -router.post( - "/service/:slug/edit", - ensureAuthenticated, - serviceController.updateService, -); -router.get( - "/service/:slug/details", - ensureAuthenticated, - serviceController.details, -); -router.post( - "/service/:slug/details/update", - ensureAuthenticated, - serviceController.updateDetails, -); // Test Image Paths route router.get("/test-images", ensureAuthenticated, (req, res) => { @@ -499,31 +416,6 @@ router.get("/test-images", ensureAuthenticated, (req, res) => { }); }); -// Display visa management page -router.get("/visa", ensureAuthenticated, visaController.index); - -// Get country data for editing -router.get("/visa/edit/:id", ensureAuthenticated, visaController.getCountry); - -// Update hero title -router.post("/visa/update", ensureAuthenticated, visaController.updateCountry); - -// Add new country -router.post("/visa/add", ensureAuthenticated, visaController.addCountry); - -// Update single country -router.put( - "/visa/update/:id", - ensureAuthenticated, - visaController.updateCountry, -); - -// Delete country -router.delete( - "/visa/delete/:id", - ensureAuthenticated, - visaController.deleteCountry, -); // Programme Management router.get("/programme", ensureAuthenticated, programmeController.index); diff --git a/routes/index.js b/routes/index.js index 529305e..5e9e6b9 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,27 +2,25 @@ const express = require("express"); const path = require("path"); const router = express.Router(); const homeController = require("../controllers/homeController"); -const aboutUsController = require("../controllers/aboutUsController"); +const aboutController = require("../controllers/aboutController"); const headerController = require("../controllers/headerController"); + const socialLinkController = require("../controllers/socialLinkController"); + const footerController = require("../controllers/footerController"); const contactController = require("../controllers/contactController"); const studentSupportController = require("../controllers/studentSupportController"); const requestInfoController = require("../controllers/requestInfoController"); + const faqController = require("../controllers/faqController"); -const visaController = require("../controllers/visaController"); const headerMenuController = require("../controllers/headerMenuController"); const safetyController = require("../controllers/safetyController"); -// Booking flow removed const programmeController = require("../controllers/programmeController"); const insuranceController = require("../controllers/insuranceController"); const termsController = require("../controllers/termsController"); // <-- IMPORT ĐÃ CÓ const activityController = require("../controllers/activityController"); -const travelController = require("../controllers/travelController"); -const bookingSubmissionController = require("../controllers/bookingSubmissionController"); -const serviceController = require("../controllers/serviceController"); // Blog controllers const blogController = require("../controllers/blogController"); const blogCategoryController = require("../controllers/blogCategoryController"); @@ -40,12 +38,7 @@ router.get("/", (req, res) => { router.get("/api/home", homeController.api); // API để lấy dữ liệu about -router.get("/api/about", aboutUsController.getAbout); -router.put("/api/about", aboutUsController.updateAbout); - -// Public about-us page and API (legacy support) -router.get("/about-us", aboutUsController.getAbout); -router.get("/api/about-us", aboutUsController.getAbout); +router.get("/api/about", aboutController.api); // Header API route router.get("/api/header", headerController.api); @@ -76,71 +69,20 @@ router.get("/api/request-info", requestInfoController.api); // Contact form submission (public) router.post("/api/contact/submit", contactController.submitForm); -// Appointment API -const appointmentController = require("../controllers/appointmentController"); -router.get("/api/appointment", appointmentController.api); -router.post("/api/appointment/submit", appointmentController.submitAppointment); - -// Pricing API -const pricingController = require("../controllers/pricingController"); -router.get("/api/pricing", pricingController.api); - router.get("/api/faq", faqController.api); + // Safety API route router.get("/api/safety", safetyController.api); + // Activity API routes router.get("/api/activities", activityController.api); router.get("/api/activities/:id", activityController.apiDetail); + // Insurance APi route router.get("/api/insurance", insuranceController.api); router.get("/api/terms", termsController.api); -// Travel public page and API -router.get("/travel", async (req, res) => { - try { - const Travel = require("../models/travel"); - const travel = await Travel.findOne(); - - if (!travel) { - return res.status(404).render("errors/404", { - title: "Page Not Found", - message: "Travel information not found", - }); - } - - res.render("page/travel", { - title: travel.page.title, - data: travel.toObject(), - }); - } catch (error) { - console.error("Error loading travel page:", error); - res.status(500).render("errors/500", { - title: "Server Error", - message: "Error loading travel page", - }); - } -}); -router.get("/api/travel", travelController.api); - -// Booking submission APIs (public endpoints) -router.post("/api/booking/submit", bookingSubmissionController.submitBooking); -router.get("/api/activity/:activityId/sessions", bookingSubmissionController.getAvailableSessions); -router.get( - "/api/activity/:activityId/session/:sessionId/availability", - bookingSubmissionController.getSessionAvailability, -); - -// Demo booking form -router.get("/demo/booking-form", (req, res) => { - res.sendFile(path.join(__dirname, "../views/demo/booking-form.html")); -}); - -// Demo session booking API -router.get("/demo/session-booking-api", (req, res) => { - res.sendFile(path.join(__dirname, "../views/demo/session-booking-api.html")); -}); - // Blog API Routes router.get("/api/blog", blogController.api); router.get("/api/blog/featured", blogController.apiFeatured); @@ -168,21 +110,6 @@ router.get("/api/blog/:slug", blogController.apiShow); // // API route cho blog detail // router.get('/api/blog-detail', blogDetailController.api); -/* CMS - Hailearning - */ -// service -router.get("/service", serviceController.index); -router.post("/service", serviceController.update); -router.get("/api/service", serviceController.api); - -// Service details by slug -router.get("/api/service/:slug", serviceController.getServiceBySlug); - -// Service slugs list -router.get("/api/service-slugs", serviceController.getServiceSlugs); - -router.get("/api/visa", visaController.api); -router.get("/api/visa/country", visaController.apiCountries); // Programmes API router.get("/api/programmes", programmeController.api); diff --git a/scripts/2025_12_02_114127_contact.js b/scripts/2025_12_02_114127_contact.js deleted file mode 100644 index c24e849..0000000 --- a/scripts/2025_12_02_114127_contact.js +++ /dev/null @@ -1,38 +0,0 @@ -require("dotenv").config(); -const fs = require("fs").promises; -const path = require("path"); -const connectDB = require("../config/database"); -const Contact = require("../models/contact"); -const mongoose = require("mongoose"); - -/** - * Migration: contact - * Migrate contact data from contact-data.json - */ -async function migrate() { - try { - await connectDB(); - - // Read contact-data.json file - const contactJsonPath = path.join(__dirname, "../data/contact.json"); - const contactData = JSON.parse(await fs.readFile(contactJsonPath, "utf8")); - - // Migrate data using the model's static method - await Contact.migrateFromJson(contactData); - - console.log("Contact migration completed successfully"); - - await mongoose.disconnect(); - process.exit(0); - } catch (error) { - console.error("Migration error:", error); - process.exit(1); - } -} - -// Chạy migration nếu được gọi trực tiếp -if (require.main === module) { - migrate(); -} - -module.exports = { migrate }; diff --git a/scripts/2026_02_02_131615_service.js b/scripts/2026_02_02_131615_service.js deleted file mode 100644 index af24f29..0000000 --- a/scripts/2026_02_02_131615_service.js +++ /dev/null @@ -1,186 +0,0 @@ -require("dotenv").config(); -const fs = require("fs").promises; -const path = require("path"); -const mongoose = require("mongoose"); - -const connectDB = require("../config/database"); -const Service = require("../models/service"); - -/** - * Transform service.json data to match Service schema - */ -function transformServiceData(sourceData) { - return { - pageTitle: sourceData.pageTitle || "", - - // Breadcrumb navigation section - breadcrumb: { - title: sourceData?.breadcrumb?.title || "", - backgroundImage: sourceData?.breadcrumb?.backgroundImage || "", - shape: sourceData?.breadcrumb?.shape || "", - items: Array.isArray(sourceData?.breadcrumb?.items) - ? sourceData.breadcrumb.items.map((item) => ({ - label: item.label || "", - href: item.href || "", - })) - : [], - }, - - // Main services section - services: { - title: { - subTitle: sourceData?.services?.title?.subTitle || "", - mainTitle: sourceData?.services?.title?.mainTitle || "", - }, - items: Array.isArray(sourceData?.services?.items) - ? sourceData.services.items.map((service) => ({ - slug: service.slug || "", - name: service.name || "", - description: service.description || "", - image: service.image || "", - layout: service.layout || "", - details: { - title: service.details?.title || "", - description: service.details?.description || "", - mainImage: service.details?.mainImage || "", - overviewTitle: service.details?.overviewTitle || "", - overviewDescription: service.details?.overviewDescription || "", - additionalDescription: - service.details?.additionalDescription || "", - keyFeaturesTitle: service.details?.keyFeaturesTitle || "", - keyFeaturesImage: service.details?.keyFeaturesImage || "", - features: Array.isArray(service.details?.features) - ? service.details.features.map((feature) => ({ - icon: feature.icon || "", - title: feature.title || "", - description: feature.description || "", - })) - : [], - faqTitle: service.details?.faqTitle || "", - faqImage: service.details?.faqImage || "", - faq: Array.isArray(service.details?.faq) - ? service.details.faq.map((faqItem) => ({ - id: faqItem.id || "", - question: faqItem.question || "", - answer: faqItem.answer || "", - isExpanded: faqItem.isExpanded || false, - })) - : [], - }, - })) - : [], - }, - - // Destination countries section - destinations: { - backgroundImage: sourceData?.destinations?.backgroundImage || "", - title: { - subTitle: sourceData?.destinations?.title?.subTitle || "", - mainTitle: sourceData?.destinations?.title?.mainTitle || "", - }, - items: Array.isArray(sourceData?.destinations?.items) - ? sourceData.destinations.items.map((country) => ({ - id: country.id || "", - name: country.name || "", - description: country.description || "", - image: country.image || "", - icon: country.icon || "", - link: country.link || "", - })) - : [], - }, - - // Visa types section - visas: { - items: Array.isArray(sourceData?.visas?.items) - ? sourceData.visas.items.map((visa) => ({ - id: visa.id || "", - number: visa.number || "", - name: visa.name || "", - description: visa.description || "", - buttonText: visa.buttonText || "", - buttonLink: visa.buttonLink || "", - })) - : [], - }, - - // Client reviews section - reviews: { - title: { - subTitle: sourceData?.reviews?.title?.subTitle || "", - mainTitle: sourceData?.reviews?.title?.mainTitle || "", - }, - viewAllButton: { - text: sourceData?.reviews?.viewAllButton?.text || "", - icon: sourceData?.reviews?.viewAllButton?.icon || "", - link: sourceData?.reviews?.viewAllButton?.link || "", - }, - thumb: sourceData?.reviews?.thumb || "", - items: Array.isArray(sourceData?.reviews?.items) - ? sourceData.reviews.items.map((review) => ({ - id: review.id || "", - rating: review.rating || 5, - content: review.content || "", - author: { - name: review.author?.name || "", - type: review.author?.type || "", - }, - icon: review.icon || "", - })) - : [], - navigation: { - prevButton: sourceData?.reviews?.navigation?.prevButton || "", - nextButton: sourceData?.reviews?.navigation?.nextButton || "", - prevIcon: sourceData?.reviews?.navigation?.prevIcon || "", - nextIcon: sourceData?.reviews?.navigation?.nextIcon || "", - }, - }, - - updatedAt: new Date(), - }; -} - -/** - * Migration function for service page data - */ -async function migrateServiceData() { - try { - await connectDB(); - console.log("🚀 Starting service page migration..."); - - // Clear existing service documents - await Service.deleteMany({}); - console.log("🗑️ Cleared existing service documents"); - - // Read service.json file - const serviceJsonPath = path.join(__dirname, "..", "data", "service.json"); - const rawJsonData = await fs.readFile(serviceJsonPath, "utf8"); - const sourceServiceData = JSON.parse(rawJsonData); - - // Transform data to match schema - const transformedServiceData = transformServiceData(sourceServiceData); - - // Create new service document - const newService = new Service(transformedServiceData); - const savedService = await newService.save(); - - console.log("✅ Service page migration completed successfully!"); - console.log(`📄 Service document ID: ${savedService._id}`); - - await mongoose.disconnect(); - process.exit(0); - } catch (error) { - console.error("❌ Service migration error:", error); - process.exit(1); - } -} - -// Run migration if called directly -if (require.main === module) { - migrateServiceData(); -} - -module.exports = { - migrate: migrateServiceData, - transformServiceData, -}; diff --git a/scripts/2026_02_02_170000_blog.js b/scripts/2026_02_02_170000_blog.js deleted file mode 100644 index 35e1f28..0000000 --- a/scripts/2026_02_02_170000_blog.js +++ /dev/null @@ -1,182 +0,0 @@ -require('dotenv').config(); -const fs = require('fs').promises; -const path = require('path'); -const connectDB = require('../config/database'); - -/** - * Migration: create_complete_blog_system - * Created: 17:00:00 2/2/2026 - * Description: Tạo hoàn chỉnh hệ thống blog với categories, tags, posts và comments - */ -async function migrate() { - try { - // Kết nối database - await connectDB(); - console.log('🚀 Starting migration: create_complete_blog_system...'); - - // Import models - 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'); - - console.log('✅ Blog models registered successfully'); - - // Load complete data - const dataPath = path.join(__dirname, '..', 'data', 'blog.json'); - const rawData = await fs.readFile(dataPath, 'utf8'); - const data = JSON.parse(rawData); - - console.log('📖 Complete blog data loaded from JSON'); - - // Clear existing data - console.log('🧹 Clearing existing blog data...'); - await BlogComment.deleteMany({}); - await Blog.deleteMany({}); - await BlogCategory.deleteMany({}); - await BlogTag.deleteMany({}); - await RecentPost.deleteMany({}); - console.log('✅ Existing data cleared'); - - // 1. Create categories - console.log('📝 Creating categories...'); - const createdCategories = []; - for (const categoryData of data.categories) { - const category = new BlogCategory(categoryData); - await category.save(); - createdCategories.push(category); - console.log(`✅ Created category: ${category.name}`); - } - - // 2. Create tags - console.log('📝 Creating tags...'); - const createdTags = []; - for (const tagData of data.tags) { - const tag = new BlogTag(tagData); - await tag.save(); - createdTags.push(tag); - console.log(`✅ Created tag: ${tag.name}`); - } - - // 3. Create blog posts - console.log('📝 Creating blog posts...'); - const createdPosts = []; - for (const postData of data.posts) { - const post = new Blog(postData); - await post.save(); - createdPosts.push(post); - console.log(`✅ Created blog post: ${post.title}`); - } - - // 4. Create comments - console.log('💬 Creating comments...'); - let createdCommentsCount = 0; - - for (const commentData of data.comments) { - // Find the blog post by slug - const blog = await Blog.findOne({ - slug: commentData.postSlug, - status: 'published' - }); - - if (blog) { - const comment = new BlogComment({ - postId: blog._id, - authorName: commentData.authorName, - authorAvatar: commentData.authorAvatar, - content: commentData.content, - createdAt: commentData.createdAt, - status: commentData.status - }); - - await comment.save(); - createdCommentsCount++; - console.log(`✅ Created comment by ${comment.authorName} for: ${blog.title}`); - } else { - console.log(`⚠️ Blog post not found for slug: ${commentData.postSlug}`); - } - } - - // 5. Update category post counts - console.log('📊 Updating category post counts...'); - for (const category of createdCategories) { - await category.updatePostCount(); - console.log(`📊 Category "${category.name}": ${category.postCount} posts`); - } - - // 6. Update tag post counts - console.log('📊 Updating tag post counts...'); - for (const tag of createdTags) { - await tag.updatePostCount(); - console.log(`📊 Tag "${tag.name}": ${tag.postCount} posts`); - } - - // 7. Update comments count in blog posts - console.log('📊 Updating comments count in blog posts...'); - const blogs = await Blog.find({ status: 'published' }); - - for (const blog of blogs) { - const commentsCount = await BlogComment.countDocuments({ - postId: blog._id, - status: 'approved' - }); - - blog.commentsCount = commentsCount; - await blog.save(); - - if (commentsCount > 0) { - console.log(`📊 Updated comments count for "${blog.title}": ${commentsCount} comments`); - } - } - - // 8. Sync recent posts - console.log('🔄 Syncing recent posts...'); - await RecentPost.syncFromBlogs(5); - const recentPostsCount = await RecentPost.countDocuments(); - console.log(`🔄 Synced ${recentPostsCount} recent posts`); - - // Final summary - console.log('\n🎉 Migration create_complete_blog_system completed successfully!'); - console.log('=' .repeat(60)); - console.log('📊 MIGRATION SUMMARY:'); - console.log(` ✅ Categories: ${createdCategories.length}`); - console.log(` ✅ Tags: ${createdTags.length}`); - console.log(` ✅ Blog Posts: ${createdPosts.length}`); - console.log(` ✅ Comments: ${createdCommentsCount}`); - console.log(` ✅ Recent Posts: ${recentPostsCount}`); - - // Statistics - const totalPublishedPosts = await Blog.countDocuments({ status: 'published' }); - const totalFeaturedPosts = await Blog.countDocuments({ status: 'published', isFeatured: true }); - const totalApprovedComments = await BlogComment.countDocuments({ status: 'approved' }); - - console.log('\n📈 SYSTEM STATISTICS:'); - console.log(` 📝 Published Posts: ${totalPublishedPosts}`); - console.log(` ⭐ Featured Posts: ${totalFeaturedPosts}`); - console.log(` 💬 Approved Comments: ${totalApprovedComments}`); - - console.log('\n🌐 ACCESS POINTS:'); - console.log(' 📱 Admin Panel: http://localhost:3001/admin/blog'); - console.log(' 🔗 API Endpoint: http://localhost:3001/api/blog'); - console.log(' 📊 Categories API: http://localhost:3001/api/blog-categories'); - console.log(' 🏷️ Tags API: http://localhost:3001/api/blog-tags'); - - console.log('\n✨ Blog system is now ready for use!'); - console.log('=' .repeat(60)); - - const mongoose = require('mongoose'); - await mongoose.disconnect(); - process.exit(0); - } catch (error) { - console.error('❌ Migration error:', error); - process.exit(1); - } -} - -// Chạy migration nếu được gọi trực tiếp -if (require.main === module) { - migrate(); -} - -module.exports = { migrate }; \ No newline at end of file diff --git a/scripts/2026_02_03_645124_visa.js b/scripts/2026_02_03_645124_visa.js deleted file mode 100644 index d3c031e..0000000 --- a/scripts/2026_02_03_645124_visa.js +++ /dev/null @@ -1,336 +0,0 @@ -// scripts/migrateVisa.js - -require("dotenv").config(); -const fs = require("fs").promises; -const path = require("path"); -const mongoose = require("mongoose"); -const Visa = require("../models/visa"); - -// 1. Đọc file JSON -async function loadVisaData() { - const filePath = path.join(__dirname, "..", "data", "visa.json"); - const raw = await fs.readFile(filePath, "utf8"); - return JSON.parse(raw); -} - -// 2. Hàm Transform: Đổ dữ liệu từ JSON vào đúng Schema -function transformVisa(sourceData) { - // JSON có structure hero.title và hero.summaryList - return { - hero: { - title: sourceData.hero?.title || "Visa", - summaryList: Array.isArray(sourceData.hero?.summaryList) - ? sourceData.hero.summaryList.map((country) => - transformCountry(country), - ) - : [], - }, - updatedAt: new Date(), - }; -} - -// Helper function: Transform individual country -function transformCountry(source) { - return { - id: source.id || 0, - name: source.name || "", - slug: source.slug || "", - icon: source.icon || "", - services: Array.isArray(source.services) ? source.services : [], - detailedView: source.detailedView - ? transformDetailedView(source.detailedView) - : null, - }; -} - -// Helper function: Transform DetailedView -function transformDetailedView(source) { - return { - activeCountry: source.activeCountry - ? transformActiveCountry(source.activeCountry) - : null, - relatedCountries: Array.isArray(source.relatedCountries) - ? source.relatedCountries.map((country) => ({ - id: country.id || 0, - name: country.name || "", - icon: country.icon || "", - })) - : [], - contactInfo: source.contactInfo - ? transformContactInfo(source.contactInfo) - : null, - }; -} - -// Helper function: Transform ActiveCountry -function transformActiveCountry(source) { - return { - id: source.id || 0, - name: source.name || "", - title: source.title || "", - mainImage: source.mainImage || "", - description: source.description || "", - additionalInfo: source.additionalInfo || "", - tagline: source.tagline || "", - visaTypes: Array.isArray(source.visaTypes) - ? source.visaTypes.map((type) => ({ - category: type.category || "", - items: Array.isArray(type.items) - ? type.items.map((item) => ({ - title: item.title || "", - description: item.description || "", - })) - : [], - })) - : [], - visaProcess: source.visaProcess - ? transformVisaProcess(source.visaProcess) - : null, - gallery: Array.isArray(source.gallery) ? source.gallery : [], - visaCategories: source.visaCategories - ? transformVisaCategories(source.visaCategories) - : null, - visaService: source.visaService - ? transformVisaService(source.visaService) - : null, - }; -} - -// Helper function: Transform VisaProcess -function transformVisaProcess(source) { - return { - title: source.title || "", - steps: Array.isArray(source.steps) - ? source.steps.map((step) => ({ - number: step.number || "", - title: step.title || "", - description: step.description || "", - })) - : [], - }; -} - -// Helper function: Transform VisaCategories -function transformVisaCategories(source) { - return { - title: source.title || "", - steps: Array.isArray(source.steps) ? source.steps : [], - }; -} - -// Helper function: Transform VisaService -function transformVisaService(source) { - return { - title: source.title || "", - steps: Array.isArray(source.steps) - ? source.steps.map((step) => ({ - number: step.number || "", - title: step.title || "", - description: step.description || "", - })) - : [], - }; -} - -// Helper function: Transform ContactInfo -function transformContactInfo(source) { - return { - img: source.img || "", - sectionTitle: source.sectionTitle || "Visa & Immigration", - helpText: source.helpText || "Need Help?", - phone: { - label: source.phone?.label || "Call Us", - value: source.phone?.value || "", - link: source.phone?.link || "", - }, - email: { - label: source.email?.label || "Mail Us", - value: source.email?.value || "", - link: source.email?.link || "", - }, - location: { - label: source.location?.label || "Location", - address: source.location?.address || "", - }, - }; -} - -// 3. Validate data before migration -function validateVisaData(visaData) { - const errors = []; - - if (!visaData.hero) { - errors.push("Missing hero section"); - } - - if (!visaData.hero?.title) { - console.warn("⚠️ Hero title is missing, using default 'Visa'"); - } - - if (!Array.isArray(visaData.hero?.summaryList)) { - errors.push("summaryList must be an array"); - } else if (visaData.hero.summaryList.length === 0) { - errors.push("summaryList is empty"); - } else { - // Validate each country - visaData.hero.summaryList.forEach((country, idx) => { - if (!country.name || !country.slug) { - errors.push(`Country at index ${idx}: missing name or slug`); - } - - if (country.detailedView) { - if (!country.detailedView.activeCountry) { - console.warn( - `⚠️ Country "${country.name}" (${idx}): missing activeCountry details`, - ); - } - if (!Array.isArray(country.detailedView.relatedCountries)) { - errors.push( - `Country "${country.name}" (${idx}): relatedCountries must be array`, - ); - } - } - }); - } - - return errors; -} - -// Helper function: Get data summary -function getDataSummary(visaData) { - const summary = { - heroTitle: visaData.hero?.title || "N/A", - totalCountries: visaData.hero?.summaryList?.length || 0, - withDetails: 0, - withoutDetails: 0, - byCountry: [], - }; - - if (visaData.hero?.summaryList) { - visaData.hero.summaryList.forEach((country) => { - const hasDetails = !!country.detailedView?.activeCountry; - const relatedCount = country.detailedView?.relatedCountries?.length || 0; - - if (hasDetails) { - summary.withDetails++; - } else { - summary.withoutDetails++; - } - - summary.byCountry.push({ - name: country.name, - slug: country.slug, - hasDetails, - relatedCountries: relatedCount, - services: country.services?.length || 0, - }); - }); - } - - return summary; -} - -// 4. Chạy Migration -async function migrate() { - try { - // Kết nối DB - console.log("🔗 Connecting to MongoDB..."); - await mongoose.connect(process.env.MONGODB_URI); - console.log("✅ Connected to MongoDB\n"); - - // A. Lấy dữ liệu thô - console.log("📖 Loading visa data from JSON..."); - const rawData = await loadVisaData(); - console.log("✅ JSON data loaded\n"); - - // B. Chuẩn hóa dữ liệu theo Schema - console.log("🔄 Transforming data structure..."); - const visaData = transformVisa(rawData); - console.log("✅ Data transformation completed\n"); - - // C. Validate dữ liệu - console.log("✔️ Validating data structure..."); - const errors = validateVisaData(visaData); - if (errors.length > 0) { - console.error("❌ Validation errors found:"); - errors.forEach((err, idx) => console.error(` ${idx + 1}. ${err}`)); - process.exit(1); - } - console.log("✅ Data validation passed\n"); - - // D. Get summary - const summary = getDataSummary(visaData); - - console.log("📊 Migration Summary:"); - console.log(` Hero Title: "${summary.heroTitle}"`); - console.log(` Total countries: ${summary.totalCountries}`); - console.log(` With details: ${summary.withDetails}`); - console.log(` Without details: ${summary.withoutDetails}`); - console.log(`\n Country Details:`); - - summary.byCountry.forEach((country) => { - const detailBadge = country.hasDetails ? "✅" : "❌"; - const detailText = country.hasDetails - ? `(${country.relatedCountries} related)` - : "(basic only)"; - console.log( - ` ${detailBadge} ${country.name.padEnd(20)} (${country.slug.padEnd( - 12, - )}) - ${country.services} services ${detailText}`, - ); - }); - console.log(""); - - // E. Lưu vào DB (Upsert: Có rồi thì update, chưa có thì tạo) - const existingDoc = await Visa.findOne().sort({ updatedAt: -1 }); - - if (existingDoc) { - console.log("📝 Updating existing Visa document..."); - console.log(` Document ID: ${existingDoc._id}`); - - const updated = await Visa.findByIdAndUpdate( - existingDoc._id, - { $set: visaData }, - { new: true }, - ); - - console.log("✅ Visa document updated successfully"); - console.log(` Updated at: ${updated.updatedAt}`); - } else { - console.log("📝 Creating NEW Visa document..."); - - const newDoc = await Visa.create(visaData); - - console.log("✅ Visa document created successfully"); - console.log(` Document ID: ${newDoc._id}`); - console.log(` Created at: ${newDoc.createdAt}`); - } - - console.log("\n✨ Visa migration completed successfully!"); - } catch (error) { - console.error("\n❌ Migration failed:"); - console.error(` Error: ${error.message}`); - - if (error.name === "ValidationError") { - console.error("\n Validation Errors:"); - Object.keys(error.errors).forEach((field) => { - console.error(` - ${field}: ${error.errors[field].message}`); - }); - } - - if (error.stack) { - console.error("\n📋 Stack trace:"); - console.error(error.stack); - } - - process.exit(1); - } finally { - await mongoose.connection.close(); - console.log("\n🔌 MongoDB connection closed"); - process.exit(0); - } -} - -// Run migration -console.log("🚀 Starting Visa Migration...\n"); -migrate(); diff --git a/scripts/2026_02_03_appointment.js b/scripts/2026_02_03_appointment.js deleted file mode 100644 index edf5315..0000000 --- a/scripts/2026_02_03_appointment.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Migration script for Appointment data - * Imports data from appointment.json to MongoDB - * - * Run: node scripts/2026_02_03_appointment.js - */ - -require("dotenv").config(); -const mongoose = require("mongoose"); -const fs = require("fs"); -const path = require("path"); - -// Connect to MongoDB -const connectDB = async () => { - try { - await mongoose.connect(process.env.MONGODB_URI); - console.log("MongoDB connected successfully"); - } catch (error) { - console.error("MongoDB connection error:", error); - process.exit(1); - } -}; - -const runMigration = async () => { - try { - await connectDB(); - - // Load Appointment model - const Appointment = require("../models/appointment"); - - // Load JSON data - const jsonPath = path.join(__dirname, "../data/appointment.json"); - - if (!fs.existsSync(jsonPath)) { - console.log("appointment.json not found, creating default data..."); - const defaultData = { - 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", - }, - }, - }; - await Appointment.migrateFromJson(defaultData); - } else { - const jsonData = JSON.parse(fs.readFileSync(jsonPath, "utf8")); - console.log("Loaded appointment.json data"); - await Appointment.migrateFromJson(jsonData); - } - - console.log("✅ Appointment migration completed successfully!"); - } catch (error) { - console.error("❌ Migration failed:", error); - } finally { - await mongoose.connection.close(); - console.log("MongoDB connection closed"); - } -}; - -runMigration(); diff --git a/scripts/2026_02_05_120000_footer.js b/scripts/2026_02_05_120000_footer.js deleted file mode 100644 index 5b8111b..0000000 --- a/scripts/2026_02_05_120000_footer.js +++ /dev/null @@ -1,68 +0,0 @@ -const mongoose = require("mongoose"); -const path = require("path"); -const fs = require("fs"); - -// Import model -const Footer = require("../models/footer"); - -/** - * Migration script để import dữ liệu footer từ JSON - */ -async function up() { - try { - console.log("Starting footer migration..."); - - // Đọc dữ liệu từ file JSON - const jsonPath = path.join(__dirname, "../data/footer.json"); - - if (!fs.existsSync(jsonPath)) { - throw new Error("Footer JSON file not found"); - } - - const footerData = JSON.parse(fs.readFileSync(jsonPath, "utf8")); - - // Sử dụng static method từ model để migrate - const result = await Footer.migrateFromJson(footerData); - - console.log("Footer migration completed successfully"); - return result; - } catch (error) { - console.error("Footer migration failed:", error); - throw error; - } -} - -/** - * Rollback migration - */ -async function down() { - try { - console.log("Rolling back footer migration..."); - - // Xóa footer data - await Footer.deleteMany({}); - - console.log("Footer rollback completed"); - } catch (error) { - console.error("Footer rollback failed:", error); - throw error; - } -} - -module.exports = { up, down }; - -// Chạy migration nếu file được gọi trực tiếp -if (require.main === module) { - const connectDB = require("../config/database"); - - connectDB() - .then(() => up()) - .then(() => { - console.log("Migration completed successfully"); - process.exit(0); - }) - .catch((error) => { - console.error("Migration failed:", error); - process.exit(1); - }); -} diff --git a/scripts/2026_02_05_130000_add_footer_menu_order.js b/scripts/2026_02_05_130000_add_footer_menu_order.js deleted file mode 100644 index e2c2f2f..0000000 --- a/scripts/2026_02_05_130000_add_footer_menu_order.js +++ /dev/null @@ -1,90 +0,0 @@ -const mongoose = require("mongoose"); -const Footer = require("../models/footer"); -const footerData = require("../data/footer.json"); - -async function addFooterMenuOrder() { - try { - console.log("=== Adding order field to Footer Menu Links ==="); - - // Connect to database - await mongoose.connect(process.env.MONGODB_URI || "mongodb://localhost:27017/hailearning"); - console.log("✓ Connected to MongoDB"); - - // Get existing footer or create from JSON - let footer = await Footer.findOne(); - - if (!footer) { - console.log("No existing footer found, creating from JSON data..."); - - // Add order to bottom menu links - if (footerData.bottom && footerData.bottom.menuLinks) { - footerData.bottom.menuLinks = footerData.bottom.menuLinks.map((link, index) => ({ - ...link, - order: index + 1, - })); - } - - // Add order to top menu links - if (footerData.top && footerData.top.menuLinks) { - footerData.top.menuLinks = footerData.top.menuLinks.map((link, index) => ({ - ...link, - order: index + 1, - })); - } - - footer = await Footer.create(footerData); - console.log("✓ Footer created with order fields"); - } else { - console.log("Found existing footer, adding order fields..."); - - // Add order to bottom menu links - if (footer.bottom && footer.bottom.menuLinks) { - footer.bottom.menuLinks = footer.bottom.menuLinks.map((link, index) => ({ - label: link.label, - href: link.href, - order: link.order || index + 1, - })); - } - - // Add order to top menu links - if (footer.top && footer.top.menuLinks) { - footer.top.menuLinks = footer.top.menuLinks.map((link, index) => ({ - label: link.label, - href: link.href, - order: link.order || index + 1, - })); - } - - await footer.save(); - console.log("✓ Footer updated with order fields"); - } - - console.log("Bottom Menu Links with order:"); - footer.bottom.menuLinks.forEach((link, index) => { - console.log(` ${index + 1}. ${link.label} (order: ${link.order}) -> ${link.href}`); - }); - - console.log("=== Footer Menu Order Migration Completed ==="); - } catch (error) { - console.error("✗ Migration failed:", error); - throw error; - } finally { - await mongoose.disconnect(); - console.log("✓ Disconnected from MongoDB"); - } -} - -// Run migration if called directly -if (require.main === module) { - addFooterMenuOrder() - .then(() => { - console.log("Migration completed successfully"); - process.exit(0); - }) - .catch((error) => { - console.error("Migration failed:", error); - process.exit(1); - }); -} - -module.exports = addFooterMenuOrder; diff --git a/scripts/2026_02_05_190000_home.js b/scripts/2026_02_05_190000_home.js deleted file mode 100644 index e657e03..0000000 --- a/scripts/2026_02_05_190000_home.js +++ /dev/null @@ -1,47 +0,0 @@ -require("dotenv").config(); -const fs = require("fs").promises; -const path = require("path"); -const connectDB = require("../config/database"); - -/** - * Migration: import_home_content - * Created: 19:00:00 2026-02-05 - * Description: - * Import nội dung trang Home từ file JSON (Next.js) vào MongoDB (model Home). - * Nguồn dữ liệu: hailearning.edu.vn/app/home.json - */ -async function migrate() { - try { - // 1) Connect DB - await connectDB(); - console.log("🚀 Starting migration: import_home_content..."); - - // 2) Load model - const Home = require("../models/home"); - console.log("✅ Home model registered successfully"); - - // 3) Load JSON data - const dataPath = path.join(__dirname, "..", "data", "home.json"); - const raw = await fs.readFile(dataPath, "utf8"); - const homeData = JSON.parse(raw); - console.log("📖 Home data loaded from:", dataPath); - - // 4) Clear existing - console.log("🧹 Clearing existing Home data..."); - await Home.deleteMany({}); - console.log("✅ Existing Home documents cleared"); - - // 5) Insert new document - const created = await Home.create(homeData); - console.log("✅ Home document created with _id:", created._id.toString()); - - console.log("🎉 Migration import_home_content completed successfully."); - process.exit(0); - } catch (err) { - console.error("❌ Migration failed:", err); - process.exit(1); - } -} - -migrate(); - diff --git a/scripts/2026_02_07_migrate_about_news_to_blog.js b/scripts/2026_02_07_migrate_about_news_to_blog.js deleted file mode 100644 index 7cce458..0000000 --- a/scripts/2026_02_07_migrate_about_news_to_blog.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Migration: Convert About News static items to dynamic Blog selection - * Date: 2026-02-07 - * - * This migration: - * 1. Adds selectedBlogIds field to About news section - * 2. Keeps existing items for backward compatibility - * 3. Does NOT delete old data (safe migration) - */ - -const mongoose = require("mongoose"); -require("dotenv").config(); - -const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/SIMS"; - -async function up() { - try { - await mongoose.connect(MONGODB_URI); - console.log("✓ Connected to MongoDB"); - - const AboutUs = mongoose.model("AboutUs", new mongoose.Schema({}, { strict: false })); - - const doc = await AboutUs.findOne(); - - if (!doc) { - console.log("⚠ No About Us document found. Skipping migration."); - return; - } - - // Check if already migrated - if (doc.news && doc.news.selectedBlogIds !== undefined) { - console.log("✓ Migration already applied. Skipping."); - return; - } - - // Add selectedBlogIds field (empty array by default) - if (!doc.news) { - doc.news = {}; - } - - doc.news.selectedBlogIds = []; - - // Keep existing items for backward compatibility - // Admin can manually select blogs after migration - - await doc.save(); - - console.log("✓ Migration completed successfully"); - console.log(" - Added selectedBlogIds field to About news section"); - console.log(" - Existing items preserved for backward compatibility"); - console.log(" - Admin can now select blogs from Blog Management"); - } catch (error) { - console.error("✗ Migration failed:", error); - throw error; - } -} - -async function down() { - try { - await mongoose.connect(MONGODB_URI); - console.log("✓ Connected to MongoDB"); - - const AboutUs = mongoose.model("AboutUs", new mongoose.Schema({}, { strict: false })); - - const doc = await AboutUs.findOne(); - - if (!doc || !doc.news) { - console.log("⚠ No About Us document found. Skipping rollback."); - return; - } - - // Remove selectedBlogIds field - if (doc.news.selectedBlogIds !== undefined) { - delete doc.news.selectedBlogIds; - await doc.save(); - console.log("✓ Rollback completed - selectedBlogIds removed"); - } else { - console.log("✓ Nothing to rollback"); - } - } catch (error) { - console.error("✗ Rollback failed:", error); - throw error; - } -} - -// Run migration -if (require.main === module) { - up() - .then(() => { - console.log("\n✓ Migration script completed"); - process.exit(0); - }) - .catch((error) => { - console.error("\n✗ Migration script failed:", error); - process.exit(1); - }); -} - -module.exports = { up, down }; diff --git a/scripts/create-audit-log.js b/scripts/create-audit-log.js deleted file mode 100644 index 75caf72..0000000 --- a/scripts/create-audit-log.js +++ /dev/null @@ -1,29 +0,0 @@ -require("dotenv").config(); -const mongoose = require("mongoose"); - -async function run() { - try { - await mongoose.connect(process.env.MONGODB_URI); - - console.log("Connected DB"); - - const collections = await mongoose.connection.db - .listCollections({ name: "auditlogs" }) - .toArray(); - - if (collections.length > 0) { - console.log("AuditLog collection already exists"); - process.exit(0); - } - - await mongoose.connection.createCollection("auditlogs"); - console.log("AuditLog collection created"); - - process.exit(0); - } catch (err) { - console.error(err); - process.exit(1); - } -} - -run(); diff --git a/scripts/make-migration.js b/scripts/make-migration.js deleted file mode 100644 index b4eda88..0000000 --- a/scripts/make-migration.js +++ /dev/null @@ -1,86 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -/** - * Tạo migration file mới với format giống Laravel - * Format: YYYY_MM_DD_HHMMSS_migration_name.js - */ -function makeMigration(migrationName) { - if (!migrationName) { - console.error('Error: Migration name is required'); - console.log('\nUsage: node scripts/make-migration.js '); - console.log('Example: node scripts/make-migration.js create_users_table'); - process.exit(1); - } - - // Tạo timestamp theo format Laravel: YYYY_MM_DD_HHMMSS - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - const day = String(now.getDate()).padStart(2, '0'); - const hours = String(now.getHours()).padStart(2, '0'); - const minutes = String(now.getMinutes()).padStart(2, '0'); - const seconds = String(now.getSeconds()).padStart(2, '0'); - - const timestamp = `${year}_${month}_${day}_${hours}${minutes}${seconds}`; - const fileName = `${timestamp}_${migrationName}.js`; - const filePath = path.join(__dirname, fileName); - - // Template migration mẫu - const template = `require('dotenv').config(); -const connectDB = require('../config/database'); - -/** - * Migration: ${migrationName} - * Created: ${now.toLocaleString('vi-VN')} - */ -async function migrate() { - try { - // Kết nối database - await connectDB(); - console.log('Starting migration: ${migrationName}...'); - - // TODO: Thêm code migration của bạn ở đây - - console.log('Migration ${migrationName} completed successfully!'); - - const mongoose = require('mongoose'); - await mongoose.disconnect(); - process.exit(0); - } catch (error) { - console.error('Migration error:', error); - process.exit(1); - } -} - -// Chạy migration nếu được gọi trực tiếp -if (require.main === module) { - migrate(); -} - -module.exports = { migrate }; -`; - - // Kiểm tra file đã tồn tại chưa - if (fs.existsSync(filePath)) { - console.error(`Error: Migration file already exists: ${fileName}`); - process.exit(1); - } - - // Tạo file migration - try { - fs.writeFileSync(filePath, template, 'utf8'); - console.log(`Migration created successfully: ${fileName}`); - console.log(`Path: ${filePath}`); - } catch (error) { - console.error('Error creating migration file:', error.message); - process.exit(1); - } -} - -// Lấy migration name từ command line arguments -const migrationName = process.argv[2]; - -// Chạy hàm tạo migration -makeMigration(migrationName); - diff --git a/scripts/migrate-about.js b/scripts/migrate-about.js new file mode 100644 index 0000000..35add47 --- /dev/null +++ b/scripts/migrate-about.js @@ -0,0 +1,60 @@ +require('dotenv').config(); +const fs = require('fs').promises; +const path = require('path'); +const connectDB = require('../config/database'); +const About = require('../models/about'); + +async function validateAboutData(data) { + if (!data || typeof data !== 'object') { + throw new Error('About data must be a valid object'); + } + + // Tuỳ schema bạn chỉnh lại cho chuẩn hơn + if (!data.title || !data.sections) { + throw new Error('Missing required fields: title or sections'); + } + + if (!Array.isArray(data.sections)) { + throw new Error('sections must be an array'); + } +} + +async function migrateAboutData() { + try { + await connectDB(); + console.log('Đã kết nối MongoDB...'); + + // Xóa dữ liệu cũ + await About.deleteMany({}); + console.log('Đã xóa dữ liệu About cũ'); + + // Đọc file JSON + const aboutData = JSON.parse( + await fs.readFile( + path.join(__dirname, '../data/about.json'), + 'utf8' + ) + ); + + // Validate + await validateAboutData(aboutData); + + // Transform (optional) + const finalData = { + ...aboutData, + updatedAt: new Date() + }; + + // Insert + await About.create(finalData); + + console.log('✓ Migrate About thành công!'); + process.exit(0); + + } catch (error) { + console.error('❌ Lỗi:', error.message); + process.exit(1); + } +} + +migrateAboutData(); \ No newline at end of file diff --git a/scripts/migrate-all.js b/scripts/migrate-all.js index 3f97890..76681a5 100644 --- a/scripts/migrate-all.js +++ b/scripts/migrate-all.js @@ -16,10 +16,9 @@ function discoverMigrations() { // Danh sách các file quản lý migration cần loại trừ const excludeFiles = [ "migrate-all.js", - "migrate-status.js", - "migrate-rollback.js", - "migrate-fresh.js", - "make-migration.js", + "migrate-home.js", + "migrate-about.js", + ]; const migrations = files diff --git a/scripts/migrate-footer.js b/scripts/migrate-footer.js new file mode 100644 index 0000000..4f376de --- /dev/null +++ b/scripts/migrate-footer.js @@ -0,0 +1,58 @@ +require('dotenv').config(); +const fs = require('fs').promises; +const path = require('path'); +const connectDB = require('../config/database'); +const Footer = require('../models/footer'); + +async function validateFooterData(data) { + if (!data || typeof data !== 'object') { + throw new Error('Footer data must be a valid object'); + } + + const required = ['brand', 'explore', 'contact', 'newsletter', 'bottom']; + for (const field of required) { + if (!data[field]) { + throw new Error(`Missing required field: ${field}`); + } + } +} + +async function migrateFooterData() { + try { + await connectDB(); + console.log('Đã kết nối MongoDB...'); + + // Xóa dữ liệu cũ + await Footer.deleteMany({}); + console.log('Đã xóa dữ liệu Footer cũ'); + + // Đọc file JSON + const footerData = JSON.parse( + await fs.readFile( + path.join(__dirname, '../data/new/footer.json'), + 'utf8' + ) + ); + + // Validate + await validateFooterData(footerData); + + // Transform + const finalData = { + ...footerData, + updatedAt: new Date(), + }; + + // Insert + await Footer.create(finalData); + + console.log('✓ Migrate Footer thành công!'); + process.exit(0); + + } catch (error) { + console.error('❌ Lỗi:', error.message); + process.exit(1); + } +} + +migrateFooterData(); diff --git a/scripts/migrate-fresh.js b/scripts/migrate-fresh.js deleted file mode 100644 index 23e4dbb..0000000 --- a/scripts/migrate-fresh.js +++ /dev/null @@ -1,189 +0,0 @@ -require('dotenv').config(); -const path = require("path"); -const fs = require("fs"); -const { execSync } = require("child_process"); -const connectDB = require("../config/database"); -const migrationHelper = require("../utils/migrationHelper"); - -/** - * Tự động phát hiện tất cả các file script trong thư mục scripts - * Loại trừ các file quản lý migration và file không phải .js - */ -function discoverMigrations() { - const scriptsDir = __dirname; - const files = fs.readdirSync(scriptsDir); - - // Danh sách các file quản lý migration cần loại trừ - const excludeFiles = [ - 'migrate-all.js', - 'migrate-status.js', - 'migrate-rollback.js', - 'migrate-fresh.js', - 'make-migration.js', - 'MIGRATION_README.md' - ]; - - const migrations = files - .filter(file => { - // Lấy tất cả file .js, trừ các file quản lý - return file.endsWith('.js') && !excludeFiles.includes(file); - }) - .map(file => { - // Tạo tên migration từ tên file (bỏ .js) - const name = file.replace('.js', ''); - return { - name: name, - script: file - }; - }) - .sort((a, b) => { - // Sắp xếp theo tên để đảm bảo thứ tự nhất quán - return a.name.localeCompare(b.name); - }); - - return migrations; -} - -/** - * Chạy migration script (suppress output) - * Sử dụng child_process để chạy script độc lập vì các script tự quản lý DB connection - * Output từ script sẽ bị suppress để chỉ hiển thị status - */ -async function runMigrationScript(migration) { - return new Promise((resolve, reject) => { - try { - // Chạy script bằng child_process với stdio: 'pipe' để suppress output - // Nhưng vẫn capture stderr để có thể hiển thị lỗi nếu cần - const result = execSync(`node scripts/${migration.script}`, { - stdio: ['ignore', 'pipe', 'pipe'], // stdin: ignore, stdout: pipe, stderr: pipe - cwd: path.join(__dirname, ".."), - encoding: 'utf8' - }); - resolve(); - } catch (error) { - // Attach stderr vào error để có thể hiển thị sau - if (error.stderr) { - error.stderr = error.stderr; - } - reject(error); - } - }); -} - -/** - * Hiển thị bảng kết quả migration đơn giản - */ -function displayResults(results) { - console.log("\nRunning migrations...\n"); - - // Tìm độ dài tên migration dài nhất để format bảng - const maxNameLength = Math.max(...results.map(r => r.name.length), 20); - const statusWidth = 10; - const totalWidth = maxNameLength + statusWidth + 7; // 7 = spaces and separators - - // Header - console.log("=".repeat(totalWidth)); - console.log(`${'Migration'.padEnd(maxNameLength)} | ${'Status'.padEnd(statusWidth)}`); - console.log("=".repeat(totalWidth)); - - // Rows - results.forEach(result => { - let statusText = ""; - if (result.status === 'DONE') { - statusText = "DONE".padEnd(statusWidth); - } else if (result.status === 'FAIL') { - statusText = "FAIL".padEnd(statusWidth); - } - - console.log(`${result.name.padEnd(maxNameLength)} | ${statusText}`); - }); - - // Footer - console.log("=".repeat(totalWidth)); - - // Summary - const doneCount = results.filter(r => r.status === 'DONE').length; - const failCount = results.filter(r => r.status === 'FAIL').length; - - console.log(""); - if (doneCount > 0) { - console.log(`${doneCount} migration(s) completed`); - } - if (failCount > 0) { - console.log(`${failCount} migration(s) failed`); - } - console.log(""); -} - -/** - * Hàm chính để chạy lại tất cả migrations từ đầu (fresh) - * Xóa tất cả tracking và chạy lại từ đầu - */ -async function runFreshMigrations() { - const mongoose = require('mongoose'); - let ownConn = false; - - try { - const wasConnected = mongoose.connection.readyState === 1; - await connectDB(); - if (!wasConnected) ownConn = true; - - // Tự động phát hiện migrations - const migrations = discoverMigrations(); - - // Xóa tất cả tracking migrations - const Migration = require('../models/migration'); - await Migration.deleteMany({}); - - const batch = 1; // Batch mới bắt đầu từ 1 - const results = []; - - // Chạy từng migration - for (let i = 0; i < migrations.length; i++) { - const migration = migrations[i]; - - try { - // Chạy migration script (output bị suppress) - await runMigrationScript(migration); - - if (mongoose.connection.readyState !== 1) { - await connectDB(); - } - await migrationHelper.markAsRun(migration.name, batch); - - results.push({ name: migration.name, status: 'DONE' }); - } catch (error) { - results.push({ name: migration.name, status: 'FAIL', error: error.message }); - // Hiển thị bảng kết quả trước khi exit - displayResults(results); - console.error(`\n❌ Migration "${migration.name}" failed: ${error.message}`); - if (error.stderr) { - console.error(error.stderr.toString()); - } - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - process.exit(1); - } - } - - // Hiển thị bảng kết quả - displayResults(results); - - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - - process.exit(0); - } catch (error) { - console.error("\n❌ Error:", error.message); - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - process.exit(1); - } -} - -// Chạy hàm chính -runFreshMigrations(); - diff --git a/scripts/migrate-header-menu.js b/scripts/migrate-header-menu.js deleted file mode 100644 index cc5b2e2..0000000 --- a/scripts/migrate-header-menu.js +++ /dev/null @@ -1,69 +0,0 @@ -const mongoose = require('mongoose'); -const fs = require('fs'); -const path = require('path'); -const dotenv = require('dotenv'); -const HeaderMenu = require('../models/headerMenu'); - -dotenv.config(); - -const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/SIMS'; - -async function connectDB() { - try { - await mongoose.connect(MONGODB_URI); - console.log('✅ MongoDB Connected for Migration'); - } catch (err) { - console.error('❌ MongoDB Connection Error:', err); - process.exit(1); - } -} - -const processMenuItems = async (items, parentId = null) => { - for (const item of items) { - console.log(` > Importing: ${item.label}`); - - const menuDoc = { - title: item.label, - slug: item.slug, - url: item.href, - parentId: parentId, - order: item.order || 0, - status: item.isActive === false ? "inactive" : "active", - type: item.type === "external" ? "external" : "internal" - }; - - const createdItem = await HeaderMenu.create(menuDoc); - - if (item.children && item.children.length > 0) { - await processMenuItems(item.children, createdItem._id); - } - } -}; - -async function migrate() { - await connectDB(); - - try { - console.log('--- Starting Header Menu Migration ---'); - - // 1. Clear existing menu items - await HeaderMenu.deleteMany({}); - console.log('🗑️ Cleared existing HeaderMenu collection'); - - // 2. Read JSON data - const dataPath = path.join(__dirname, '../data/header-menu.json'); - const fileData = fs.readFileSync(dataPath, 'utf8'); - const menuItems = JSON.parse(fileData); - - // 3. Recursive import - await processMenuItems(menuItems); - - console.log('--- Migration Completed Successfully ---'); - process.exit(0); - } catch (error) { - console.error('❌ Migration Failed:', error); - process.exit(1); - } -} - -migrate(); diff --git a/scripts/migrate-header.js b/scripts/migrate-header.js deleted file mode 100644 index 8a9c06b..0000000 --- a/scripts/migrate-header.js +++ /dev/null @@ -1,73 +0,0 @@ -const mongoose = require("mongoose"); -const path = require("path"); -require("dotenv").config({ path: path.join(__dirname, "../.env") }); - -const Header = require("../models/header"); -const headerData = require("../data/header.json"); - -const migrateHeader = async () => { - try { - const mongoUri = process.env.MONGODB_URI; - if (!mongoUri) { - throw new Error("MONGODB_URI not found in environment variables"); - } - await mongoose.connect(mongoUri); - console.log("Connected to MongoDB"); - - // Delete existing header - await Header.deleteMany({}); - console.log("Cleared existing headers"); - - // Transform and insert data - const headerDocument = { - top: { - phone: headerData.top?.phone || "", - email: headerData.top?.email || "", - location: headerData.top?.location || "", - socialLinks: (headerData.top?.socialLinks || []).map((link, idx) => ({ - ...link, - order: idx, - })), - languages: headerData.top?.languages || [], - }, - offcanvas: headerData.offcanvas || {}, - menu: (headerData.menu || []).map((item, idx) => ({ - ...item, - order: idx, - children: - item.children?.map((child, childIdx) => ({ - ...child, - order: childIdx, - children: - child.children?.map((subchild, subIdx) => ({ - ...subchild, - order: subIdx, - })) || [], - })) || [], - })), - logo: { - light: "/assets/img/logo/white-logo.svg", - dark: "/assets/img/logo/black-logo.svg", - alt: "Hai Learning", - }, - ctaButton: { - label: "Get Started", - href: "/contact", - style: "primary", - }, - status: "active", - order: 1, - }; - - const result = await Header.create(headerDocument); - console.log("Header migrated successfully:", result._id); - - await mongoose.connection.close(); - process.exit(0); - } catch (error) { - console.error("Migration error:", error); - process.exit(1); - } -}; - -migrateHeader(); diff --git a/scripts/migrate-home.js b/scripts/migrate-home.js new file mode 100644 index 0000000..423c241 --- /dev/null +++ b/scripts/migrate-home.js @@ -0,0 +1,56 @@ +require('dotenv').config(); +const fs = require('fs').promises; +const path = require('path'); +const connectDB = require('../config/database'); +const Home = require('../models/home'); + +async function validateHomeData(data) { + if (!data || typeof data !== 'object') { + throw new Error('Home data must be a valid object'); + } + + // Ví dụ validate cơ bản (tuỳ schema bạn chỉnh thêm) + if (!data.hero || !data.quickLinks) { + throw new Error('Missing required fields: hero or quickLinks'); + } +} + +async function migrateHomeData() { + try { + await connectDB(); + console.log('Đã kết nối MongoDB...'); + + // Xóa dữ liệu cũ + await Home.deleteMany({}); + console.log('Đã xóa dữ liệu Home cũ'); + + // Đọc file JSON + const homeData = JSON.parse( + await fs.readFile( + path.join(__dirname, '../data/home.json'), + 'utf8' + ) + ); + + // Validate + await validateHomeData(homeData); + + // Transform nếu cần (optional) + const finalData = { + ...homeData, + updatedAt: new Date() + }; + + // Insert + await Home.create(finalData); + + console.log('✓ Migrate Home thành công!'); + process.exit(0); + + } catch (error) { + console.error('❌ Lỗi:', error.message); + process.exit(1); + } +} + +migrateHomeData(); \ No newline at end of file diff --git a/scripts/migrate-rollback.js b/scripts/migrate-rollback.js deleted file mode 100644 index 32d6888..0000000 --- a/scripts/migrate-rollback.js +++ /dev/null @@ -1,111 +0,0 @@ -require('dotenv').config(); -const connectDB = require('../config/database'); -const migrationHelper = require('../utils/migrationHelper'); - -/** - * Rollback một migration cụ thể - */ -async function rollbackMigration(migrationName) { - const mongoose = require('mongoose'); - let ownConn = false; - - try { - const wasConnected = mongoose.connection.readyState === 1; - await connectDB(); - if (!wasConnected) ownConn = true; - - const hasRun = await migrationHelper.hasRun(migrationName); - if (!hasRun) { - console.log(`⚠️ Migration "${migrationName}" chưa được chạy, không thể rollback.`); - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - process.exit(0); - } - - const result = await migrationHelper.rollback(migrationName); - if (result) { - console.log(`✅ Đã rollback migration: ${migrationName}`); - } else { - console.log(`❌ Không thể rollback migration: ${migrationName}`); - } - - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - } catch (error) { - console.error('❌ Lỗi:', error.message); - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - process.exit(1); - } -} - -/** - * Rollback batch cuối cùng - */ -async function rollbackLastBatch() { - const mongoose = require('mongoose'); - let ownConn = false; - - try { - const wasConnected = mongoose.connection.readyState === 1; - await connectDB(); - if (!wasConnected) ownConn = true; - - const lastBatch = await migrationHelper.getLastBatch(); - if (lastBatch === 0) { - console.log('⚠️ Không có batch nào để rollback.'); - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - process.exit(0); - } - - const migrations = await migrationHelper.getMigrationsByBatch(lastBatch); - if (migrations.length === 0) { - console.log(`⚠️ Batch ${lastBatch} không có migration nào.`); - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - process.exit(0); - } - - console.log(`\n🔄 Đang rollback batch ${lastBatch}...`); - console.log(`📋 Các migration sẽ được rollback:`); - migrations.forEach(m => { - console.log(` - ${m.name}`); - }); - - const deletedCount = await migrationHelper.rollbackBatch(lastBatch); - console.log(`\n✅ Đã rollback ${deletedCount} migration(s) trong batch ${lastBatch}`); - - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - } catch (error) { - console.error('❌ Lỗi:', error.message); - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - process.exit(1); - } -} - -// Xử lý command line arguments -const args = process.argv.slice(2); - -if (args.length === 0) { - console.log('Usage:'); - console.log(' node scripts/migrate-rollback.js - Rollback một migration cụ thể'); - console.log(' node scripts/migrate-rollback.js --batch - Rollback batch cuối cùng'); - process.exit(0); -} - -if (args[0] === '--batch') { - rollbackLastBatch(); -} else { - rollbackMigration(args[0]); -} - diff --git a/scripts/migrate-status.js b/scripts/migrate-status.js deleted file mode 100644 index e53dc22..0000000 --- a/scripts/migrate-status.js +++ /dev/null @@ -1,120 +0,0 @@ -require('dotenv').config(); -const connectDB = require('../config/database'); -const migrationHelper = require('../utils/migrationHelper'); -const path = require('path'); -const fs = require('fs'); - -/** - * Tự động phát hiện tất cả các file script trong thư mục scripts - * Loại trừ các file quản lý migration và file không phải .js - */ -function discoverMigrations() { - const scriptsDir = __dirname; - const files = fs.readdirSync(scriptsDir); - - // Danh sách các file quản lý migration cần loại trừ - const excludeFiles = [ - 'migrate-all.js', - 'migrate-status.js', - 'migrate-rollback.js', - 'migrate-fresh.js', - 'make-migration.js', - 'MIGRATION_README.md' - ]; - - const migrations = files - .filter(file => { - // Lấy tất cả file .js, trừ các file quản lý - return file.endsWith('.js') && !excludeFiles.includes(file); - }) - .map(file => file.replace('.js', '')) - .sort(); - - return migrations; -} - -// Tự động phát hiện migrations -const availableMigrations = discoverMigrations(); - -/** - * Hiển thị trạng thái của tất cả migrations - */ -async function showStatus() { - const mongoose = require('mongoose'); - let ownConn = false; - - try { - const wasConnected = mongoose.connection.readyState === 1; - await connectDB(); - if (!wasConnected) ownConn = true; - - console.log('\nMigration Status:\n'); - - const ranMigrations = await migrationHelper.getRanMigrations(); - const ranMap = new Map(); - ranMigrations.forEach(m => ranMap.set(m.name, m)); - - // Tính toán độ rộng cột - const maxNameLength = Math.max(...availableMigrations.map(name => name.length), 20); - const statusWidth = 10; - const batchWidth = 6; - const ranAtWidth = 20; - const totalWidth = maxNameLength + statusWidth + batchWidth + ranAtWidth + 11; // 11 = spaces and separators - - // Header - console.log('='.repeat(totalWidth)); - console.log( - `${'Migration Name'.padEnd(maxNameLength)} | ${'Status'.padEnd(statusWidth)} | ${'Batch'.padEnd(batchWidth)} | Ran At` - ); - console.log('='.repeat(totalWidth)); - - let pendingCount = 0; - let ranCount = 0; - - for (const migrationName of availableMigrations) { - const migration = ranMap.get(migrationName); - if (migration) { - const ranAt = new Date(migration.ranAt).toLocaleString('vi-VN'); - console.log( - `${migrationName.padEnd(maxNameLength)} | ${'Ran'.padEnd(statusWidth)} | ${String(migration.batch).padEnd(batchWidth)} | ${ranAt}` - ); - ranCount++; - } else { - console.log( - `${migrationName.padEnd(maxNameLength)} | ${'Pending'.padEnd(statusWidth)} | ${'-'.padEnd(batchWidth)} | -` - ); - pendingCount++; - } - } - - console.log('='.repeat(totalWidth)); - console.log(`\nSummary:`); - console.log(` Ran: ${ranCount} migration(s)`); - console.log(` Pending: ${pendingCount} migration(s)`); - - const lastBatch = await migrationHelper.getLastBatch(); - if (lastBatch > 0) { - console.log(` Last batch: ${lastBatch}`); - } - - console.log(''); - - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - } catch (error) { - console.error('Error:', error.message); - if (ownConn && mongoose.connection.readyState === 1) { - await mongoose.disconnect(); - } - process.exit(1); - } -} - -// Chạy nếu được gọi trực tiếp -if (require.main === module) { - showStatus(); -} - -module.exports = { showStatus }; - diff --git a/scripts/migrateAboutUs.js b/scripts/migrateAboutUs.js deleted file mode 100644 index 29b705d..0000000 --- a/scripts/migrateAboutUs.js +++ /dev/null @@ -1,48 +0,0 @@ -const mongoose = require("mongoose"); -const dotenv = require("dotenv"); -const fs = require("fs"); -const path = require("path"); - -// Load environment variables -dotenv.config(); - -const AboutUs = require("../models/aboutUs"); - -const migrate = async () => { - try { - console.log("🚀 Starting About Us migration..."); - - // 1. Connect to MongoDB - await mongoose.connect(process.env.MONGODB_URI); - console.log("✅ MongoDB Connected"); - - // 2. Read about.json from Backend (Source of Truth) - const jsonPath = path.join(__dirname, "../data/about.json"); - if (!fs.existsSync(jsonPath)) { - throw new Error(`Source about.json not found at: ${jsonPath}`); - } - - const rawData = fs.readFileSync(jsonPath, "utf8"); - const jsonData = JSON.parse(rawData); - console.log("✅ Read about.json successfully"); - - // 3. Delete existing AboutUs documents (Singleton pattern) - await AboutUs.deleteMany({}); - console.log("✅ Cleared existing AboutUs collection"); - - // 4. Create new AboutUs document with JSON data - const newAboutUs = new AboutUs(jsonData); - await newAboutUs.save(); - console.log("✅ Successfully migrated about.json data to MongoDB"); - - } catch (error) { - console.error("❌ Migration failed:", error.message); - } finally { - // 5. Close connection - await mongoose.connection.close(); - console.log("👋 Database connection closed"); - process.exit(0); - } -}; - -migrate(); diff --git a/scripts/seedAbout.js b/scripts/seedAbout.js deleted file mode 100644 index 3e60620..0000000 --- a/scripts/seedAbout.js +++ /dev/null @@ -1,52 +0,0 @@ -const mongoose = require("mongoose"); -const dotenv = require("dotenv"); -const fs = require("fs"); -const path = require("path"); - -// Load environment variables -dotenv.config(); - -const AboutUs = require("../models/aboutUs"); - -const seedAbout = async () => { - try { - console.log("🚀 Starting About section seeding..."); - - // 1. Connect to MongoDB - if (!process.env.MONGODB_URI) { - throw new Error("MONGODB_URI is not defined in environment variables"); - } - await mongoose.connect(process.env.MONGODB_URI); - console.log("✅ MongoDB Connected"); - - // 2. Read about.json (Single Source of Truth) - const jsonPath = path.join(__dirname, "../data/about.json"); - if (!fs.existsSync(jsonPath)) { - throw new Error(`Source about.json not found at: ${jsonPath}`); - } - - const rawData = fs.readFileSync(jsonPath, "utf8"); - const jsonData = JSON.parse(rawData); - console.log("✅ Read data/about.json successfully"); - - // 3. Upsert logic (Singleton pattern) - // We look for any existing document and update it, or create a new one if none exists. - await AboutUs.findOneAndUpdate( - {}, - jsonData, - { upsert: true, new: true, setDefaultsOnInsert: true } - ); - - console.log("✅ Successfully seeded about.json data to MongoDB (Upserted)"); - - } catch (error) { - console.error("❌ Seeding failed:", error.message); - } finally { - // 4. Close connection - await mongoose.connection.close(); - console.log("👋 Database connection closed"); - process.exit(0); - } -}; - -seedAbout(); diff --git a/scripts/update-header-data.js b/scripts/update-header-data.js deleted file mode 100644 index 0607352..0000000 --- a/scripts/update-header-data.js +++ /dev/null @@ -1,106 +0,0 @@ -const mongoose = require("mongoose"); -const path = require("path"); -require("dotenv").config({ path: path.join(__dirname, "../.env") }); - -const Header = require("../models/header"); - -async function updateHeaderData() { - try { - // Connect to MongoDB - await mongoose.connect(process.env.MONGODB_URI || "mongodb://localhost:27017/hailearning"); - console.log("Connected to MongoDB"); - - // Find the first header - let header = await Header.findOne().sort({ order: 1 }); - - if (!header) { - console.log("No header found, creating new one..."); - header = new Header({ - top: { - phone: "+09 378 357 5222", - email: "info@hailearning.edu.vn", - location: "69 Street, 5th Avenue LA, United States", - socialLinks: [ - { - platform: "linkedin", - url: "https://linkedin.com", - icon: "fa-brands fa-linkedin", - }, - { - platform: "twitter", - url: "https://twitter.com", - icon: "fa-brands fa-twitter", - }, - { - platform: "instagram", - url: "https://instagram.com", - icon: "fa-brands fa-instagram", - }, - { - platform: "youtube", - url: "https://youtube.com", - icon: "fa-brands fa-youtube", - }, - ], - languages: [ - { name: "English", value: "1" }, - { name: "Bangla", value: "2" }, - { name: "Hindi", value: "3" }, - ], - }, - status: "active", - order: 1, - }); - } else { - console.log("Header found, updating..."); - // Update existing header - header.top = { - phone: header.top?.phone || "+09 378 357 5222", - email: header.top?.email || "info@hailearning.edu.vn", - location: header.top?.location || "69 Street, 5th Avenue LA, United States", - socialLinks: - header.top?.socialLinks?.length > 0 - ? header.top.socialLinks - : [ - { - platform: "linkedin", - url: "https://linkedin.com", - icon: "fa-brands fa-linkedin", - }, - { - platform: "twitter", - url: "https://twitter.com", - icon: "fa-brands fa-twitter", - }, - { - platform: "instagram", - url: "https://instagram.com", - icon: "fa-brands fa-instagram", - }, - { - platform: "youtube", - url: "https://youtube.com", - icon: "fa-brands fa-youtube", - }, - ], - languages: header.top?.languages || [ - { name: "English", value: "1" }, - { name: "Bangla", value: "2" }, - { name: "Hindi", value: "3" }, - ], - }; - } - - await header.save(); - console.log("Header updated successfully!"); - console.log("Header data:", JSON.stringify(header, null, 2)); - - await mongoose.connection.close(); - console.log("Database connection closed"); - } catch (error) { - console.error("Error updating header:", error); - process.exit(1); - } -} - -updateHeaderData(); diff --git a/services/keycloak.js b/services/keycloak.js deleted file mode 100644 index 4eeb13a..0000000 --- a/services/keycloak.js +++ /dev/null @@ -1,10 +0,0 @@ -// Keycloak config -const keycloakConfig = { - clientId: process.env.KEYCLOAK_CLIENT_ID, - clientSecret: process.env.KEYCLOAK_CLIENT_SECRET, - redirectUri: process.env.KEYCLOAK_REDIRECT_URI, - authServerUrl: process.env.KEYCLOAK_AUTH_SERVER_URL, - isDev: process.env.KEYCLOAK_IS_DEV, -}; - -module.exports = keycloakConfig; \ No newline at end of file diff --git a/views/admin/about/index.ejs b/views/admin/about/index.ejs new file mode 100644 index 0000000..8bc3ba4 --- /dev/null +++ b/views/admin/about/index.ejs @@ -0,0 +1,812 @@ +
+
+
+

+ <%= title %> +

+

Edit content displayed on the About page

+
+
+ +
+
+
+ + + + + + + + + +
+
+ +
+ +
+
+ + +
+
+
+
Hero Section
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ <% if (data.hero?.image) { %> + + <% } %> +
+
+ +
+
+
Core Values
+ +
+
+ <% (data.hero?.coreValues || []).forEach(val=> { %> +
+ + +
+ <% }) %> +
+
+
+
+
+ + +
+
+
+
Leadership
+ +
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+
+
Learning Model +
+
+
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
Asynchronous Learning
+ +
+
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+
Synchronous Learning
+ +
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+ + +
+
+
+
Accreditation
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
Badges
+ +
+
+
+
+
+
Stats
+ +
+
+
+
+
+
+
+ + +
+
+
+
Success Stories
+ +
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+
+
+
Call to Action
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+ + +
+
+
+
+
+ + \ No newline at end of file diff --git a/views/admin/aboutUs/index.ejs b/views/admin/aboutUs/index.ejs deleted file mode 100644 index 556b9cd..0000000 --- a/views/admin/aboutUs/index.ejs +++ /dev/null @@ -1,967 +0,0 @@ -
-
-
-

- <%= title %> -

-

Edit content displayed on About Us page

-
-
- - View About Us Page - -
-
- -
-
-
- - - - - - - - - -
-
- - -
- -
-
- -
-
-
-
Hero Section
-
-
-
-
- - -
-
- - -
-
- -
- - -
- <% if (data.hero?.backgroundImage) { %> - - <% } %> -
-
-
-
-
- - -
-
-
-
Introduction
-
-
-
-
- - -
-
- - -
-
- - -
-
- -
- - -
- <% if (data.intro?.image) { %> - - <% } %> -
-
-
-
-
- - -
-
-
-
Mission Section
-
-
-
-
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
Images
-
- <% ['main', 'secondary', 'bgShape', 'planeShape', 'topShape', 'globeShape'].forEach(imgKey => { %> -
- -
- - -
-
- <% }) %> -
- -
-
-
- - -
-
-
-
-
- - -
-
-
-
-
-
-
- - -
-
-
-
Features Section
-
-
-
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-
- -
- - -
-
-
- - -
-
- - -
-
- -
-
-
Feature Items
- -
-
-
-
-
-
- - -
-
-
-
News Section (Blog Preview)
- System will automatically fetch the 3 latest posts if no specific blog is selected. -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- -

Select blog posts to display on About page. If none are selected, the system will use the 3 latest posts.

-
- <% if (allBlogs && allBlogs.length > 0) { %> - <% allBlogs.forEach(blog => { - const isSelected = data.news?.selectedBlogIds && data.news.selectedBlogIds.some(id => id.toString() === blog._id.toString()); - %> -
-
-
-
- onclick="event.stopPropagation(); handleAboutCheckboxChange(this)"> -
-
- -
-
- <%= blog.title %> -
-

- <%= blog.publishedAt ? new Date(blog.publishedAt).toLocaleDateString('vi-VN') : '' %> -

-
-
-
- <% }) %> - <% } else { %> -
-

No published blogs found. Please create some blogs first.

- Create Blog -
- <% } %> -
-
-
-
-
-
-
-
- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/views/admin/appointment/index.ejs b/views/admin/appointment/index.ejs deleted file mode 100644 index 707fba1..0000000 --- a/views/admin/appointment/index.ejs +++ /dev/null @@ -1,791 +0,0 @@ -
-
-
-

- <%= title %> -

-

Edit content displayed on Appointment page

-
-
- - View Appointment Page - -
-
- -
-
-
- - - - - - -
- - -
-
- -
-
-
-
Hero Section
-
-
- -
- - -
- Recommended size: 1920x1080px -
-
-
- <% if (data.hero?.backgroundImage) { %> - <% let heroImgSrc=data.hero.backgroundImage; if (heroImgSrc && - !heroImgSrc.startsWith('http://') && - !heroImgSrc.startsWith('https://')) { - heroImgSrc=heroImgSrc.startsWith('/') ? heroImgSrc : '/' + - heroImgSrc; } %> - Background image preview - - <% } else { %> -
- Image preview -
- <% } %> -
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- - -
-
-
-
-
Visa Options
- -
-

These options will appear in the visa type selection - dropdown on the appointment form.

-
- <% if (data.visaOptions && data.visaOptions.length> 0) { %> - <% data.visaOptions.forEach((option, index)=> { %> -
- - - -
- <% }); %> - <% } %> -
-
-
-
- - -
-
-
-
Form Settings
-
-
- - -
-
- - -
- - - -
- -
- -
-
Form Fields
- -
- -
- <% if (data.form?.fields && data.form.fields.length> 0) { %> - <% data.form.fields.forEach((field, index)=> { %> -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- > -
-
-
- - -
-
- -
-
- <% }); %> - <% } %> -
-
-
-
- - -
-
-
-
-
Recent Submissions
-
- - -
- -
- - -
-
- - -
-
- -
- -
- -
- - - - - - - - - - - - - - - <% if (locals.submissions && submissions.length> 0) { %> - <% submissions.forEach(submission=> { %> - - - - - - - - - - - <% }); %> - <% } else { %> - - - - <% } %> - -
DateNameContactAppt DateVisa TypesMessageStatusAction
- <%= new - Date(submission.createdAt).toLocaleDateString() - %> -
- - <%= new - Date(submission.createdAt).toLocaleTimeString([], - {hour: '2-digit' , minute:'2-digit'}) %> - -
- <%= submission.name %> - -
- - <%= submission.email %> - - <% if(submission.phone) { %> - - <%= submission.phone %> - - <% } %> -
-
- <%= submission.appointmentDate || '-' %> - - <% if (submission.visaTypes && - submission.visaTypes.length> 0) { %> - <% submission.visaTypes.forEach(type=> { %> - - <%= type %> - - <% }); %> - <% } else { %> - - - <% } %> - - <% if (submission.message) { %> -
- <%= submission.message %> -
- <% } else { %> - - - <% } %> -
- <% let statusClass='bg-secondary' ; - if(submission.status==='pending' ) - statusClass='bg-warning text-dark' ; - if(submission.status==='confirmed' ) - statusClass='bg-success' ; - if(submission.status==='completed' ) - statusClass='bg-info text-dark' ; - if(submission.status==='cancelled' ) - statusClass='bg-danger' ; %> - - <%= submission.status %> - - - -
No - submissions found
-
-
- Showing last 50 submissions -
-
-
-
-
-
-
- - -
- - -
-
-
-
-
- - - - - - - \ No newline at end of file diff --git a/views/admin/booking/index.ejs b/views/admin/booking/index.ejs deleted file mode 100644 index 0c86370..0000000 --- a/views/admin/booking/index.ejs +++ /dev/null @@ -1,2034 +0,0 @@ -
-
-
-

Booking Management

-

Edit content displayed on Booking page

-
-
- - View Booking Page - -
-
- -
-
-
- - - - - - - - - - - - - - -
- - -
-
- -
-
-
-
-
- -
- - -
-
-
- <% if (data.hero?.backgroundImage) { %> - Background image preview - <% } %> -
-
-
-
- - -
-
-
-
-
- - - - - -
-
-
-
-
- - -
-
- -
-
Price Settings
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
Age Settings
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
Other Settings
-
-
- - -
-
- - -
-
- - -
-
- -
-
-
Rating Options
- -
- -
- <% if (data.filterPanel?.ratingOptions && data.filterPanel.ratingOptions.length > 0) { %> - <% data.filterPanel.ratingOptions.forEach((option, index) => { %> -
-
-
-
- - -
-
- - -
-
- -
-
- <% }); %> - <% } %> -
- -
-
-
- - -
-
-
-
-
Programs
- -
-
- <% if (data.programs && data.programs.length > 0) { %> - <% data.programs.forEach((program, index) => { %> -
-
-
-
- - -
-
- - -
-
- -
-
- <% }); %> - <% } %> -
-
-
-
- - -
-
-
-
-
Camps
- -
-
- <% if (data.camps && data.camps.length > 0) { %> - <% data.camps.forEach((camp, index) => { %> -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
-
- - -
-
- -
- - Camp Image Preview - -
-
- - -
-
- -
-
- <% }); %> - <% } %> -
-
-
-
- - -
-
-
-
Discount Management
- -
-
-
-
-
-
- - -
-
-
-
Voucher Management
- -
-
-
-
-
-
- -
-
-
- -
- -
-
- -
-
- - Edit fields for the Participant Information step. - Field "Name" keys should not - be changed as they link to backend logic. -
-
-
-
- -
-
- - Edit fields for the Guardian Information step. - Use the "Required" checkbox on each field to mark mandatory fields. -
-
-
-
-
-
-
-
- - -
- - -
- <% if (!data || !data._id) { %> - <% } %> - -
-
-
- - - - - - - - \ No newline at end of file diff --git a/views/admin/dashboard.ejs b/views/admin/dashboard.ejs index 1a8eee7..b28ccc8 100644 --- a/views/admin/dashboard.ejs +++ b/views/admin/dashboard.ejs @@ -65,7 +65,7 @@
- +
@@ -78,7 +78,7 @@

Manage about us

- + Edit
@@ -123,7 +123,7 @@
-
+
- -
-
-
-
- -
-
-
Appointment
-

Manage appointment page

-
-
- - Edit - -
-
- - -
-
-
-
- -
-
-
Pricing
-

Manage pricing page

-
-
- - Edit - -
-
- -
-
-
-
- -
-
-
Services
-

Manage services

-
-
- - Edit - -
-
- -
+
- -
-
-
-
- -
-
-
Visa
-

Manage visa countries

-
-
- - Edit - -
-
- -
+
+
@@ -297,6 +223,8 @@ + +
@@ -318,6 +246,31 @@ + + + + +
+
+ +
+ Footer API +
+ + /api/footer + + GET + + API to get footer data + + + View + + + + +
@@ -339,30 +292,8 @@ - - -
-
- -
- About Us API -
- - /api/about-us - - GET - - API to get about us data - - - View - - - - - +
@@ -384,6 +315,8 @@ + +
@@ -405,6 +338,8 @@ + +
@@ -426,6 +361,8 @@ + +
@@ -447,6 +384,32 @@ + + + + + +
@@ -468,6 +431,8 @@ + +
@@ -515,7 +480,7 @@
Version
- CMS.HAILearning v1.0.0 + CMS.LAMS v1.0.0
diff --git a/views/admin/department/create.ejs b/views/admin/department/create.ejs deleted file mode 100644 index c7cdcd0..0000000 --- a/views/admin/department/create.ejs +++ /dev/null @@ -1,98 +0,0 @@ - -
-
-
-

Create New Department

-

Add a new department to the system

-
- -
- -
-
-
-
-
-
- - -
- - Enter the full name of the department (e.g. Business, Engineering) -
-
- -
- - Tips: -
    -
  • Department name should be clear and descriptive
  • -
  • Use proper capitalization
  • -
  • Avoid abbreviations unless commonly known
  • -
-
- -
- -
-
-
-
-
-
-
- - -
-
-
-
Notification
- -
-
-

Content of the notification will appear here.

-
- -
-
- - - - - diff --git a/views/admin/department/edit.ejs b/views/admin/department/edit.ejs deleted file mode 100644 index b2dcb1a..0000000 --- a/views/admin/department/edit.ejs +++ /dev/null @@ -1,100 +0,0 @@ - -
-
-
-

Edit Department

-

- Editing department: <%= department.name %> -

-
- -
- -
-
-
-
-
-
- - -
- - Enter the full name of the department (e.g. Business, Engineering) -
-
- -
- - Tips: -
    -
  • Department name should be clear and descriptive
  • -
  • Use proper capitalization
  • -
  • Avoid abbreviations unless commonly known
  • -
-
- -
- -
-
-
-
-
-
-
- - -
-
-
-
Notification
- -
-
-

Content of the notification will appear here.

-
- -
-
- - - - - diff --git a/views/admin/department/index.ejs b/views/admin/department/index.ejs deleted file mode 100644 index bc4d528..0000000 --- a/views/admin/department/index.ejs +++ /dev/null @@ -1,126 +0,0 @@ - -
-
-
-

Department Management

-

Manage all departments in the system

-
- -
- -
-
-
-
- <% if (departments && departments.length > 0) { %> -
- - - - - - - - - - - <% departments.forEach((department, index) => { %> - - - - - - - <% }); %> - -
#Department NameSlugActions
<%= index + 1 %> - <%= department.name %> - - <%= department.slug %> - -
- - Edit - - -
-
-
- <% } else { %> -
- -
No Departments Found
- - Create First Department - -
- <% } %> -
-
-
-
-
- - -
-
-
-
Delete Confirmation
- -
-
-

Are you sure you want to delete this department?

-
- -
-
- - - - - diff --git a/views/admin/footer/index.ejs b/views/admin/footer/index.ejs index 08257ef..f994aa8 100644 --- a/views/admin/footer/index.ejs +++ b/views/admin/footer/index.ejs @@ -1,45 +1,50 @@
-

Footer Management

-

Edit footer content and structure

+

+ <%= title %> +

+

Edit content displayed in the site footer

-
- - - +
+ + + + + + - +
-