Files
uldp-degree-mangement-system/controllers/activityController.js
r2xrzh9q2z-lab d1b931d547 first commit
2026-02-02 11:07:09 +07:00

1617 lines
58 KiB
JavaScript

const {addBaseUrlToImages} = require("../utils/imageHelper");
const Activity = require("../models/activity");
const mongoose = require('mongoose');
// -------------------- Public (API) exports --------------------
// API endpoint: return all active activities as JSON
exports.api = async (req, res) => {
try {
// Return structured response with filters and camps
const baseUrl =
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
// Get filters document (single doc with isFiltersDoc:true)
const filtersDoc = await Activity.findOne({ isFiltersDoc: true }).lean();
const filters = (filtersDoc && Array.isArray(filtersDoc.filters)) ? filtersDoc.filters : [];
// Fetch camps (activities) excluding the filters doc
const activities = await Activity.find({ isFiltersDoc: { $ne: true }, isActive: true })
.sort({ order: 1, createdAt: -1 })
.lean();
const camps = (activities || []).map((activity) => addBaseUrlToImages(activity, baseUrl));
// Get hero data from the first activity (assuming all activities share the same hero)
const heroRaw = activities.length > 0 && activities[0].hero ? activities[0].hero : {};
const hero = addBaseUrlToImages(heroRaw, baseUrl);
return res.json({ hero, filter: filters, camps });
} catch (err) {
console.error("activity.api error:", err);
return res.status(500).json({error: "Error loading activities data"});
}
};
// API endpoint: return a single activity by ID or link
exports.apiDetail = async (req, res) => {
try {
const {id} = req.params;
// Try to find by ID first, then by link
let activity;
if (id.match(/^[0-9a-fA-F]{24}$/)) {
activity = await Activity.findById(id).lean();
}
if (!activity) {
activity = await Activity.findOne({link: `/${id}`}).lean();
}
if (!activity) {
return res.status(404).json({error: "Activity not found"});
}
const baseUrl =
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
const processed = addBaseUrlToImages(activity, baseUrl);
return res.json(processed);
} catch (err) {
console.error("activity.apiDetail error:", err);
return res.status(500).json({error: "Error loading activity data"});
}
};
// -------------------- Admin exports --------------------
// Get default activity data for creating new activity
const getDefaultActivityData = () => ({
hero: {
title: "",
bannerImage: "",
},
name: "",
price: 0,
priceText: "",
season: [],
age: [12, 18],
locations: [],
image: "",
link: "",
program: "",
rating: 4,
isActive: true,
order: 0,
});
// Display activities management page (list)
exports.index = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const skip = (page - 1) * limit;
const activitiesPromise = Activity.find({ isFiltersDoc: { $ne: true } })
.sort({order: 1, createdAt: -1})
.skip(skip)
.limit(limit)
.lean();
const totalPromise = Activity.countDocuments({ isFiltersDoc: { $ne: true } });
const activePromise = Activity.countDocuments({ isFiltersDoc: { $ne: true }, isActive: true });
// Fetch filters from the consolidated Activity document (isFiltersDoc:true)
const filtersPromise = Activity.findOne({isFiltersDoc: true}).lean();
// Get all activities with booking sessions for extracting all bookings
const allActivitiesForBookingsPromise = Activity.find({
isFiltersDoc: { $ne: true },
'bookingSessions.bookingList': { $exists: true, $ne: [] }
}).lean();
const [activities, total, filtersDoc, activeCount, allActivitiesForBookings] = await Promise.all([
activitiesPromise,
totalPromise,
filtersPromise,
activePromise,
allActivitiesForBookingsPromise,
]);
// Extract all bookings from bookingSessions.bookingList
const allBookings = [];
const bookingCountMap = {};
const sessionBookingCountMap = {};
allActivitiesForBookings.forEach(activity => {
const actId = activity._id.toString();
let activityBookingCount = 0;
sessionBookingCountMap[actId] = {};
if (activity.bookingSessions && Array.isArray(activity.bookingSessions)) {
activity.bookingSessions.forEach(session => {
if (session.bookingList && Array.isArray(session.bookingList)) {
const sessionBookingCount = session.bookingList.length;
activityBookingCount += sessionBookingCount;
sessionBookingCountMap[actId][session.sessionId] = sessionBookingCount;
// Add each booking to allBookings array with activity info
session.bookingList.forEach(booking => {
const bookingWithActivityInfo = {
...booking,
activityId: {
_id: activity._id,
name: activity.name,
link: activity.link
},
sessionId: session.sessionId,
createdAt: booking.createdAt || booking.bookingDate || new Date(),
status: booking.status || booking.bookingStatus || 'pending',
paymentStatus: booking.paymentStatus || 'pending',
totalAmount: booking.totalAmount || 0,
paidAmount: booking.paidAmount || 0
};
allBookings.push(bookingWithActivityInfo);
});
}
});
}
bookingCountMap[actId] = activityBookingCount;
});
// Sort all bookings by creation date (newest first)
allBookings.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
// Add booking counts to activities
const activitiesWithBookings = activities.map(activity => {
const actId = activity._id.toString();
return {
...activity,
bookingCount: bookingCountMap[actId] || 0,
sessionBookingCounts: sessionBookingCountMap[actId] || {}
};
});
const filters = (filtersDoc && Array.isArray(filtersDoc.filters)) ? filtersDoc.filters : [];
const totalPages = Math.ceil(total / limit);
// Calculate all bookings stats
const allBookingsStats = {
total: allBookings.length,
confirmed: allBookings.filter(b => b.status === 'confirmed').length,
pending: allBookings.filter(b => b.status === 'pending').length,
cancelled: allBookings.filter(b => b.status === 'cancelled').length,
completed: allBookings.filter(b => b.status === 'completed').length,
totalRevenue: allBookings.filter(b => b.status !== 'cancelled').reduce((sum, b) => sum + (b.totalAmount || 0), 0)
};
res.render("admin/activity/index", {
layout: "layouts/main",
title: "Activities Management",
items: activitiesWithBookings,
filters: filters, // Pass filters to view
activeCount,
allBookings: allBookings, // All bookings for the All Bookings tab
allBookingsStats: allBookingsStats, // Stats for bookings
pagination: {
page,
limit,
total,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1,
},
frontendUrl:
process.env.FRONTEND_URL || req.protocol + "://" + req.get("host"),
currentPath: req.path,
user: req.session.user,
});
} catch (err) {
console.error(err);
req.flash("error_msg", "Error loading activities data");
res.redirect("/admin/dashboard");
}
};
// Update activity filters
exports.updateFilters = async (req, res) => {
try {
// Accept filters submitted as an array or as an object with numeric keys
let filters = req.body.filters;
// If form submission uses `filters[0]`, `filters[1]` style names, Express/body-parser
// may produce an object with numeric keys rather than a true Array. Normalize it.
if (!filters) {
filters = [];
} else if (!Array.isArray(filters) && typeof filters === 'object') {
try {
filters = Object.keys(filters)
.sort((a, b) => (parseInt(a, 10) || 0) - (parseInt(b, 10) || 0))
.map((k) => filters[k]);
} catch (e) {
filters = [];
}
}
// Sanitize and normalize incoming filters robustly
const sanitizedFilters = [];
try {
const iterable = Array.isArray(filters) ? filters : Object.keys(filters || {}).map((k) => filters[k]);
for (let idx = 0; idx < iterable.length; idx++) {
const filterData = iterable[idx] || {};
// Items can be JSON string, array, or an object with numeric keys
let items = filterData.items;
if (typeof items === 'string') {
try {
items = JSON.parse(items);
} catch (e) {
items = [];
}
}
if (!Array.isArray(items) && typeof items === 'object' && items !== null) {
// convert object with numeric keys to array
try {
items = Object.keys(items)
.sort((a, b) => (parseInt(a, 10) || 0) - (parseInt(b, 10) || 0))
.map((k) => items[k]);
} catch (e) {
items = [];
}
}
if (!Array.isArray(items)) items = [];
const cleanedItems = items
.map((it) => ({ value: (it && it.value) ? it.value.toString().trim() : "", label: (it && it.label) ? it.label.toString().trim() : "" }))
.filter((it) => it.value && it.label);
// normalize id to ObjectId when possible
let subId = filterData._id || filterData.id || undefined;
if (subId && typeof subId === 'string' && /^[0-9a-fA-F]{24}$/.test(subId)) {
try {
subId = mongoose.Types.ObjectId(subId);
} catch (e) {
subId = undefined;
}
} else {
subId = undefined; // don't set invalid ids
}
const label = (filterData.label || '').toString().trim();
const value = (filterData.value || '').toString().trim();
const order = parseInt(filterData.order, 10) || idx + 1;
if (!label || !value) continue; // skip invalid
sanitizedFilters.push({ _id: subId, label, value, items: cleanedItems, order });
}
} catch (e) {
console.error('Error normalizing filters payload:', e);
}
if (!Array.isArray(sanitizedFilters)) {
req.flash('error_msg', 'Invalid filters payload');
return res.redirect('/admin/activity');
}
// Upsert the single filters document in Activities collection
try {
// Provide minimal valid fields when inserting a new filters document so
// schema validators (e.g., age validator) do not fail on upsert.
const setOnInsert = {
name: "_filters_doc",
price: 0,
priceText: "",
season: [],
age: [12, 18],
locations: [],
image: "",
link: "",
program: "",
rating: 4,
isActive: false,
order: 0,
isFiltersDoc: true,
};
const upsertResult = await Activity.findOneAndUpdate(
{ isFiltersDoc: true },
{ $set: { filters: sanitizedFilters }, $setOnInsert: setOnInsert },
{ upsert: true, new: true, setDefaultsOnInsert: true }
);
req.flash('success_msg', 'Filters updated successfully');
return res.redirect('/admin/activity');
} catch (e) {
console.error('Activity upsert filters error:', e);
req.flash('error_msg', `Error saving filters: ${e.message || 'Unknown'}`);
return res.redirect('/admin/activity');
}
} catch (err) {
console.error("Update filters error:", err);
req.flash("error_msg", `Error updating filters: ${err.message || "Unknown error"}`);
res.redirect("/admin/activity");
}
};
// Update global hero section (admin) - updates filters doc and all activities
exports.updateHero = async (req, res) => {
try {
const titleActivities = (req.body.titleActivities || '').toString().trim();
const titleBooking = (req.body.titleBooking || '').toString().trim();
const bannerImageActivities = (req.body.bannerImageActivities || '').toString().trim();
const bannerImageBooking = (req.body.bannerImageBooking || '').toString().trim();
const hero = {
titleActivities: titleActivities || 'Activities',
titleBooking: titleBooking || 'Activities',
bannerImageActivities: bannerImageActivities || '/uploads/banner/b9.jpg',
bannerImageBooking: bannerImageBooking || '/uploads/banner/b9.jpg',
};
// Update all activity docs to keep hero consistent
await Activity.updateMany({ isFiltersDoc: { $ne: true } }, { $set: { hero } });
// Upsert hero into the filters document as well
const setOnInsert = {
name: "_filters_doc",
price: 0,
priceText: "",
season: [],
age: [12, 18],
locations: [],
image: "",
link: "",
program: "",
rating: 4,
isActive: false,
order: 0,
isFiltersDoc: true,
};
await Activity.findOneAndUpdate(
{ isFiltersDoc: true },
{ $set: { hero }, $setOnInsert: setOnInsert },
{ upsert: true, new: true, setDefaultsOnInsert: true }
);
req.flash('success_msg', 'Hero updated successfully');
return res.redirect('/admin/activity');
} catch (e) {
console.error('Update hero error:', e);
req.flash('error_msg', `Error updating hero: ${e.message || 'Unknown'}`);
return res.redirect('/admin/activity');
}
};
// Display create form
exports.createForm = async (req, res) => {
try {
const data = getDefaultActivityData();
res.render("admin/activity/form", {
layout: "layouts/main",
title: "Create Activity",
data,
isEdit: false,
currentPath: req.path,
user: req.session.user,
});
} catch (err) {
console.error(err);
req.flash("error_msg", "Error loading create form");
res.redirect("/admin/activity");
}
};
// Create new activity
exports.create = async (req, res) => {
try {
const activityData = parseActivityFormData(req.body);
const newActivity = new Activity(activityData);
await newActivity.save();
req.flash("success_msg", "Activity created successfully");
res.redirect("/admin/activity");
} catch (err) {
console.error("Create error:", err);
req.flash("error_msg", `Create error: ${err.message || "Unknown"}`);
res.redirect("/admin/activity/create");
}
};
// Display edit form
exports.editForm = async (req, res) => {
try {
const activity = await Activity.findById(req.params.id).lean();
if (!activity) {
req.flash("error_msg", "Activity not found");
return res.redirect("/admin/activity");
}
res.render("admin/activity/form", {
layout: "layouts/main",
title: "Edit Activity",
data: activity,
isEdit: true,
currentPath: req.path,
user: req.session.user,
});
} catch (err) {
console.error(err);
req.flash("error_msg", "Error loading edit form");
res.redirect("/admin/activity");
}
};
// Update activity
exports.update = async (req, res) => {
try {
const activity = await Activity.findById(req.params.id);
if (!activity) {
req.flash("error_msg", "Activity not found");
return res.redirect("/admin/activity");
}
const activityData = parseActivityFormData(req.body, activity);
// Force status to active on update (always set isActive true when editing)
activityData.isActive = true;
await Activity.findByIdAndUpdate(req.params.id, activityData, {new: true});
req.flash("success_msg", "Activity updated successfully");
return req.session.save(() => res.redirect("/admin/activity"));
} catch (err) {
console.error("Update error:", err);
req.flash("error_msg", `Update error: ${err.message || "Unknown"}`);
return req.session.save(() =>
res.redirect(`/admin/activity/${req.params.id}/edit`)
);
}
};
// Delete activity
exports.delete = async (req, res) => {
try {
const activity = await Activity.findById(req.params.id);
if (!activity) {
req.flash("error_msg", "Activity not found");
return res.redirect("/admin/activity");
}
await Activity.findByIdAndDelete(req.params.id);
req.flash("success_msg", "Activity deleted successfully");
res.redirect("/admin/activity");
} catch (err) {
console.error("Delete error:", err);
req.flash("error_msg", `Delete error: ${err.message || "Unknown"}`);
res.redirect("/admin/activity");
}
};
// Toggle activity status (active/inactive)
exports.toggleStatus = async (req, res) => {
try {
const activity = await Activity.findById(req.params.id);
if (!activity) {
return res.status(404).json({error: "Activity not found"});
}
activity.isActive = !activity.isActive;
await activity.save();
// Return updated global counts so front-end widgets can reflect totals
const total = await Activity.countDocuments({ isFiltersDoc: { $ne: true } });
const activeCount = await Activity.countDocuments({ isFiltersDoc: { $ne: true }, isActive: true });
return res.json({
success: true,
isActive: activity.isActive,
message: `Activity ${
activity.isActive ? "activated" : "deactivated"
} successfully`,
total,
activeCount,
});
} catch (err) {
console.error("Toggle status error:", err);
return res.status(500).json({error: "Error toggling activity status"});
}
};
// Update activity order (for drag & drop reordering)
exports.updateOrder = async (req, res) => {
try {
const {items} = req.body; // Array of { id, order }
if (!Array.isArray(items)) {
return res.status(400).json({error: "Invalid data format"});
}
const bulkOps = items.map((item) => ({
updateOne: {
filter: {_id: item.id},
update: {$set: {order: item.order}},
},
}));
await Activity.bulkWrite(bulkOps);
return res.json({success: true, message: "Order updated successfully"});
} catch (err) {
console.error("Update order error:", err);
return res.status(500).json({error: "Error updating order"});
}
};
// Preview activity
exports.preview = async (req, res) => {
try {
const activity = await Activity.findById(req.params.id).lean();
if (!activity) {
return res.status(404).json({error: "Activity not found"});
}
const baseUrl =
process.env.BACKEND_URL || `${req.protocol}://${req.get("host")}`;
const processed = addBaseUrlToImages(activity, baseUrl);
res.json(processed);
} catch (err) {
console.error("Preview error:", err);
res.status(500).json({error: "Error loading preview data"});
}
};
// -------------------- Helper functions --------------------
function parseActivityFormData(body, existingActivity = null) {
// Parse season (can be string or array)
let season = body.season || [];
if (typeof season === "string") {
season = season
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
// Parse age range
let age = [12, 18];
if (body.ageMin && body.ageMax) {
age = [parseInt(body.ageMin) || 12, parseInt(body.ageMax) || 18];
} else if (body.age) {
try {
age = JSON.parse(body.age);
} catch (e) {
// Keep default
}
}
// Parse locations (can be string or array)
let locations = body.locations || [];
if (typeof locations === "string") {
locations = locations
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
// Parse campDetail from form data
let campDetail = {};
try {
if (body.campDetail) {
if (typeof body.campDetail === "string") {
campDetail = JSON.parse(body.campDetail);
} else {
campDetail = body.campDetail;
}
}
// Handle individual campDetail fields if sent separately
// Hero section
if (body.campDetailHeroTitle || body.campDetailHeroBgImage) {
campDetail.hero = campDetail.hero || {};
if (body.campDetailHeroTitle) campDetail.hero.title = body.campDetailHeroTitle.trim();
if (body.campDetailHeroBgImage) campDetail.hero.bgImage = body.campDetailHeroBgImage.trim();
}
// Basic Info section
if (body.campDetailBasicInfoLocation || body.campDetailBasicInfoAgeRange ||
body.campDetailBasicInfoAccommodationType || body.campDetailBasicInfoCareLevel ||
body.campDetailBasicInfoLanguages) {
campDetail.basicInfo = campDetail.basicInfo || {};
if (body.campDetailBasicInfoLocation) campDetail.basicInfo.location = body.campDetailBasicInfoLocation.trim();
if (body.campDetailBasicInfoAgeRange) campDetail.basicInfo.ageRange = body.campDetailBasicInfoAgeRange.trim();
if (body.campDetailBasicInfoAccommodationType) campDetail.basicInfo.accommodationType = body.campDetailBasicInfoAccommodationType.trim();
if (body.campDetailBasicInfoCareLevel) campDetail.basicInfo.careLevel = body.campDetailBasicInfoCareLevel.trim();
if (body.campDetailBasicInfoLanguages) campDetail.basicInfo.languages = body.campDetailBasicInfoLanguages.trim();
}
// Sidebar section
if (body.campDetailSidebarContactPhone || body.campDetailSidebarContactEmail ||
body.campDetailSidebarMenuItems || body.campDetailSidebarUpcomingTours) {
campDetail.sidebar = campDetail.sidebar || {};
// Contact info
if (body.campDetailSidebarContactPhone || body.campDetailSidebarContactEmail) {
campDetail.sidebar.contact = campDetail.sidebar.contact || {};
if (body.campDetailSidebarContactPhone) campDetail.sidebar.contact.phone = body.campDetailSidebarContactPhone.trim();
if (body.campDetailSidebarContactEmail) campDetail.sidebar.contact.email = body.campDetailSidebarContactEmail.trim();
}
// Menu items (JSON array)
if (body.campDetailSidebarMenuItems) {
try {
campDetail.sidebar.menuItems = JSON.parse(body.campDetailSidebarMenuItems);
} catch (e) {
console.warn("Error parsing sidebar menuItems:", e);
}
}
// Upcoming tours (JSON array)
if (body.campDetailSidebarUpcomingTours) {
try {
campDetail.sidebar.upcomingTours = JSON.parse(body.campDetailSidebarUpcomingTours);
} catch (e) {
console.warn("Error parsing sidebar upcomingTours:", e);
}
}
}
// Main Gallery section
if (body.campDetailMainGallerySlides || body.campDetailMainGalleryOverlayLocation ||
body.campDetailMainGalleryOverlaySeason || body.campDetailMainGalleryOverlayLanguages) {
campDetail.mainGallery = campDetail.mainGallery || {};
// Gallery slides (JSON array)
if (body.campDetailMainGallerySlides) {
try {
campDetail.mainGallery.slides = JSON.parse(body.campDetailMainGallerySlides);
} catch (e) {
console.warn("Error parsing mainGallery slides:", e);
}
}
// Overlay info
if (body.campDetailMainGalleryOverlayLocation || body.campDetailMainGalleryOverlaySeason ||
body.campDetailMainGalleryOverlayLanguages) {
campDetail.mainGallery.overlayInfo = campDetail.mainGallery.overlayInfo || {};
if (body.campDetailMainGalleryOverlayLocation) campDetail.mainGallery.overlayInfo.location = body.campDetailMainGalleryOverlayLocation.trim();
if (body.campDetailMainGalleryOverlaySeason) campDetail.mainGallery.overlayInfo.season = body.campDetailMainGalleryOverlaySeason.trim();
if (body.campDetailMainGalleryOverlayLanguages) campDetail.mainGallery.overlayInfo.languages = body.campDetailMainGalleryOverlayLanguages.trim();
}
}
// Event Schedule section
if (body.campDetailEventScheduleStartDate || body.campDetailEventScheduleDuration ||
body.campDetailEventScheduleTickets) {
campDetail.eventSchedule = campDetail.eventSchedule || {};
if (body.campDetailEventScheduleStartDate) campDetail.eventSchedule.startDate = body.campDetailEventScheduleStartDate.trim();
if (body.campDetailEventScheduleDuration) campDetail.eventSchedule.duration = body.campDetailEventScheduleDuration.trim();
if (body.campDetailEventScheduleTickets) campDetail.eventSchedule.tickets = body.campDetailEventScheduleTickets.trim();
}
// Sections - handle both overview individual fields and complete sections JSON
if (body.campDetailSectionsOverviewIntro || body.campDetailSectionsOverviewMainText ||
body.campDetailSectionsOverviewFeatures || body.campDetailSectionsOverviewFeatureImage ||
body.campDetailSections) {
// If complete sections JSON is provided, use it
if (body.campDetailSections) {
try {
campDetail.sections = JSON.parse(body.campDetailSections);
} catch (e) {
console.warn("Error parsing complete sections JSON:", e);
}
}
// Handle individual overview fields (will override sections JSON if both provided)
if (body.campDetailSectionsOverviewIntro || body.campDetailSectionsOverviewMainText ||
body.campDetailSectionsOverviewFeatures || body.campDetailSectionsOverviewFeatureImage) {
campDetail.sections = campDetail.sections || {};
campDetail.sections.overview = campDetail.sections.overview || {};
if (body.campDetailSectionsOverviewIntro) campDetail.sections.overview.intro = body.campDetailSectionsOverviewIntro.trim();
if (body.campDetailSectionsOverviewMainText) campDetail.sections.overview.mainText = body.campDetailSectionsOverviewMainText.trim();
if (body.campDetailSectionsOverviewFeatureImage) campDetail.sections.overview.featureImage = body.campDetailSectionsOverviewFeatureImage.trim();
// Features array
if (body.campDetailSectionsOverviewFeatures) {
try {
campDetail.sections.overview.features = JSON.parse(body.campDetailSectionsOverviewFeatures);
} catch (e) {
console.warn("Error parsing overview features:", e);
}
}
}
}
// Map tipsImage (sidebar) into campDetail.hero.bgImage when provided
if (body.tipsImage && typeof body.tipsImage === 'string' && body.tipsImage.trim()) {
campDetail.hero = campDetail.hero || {};
campDetail.hero.bgImage = body.tipsImage.trim();
}
} catch (e) {
console.warn("Error parsing campDetail:", e);
campDetail = {};
}
// Parse hero section (activities + booking variants)
const existingHero = existingActivity?.hero || {};
const hero = {
titleActivities: body.titleActivities?.trim() || existingHero.titleActivities || "",
titleBooking: body.titleBooking?.trim() || existingHero.titleBooking || "",
bannerImageActivities: body.bannerImageActivities?.trim() || existingHero.bannerImageActivities || "",
bannerImageBooking: body.bannerImageBooking?.trim() || existingHero.bannerImageBooking || "",
};
// Parse bookingSessions
let bookingSessions = [];
try {
if (body.bookingSessions) {
if (typeof body.bookingSessions === "string") {
bookingSessions = JSON.parse(body.bookingSessions);
} else if (Array.isArray(body.bookingSessions)) {
bookingSessions = body.bookingSessions;
} else if (typeof body.bookingSessions === "object") {
bookingSessions = Object.keys(body.bookingSessions)
.sort((a, b) => parseInt(a) - parseInt(b))
.map(k => body.bookingSessions[k]);
}
}
// Validate và clean sessions
bookingSessions = bookingSessions
.filter(s => s && s.startDate && s.endDate)
.map((s, index) => ({
// Auto generate sessionId if not provided
sessionId: s.sessionId?.trim() || `session-${Date.now()}-${index}`,
startDate: new Date(s.startDate),
endDate: new Date(s.endDate),
overnightStays: parseInt(s.overnightStays) || 14,
// Spots theo giới tính
totalMaleSpots: parseInt(s.totalMaleSpots) || 25,
totalFemaleSpots: parseInt(s.totalFemaleSpots) || 25,
bookedMaleSpots: parseInt(s.bookedMaleSpots) || 0,
bookedFemaleSpots: parseInt(s.bookedFemaleSpots) || 0,
price: s.price ? parseFloat(s.price) : null,
isActive: s.isActive === true || s.isActive === "true" || s.isActive === "on"
}));
} catch (e) {
console.warn("Error parsing bookingSessions:", e);
bookingSessions = existingActivity?.bookingSessions || [];
}
// Determine final image value from various input sources
const finalImageValue = (function(){
const img = body.image?.trim() || (body.sidebarImage?.trim() || '') || (body.tipsImage?.trim() || '');
return img || "";
})();
// Đồng bộ campDetail.hero.bgImage với main image - 2 trường này luôn giống nhau
if (finalImageValue && campDetail && campDetail.hero) {
campDetail.hero.bgImage = finalImageValue;
} else if (finalImageValue) {
// Tạo campDetail.hero nếu chưa có và gán bgImage
campDetail = campDetail || {};
campDetail.hero = campDetail.hero || {};
campDetail.hero.bgImage = finalImageValue;
}
return {
hero,
name: body.name?.trim() || "",
price: Math.max(0, parseFloat(body.price) || 0),
priceText: body.priceText?.trim() || `from ${body.price || 0} USD`,
season,
age,
locations,
image: finalImageValue,
link: body.link?.trim() ? (body.link.trim().startsWith('/') ? body.link.trim() : '/' + body.link.trim()) : "",
program: body.program?.trim() || "",
rating: Math.max(1, Math.min(5, parseInt(body.rating) || 4)),
isActive:
body.isActive === "true" ||
body.isActive === true ||
body.isActive === "on" ||
body.isActive === 1,
order: Math.max(0, parseInt(body.order) || 0),
campDetail: campDetail,
bookingSessions: bookingSessions,
};
}
// -------------------- Booking Submissions Management --------------------
// Get booking count for an activity
exports.getBookingCount = async (req, res) => {
try {
const { id } = req.params;
const BookingSubmission = require('../models/bookingSubmission');
let count = await BookingSubmission.countDocuments({ activityId: id });
// Fallback to embedded bookingList in Activity if no separate BookingSubmission docs
if (!count) {
const activity = await Activity.findById(id).lean();
if (activity && Array.isArray(activity.bookingSessions)) {
count = activity.bookingSessions.reduce((sum, s) => {
return sum + (Array.isArray(s.bookingList) ? s.bookingList.length : 0);
}, 0);
}
}
return res.json({ count });
} catch (err) {
console.error("getBookingCount error:", err);
return res.status(500).json({ error: "Error loading booking count" });
}
};
// Get booking submissions for an activity with stats
exports.getBookingSubmissions = async (req, res) => {
try {
const { id } = req.params;
const BookingSubmission = require('../models/bookingSubmission');
// Get activity with sessions
const activity = await Activity.findById(id).lean();
if (!activity) {
return res.status(404).json({ error: "Activity not found" });
}
// Get all booking submissions for this activity (separate collection)
let bookings = await BookingSubmission.find({ activityId: id })
.sort({ createdAt: -1 })
.lean();
// Fallback: if there are no BookingSubmission documents, attempt to read embedded bookingList from Activity.bookingSessions
if ((!bookings || bookings.length === 0) && Array.isArray(activity.bookingSessions)) {
bookings = [];
activity.bookingSessions.forEach((session) => {
if (Array.isArray(session.bookingList)) {
session.bookingList.forEach((b) => {
// normalize embedded booking fields to match BookingSubmission shape
const item = Object.assign({}, b);
item.sessionId = session.sessionId || item.sessionDate || item.sessionId;
item.createdAt = item.bookingDate || item.createdAt || new Date();
// normalize status/payment field names
item.status = item.status || item.bookingStatus || 'pending';
item.paymentStatus = item.paymentStatus || item.paymentStatus || 'pending';
// ensure participantBirthDate is Date
if (item.participantBirthDate && typeof item.participantBirthDate === 'string') {
item.participantBirthDate = new Date(item.participantBirthDate);
}
bookings.push(item);
});
}
});
// sort by createdAt desc
bookings.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}
// Calculate statistics
const stats = {
total: bookings.length,
confirmed: bookings.filter(b => b.status === 'confirmed').length,
pending: bookings.filter(b => b.status === 'pending').length,
cancelled: bookings.filter(b => b.status === 'cancelled').length,
completed: bookings.filter(b => b.status === 'completed').length,
totalRevenue: bookings.filter(b => b.status !== 'cancelled').reduce((sum, b) => sum + (b.totalAmount || 0), 0)
};
// Create session breakdown
const sessionBreakdown = {};
const sessions = activity.bookingSessions || [];
sessions.forEach(session => {
const sessionBookings = bookings.filter(b => b.sessionId === session.sessionId);
const totalCapacity = session.totalMaleSpots + session.totalFemaleSpots;
const bookedCount = sessionBookings.length;
sessionBreakdown[session.sessionId] = {
sessionName: `${new Date(session.startDate).toLocaleDateString()} - ${new Date(session.endDate).toLocaleDateString()}`,
dateRange: `${new Date(session.startDate).toLocaleDateString()} - ${new Date(session.endDate).toLocaleDateString()}`,
totalCapacity,
bookedCount,
bookings: sessionBookings.length
};
});
// Format sessions for filter dropdown
const sessionsForFilter = sessions.map(s => ({
sessionId: s.sessionId,
sessionName: `${new Date(s.startDate).toLocaleDateString()} - ${new Date(s.endDate).toLocaleDateString()}`
}));
return res.json({
bookings,
stats,
sessionBreakdown,
sessions: sessionsForFilter
});
} catch (err) {
console.error("getBookingSubmissions error:", err);
return res.status(500).json({ error: "Error loading booking submissions" });
}
};
// Export booking data as CSV
exports.exportBookingData = async (req, res) => {
try {
const { id } = req.params;
const BookingSubmission = require('../models/bookingSubmission');
const bookings = await BookingSubmission.find({ activityId: id })
.populate('activityId', 'name')
.sort({ createdAt: -1 })
.lean();
if (bookings.length === 0) {
return res.status(404).json({ error: "No bookings found" });
}
// CSV headers
const headers = [
'Date Submitted',
'Activity',
'Session ID',
'Participant Name',
'Participant Gender',
'Participant Birth Date',
'Parent Name',
'Email',
'Phone',
'Address',
'City',
'Country',
'Postal Code',
'Number of Participants',
'Medical Conditions',
'Dietary Restrictions',
'Special Requests',
'Emergency Contact',
'Emergency Phone',
'Status',
'Payment Status',
'Total Amount',
'Paid Amount'
];
// Convert bookings to CSV rows
const rows = bookings.map(booking => [
new Date(booking.createdAt).toISOString().split('T')[0],
booking.activityId?.name || 'Unknown Activity',
booking.sessionId,
`${booking.participantFirstName} ${booking.participantLastName}`,
booking.participantGender,
new Date(booking.participantBirthDate).toISOString().split('T')[0],
`${booking.parentFirstName} ${booking.parentLastName}`,
booking.email,
booking.phone,
booking.address,
booking.city,
booking.country,
booking.postalCode,
booking.numberOfParticipants,
booking.medicalConditions || '',
booking.dietaryRestrictions || 'none',
booking.specialRequests || '',
booking.emergencyContact,
booking.emergencyPhone,
booking.status,
booking.paymentStatus,
booking.totalAmount || 0,
booking.paidAmount || 0
]);
// Generate CSV content
const csvContent = [headers, ...rows]
.map(row => row.map(field => `"${(field || '').toString().replace(/"/g, '""')}"`).join(','))
.join('\n');
// Set response headers for CSV download
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="bookings_${id}_${new Date().toISOString().split('T')[0]}.csv"`);
return res.send(csvContent);
} catch (err) {
console.error("exportBookingData error:", err);
return res.status(500).json({ error: "Error exporting booking data" });
}
};
// Export ALL booking data as CSV (across all activities)
exports.exportAllBookingsData = async (req, res) => {
try {
// Get all activities with booking sessions
const allActivities = await Activity.find({
isFiltersDoc: { $ne: true },
'bookingSessions.bookingList': { $exists: true, $ne: [] }
}).lean();
// Extract all bookings from bookingSessions.bookingList
const allBookings = [];
allActivities.forEach(activity => {
if (activity.bookingSessions && Array.isArray(activity.bookingSessions)) {
activity.bookingSessions.forEach(session => {
if (session.bookingList && Array.isArray(session.bookingList)) {
session.bookingList.forEach(booking => {
const bookingWithActivityInfo = {
...booking,
activityName: activity.name,
sessionId: session.sessionId,
createdAt: booking.createdAt || booking.bookingDate || new Date(),
status: booking.status || booking.bookingStatus || 'pending',
paymentStatus: booking.paymentStatus || 'pending',
totalAmount: booking.totalAmount || 0,
paidAmount: booking.paidAmount || 0
};
allBookings.push(bookingWithActivityInfo);
});
}
});
}
});
if (allBookings.length === 0) {
return res.status(404).json({ error: "No bookings found" });
}
// Sort by creation date (newest first)
allBookings.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
// CSV headers
const headers = [
'Date Submitted',
'Activity',
'Session ID',
'Participant Name',
'Participant Gender',
'Participant Birth Date',
'Parent Name',
'Email',
'Phone',
'Address',
'City',
'Country',
'Postal Code',
'Number of Participants',
'Medical Conditions',
'Dietary Restrictions',
'Special Requests',
'Emergency Contact',
'Emergency Phone',
'Status',
'Payment Status',
'Total Amount',
'Paid Amount'
];
// Convert bookings to CSV rows
const rows = allBookings.map(booking => [
new Date(booking.createdAt).toISOString().split('T')[0],
booking.activityName || 'Unknown Activity',
booking.sessionId,
`${booking.participantFirstName} ${booking.participantLastName}`,
booking.participantGender,
booking.participantBirthDate ? new Date(booking.participantBirthDate).toISOString().split('T')[0] : '',
`${booking.parentFirstName} ${booking.parentLastName}`,
booking.email,
booking.phone,
booking.address,
booking.city,
booking.country,
booking.postalCode,
booking.numberOfParticipants,
booking.medicalConditions || '',
booking.dietaryRestrictions || 'none',
booking.specialRequests || '',
booking.emergencyContact,
booking.emergencyPhone,
booking.status,
booking.paymentStatus,
booking.totalAmount || 0,
booking.paidAmount || 0
]);
// Generate CSV content
const csvContent = [headers, ...rows]
.map(row => row.map(field => `"${(field || '').toString().replace(/"/g, '""')}"`).join(','))
.join('\n');
// Set response headers for CSV download
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="all_bookings_${new Date().toISOString().split('T')[0]}.csv"`);
return res.send(csvContent);
} catch (err) {
console.error("exportAllBookingsData error:", err);
return res.status(500).json({ error: "Error exporting all booking data" });
}
};
// Delete a booking submission
exports.deleteBookingSubmission = async (req, res) => {
try {
const { bookingId } = req.params;
const BookingSubmission = require('../models/bookingSubmission');
const booking = await BookingSubmission.findById(bookingId);
if (!booking) {
return res.status(404).json({ error: "Booking not found" });
}
await BookingSubmission.findByIdAndDelete(bookingId);
return res.json({ message: "Booking deleted successfully" });
} catch (err) {
console.error("deleteBookingSubmission error:", err);
return res.status(500).json({ error: "Error deleting booking" });
}
};
// -------------------- Camp Session Booking Management --------------------
// Create a new booking directly into camp session
exports.createSessionBooking = async (req, res) => {
try {
const { activityId, sessionId } = req.params;
const bookingData = req.body;
// Validate required fields
const requiredFields = [
'address', 'agreeTerms', 'city', 'country', 'email', 'emergencyContact',
'emergencyPhone', 'numberOfParticipants', 'parentFirstName', 'parentLastName',
'participantBirthDate', 'participantFirstName', 'participantGender',
'participantLastName', 'phone', 'postalCode'
];
for (let field of requiredFields) {
if (!bookingData[field]) {
return res.status(400).json({
error: `Missing required field: ${field}`
});
}
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(bookingData.email)) {
return res.status(400).json({ error: "Invalid email format" });
}
// Find the activity
const activity = await Activity.findById(activityId);
if (!activity) {
return res.status(404).json({ error: "Activity not found" });
}
// Find the specific session
const sessionIndex = activity.bookingSessions.findIndex(s => s.sessionId === sessionId);
if (sessionIndex === -1) {
return res.status(404).json({ error: "Session not found" });
}
const session = activity.bookingSessions[sessionIndex];
// Check if session is active
if (!session.isActive) {
return res.status(400).json({ error: "Session is not active for booking" });
}
// Check availability based on participant gender
const participantGender = bookingData.participantGender;
const numberOfParticipants = parseInt(bookingData.numberOfParticipants) || 1;
let availableSpots = 0;
if (participantGender === 'male') {
availableSpots = session.totalMaleSpots - session.bookedMaleSpots;
} else if (participantGender === 'female') {
availableSpots = session.totalFemaleSpots - session.bookedFemaleSpots;
} else {
// For 'other' gender, check both male and female availability
const maleAvailable = session.totalMaleSpots - session.bookedMaleSpots;
const femaleAvailable = session.totalFemaleSpots - session.bookedFemaleSpots;
availableSpots = Math.max(maleAvailable, femaleAvailable);
}
if (availableSpots < numberOfParticipants) {
return res.status(400).json({
error: `Not enough spots available. Only ${availableSpots} spots left for ${participantGender} participants.`,
availableSpots
});
}
// Generate unique confirmation code
const confirmationCode = `GG${Date.now()}${Math.random().toString(36).substr(2, 5).toUpperCase()}`;
// Calculate total amount
const pricePerParticipant = session.price || activity.price || 0;
const totalAmount = pricePerParticipant * numberOfParticipants;
// Create booking object
const newBooking = {
address: bookingData.address.trim(),
agreeNewsletter: bookingData.agreeNewsletter === true || bookingData.agreeNewsletter === 'true',
agreeTerms: bookingData.agreeTerms === true || bookingData.agreeTerms === 'true',
city: bookingData.city.trim(),
country: bookingData.country.trim(),
dietaryRestrictions: bookingData.dietaryRestrictions || 'none',
email: bookingData.email.toLowerCase().trim(),
emergencyContact: bookingData.emergencyContact.trim(),
emergencyPhone: bookingData.emergencyPhone.trim(),
medicalConditions: bookingData.medicalConditions || '',
numberOfParticipants: numberOfParticipants,
parentFirstName: bookingData.parentFirstName.trim(),
parentLastName: bookingData.parentLastName.trim(),
participantBirthDate: new Date(bookingData.participantBirthDate),
participantFirstName: bookingData.participantFirstName.trim(),
participantGender: participantGender,
participantLastName: bookingData.participantLastName.trim(),
phone: bookingData.phone.trim(),
postalCode: bookingData.postalCode.trim(),
sessionDate: sessionId,
specialRequests: bookingData.specialRequests || '',
bookingStatus: 'pending',
paymentStatus: 'pending',
totalAmount: totalAmount,
paidAmount: 0,
bookingDate: new Date(),
confirmationCode: confirmationCode,
adminNotes: ''
};
// Add booking to session
if (!activity.bookingSessions[sessionIndex].bookingList) {
activity.bookingSessions[sessionIndex].bookingList = [];
}
activity.bookingSessions[sessionIndex].bookingList.push(newBooking);
// Update booked spots count
if (participantGender === 'male') {
activity.bookingSessions[sessionIndex].bookedMaleSpots += numberOfParticipants;
} else if (participantGender === 'female') {
activity.bookingSessions[sessionIndex].bookedFemaleSpots += numberOfParticipants;
} else {
// For 'other' gender, distribute to the gender with more availability
const maleAvailable = session.totalMaleSpots - session.bookedMaleSpots;
const femaleAvailable = session.totalFemaleSpots - session.bookedFemaleSpots;
if (maleAvailable >= femaleAvailable) {
activity.bookingSessions[sessionIndex].bookedMaleSpots += numberOfParticipants;
} else {
activity.bookingSessions[sessionIndex].bookedFemaleSpots += numberOfParticipants;
}
}
// Save the updated activity
await activity.save();
// Return success response with booking details
return res.status(201).json({
message: "Booking created successfully",
booking: {
id: activity.bookingSessions[sessionIndex].bookingList[activity.bookingSessions[sessionIndex].bookingList.length - 1]._id,
confirmationCode: confirmationCode,
activityName: activity.name,
sessionId: sessionId,
participantName: `${bookingData.participantFirstName} ${bookingData.participantLastName}`,
parentName: `${bookingData.parentFirstName} ${bookingData.parentLastName}`,
email: bookingData.email,
totalAmount: totalAmount,
numberOfParticipants: numberOfParticipants,
status: 'pending',
sessionDetails: {
startDate: session.startDate,
endDate: session.endDate,
overnightStays: session.overnightStays
}
}
});
} catch (err) {
console.error("createSessionBooking error:", err);
return res.status(500).json({ error: "Error creating booking" });
}
};
// Create booking by program (wrapper) - find activity by `program` then delegate
exports.createSessionBookingByProgram = async (req, res) => {
try {
const { program, sessionId } = req.params;
// Find activity by program field
const activity = await Activity.findOne({ program: program });
if (!activity) return res.status(404).json({ error: 'Activity not found for program: ' + program });
// Inject activityId into params and call existing handler
req.params.activityId = activity._id.toString();
req.params.sessionId = sessionId;
return await exports.createSessionBooking(req, res);
} catch (err) {
console.error('createSessionBookingByProgram error:', err);
return res.status(500).json({ error: 'Error creating booking by program' });
}
};
// Get all bookings for a specific session
exports.getSessionBookings = async (req, res) => {
try {
const { activityId, sessionId } = req.params;
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const status = req.query.status;
const search = req.query.search;
// Find the activity
const activity = await Activity.findById(activityId);
if (!activity) {
return res.status(404).json({ error: "Activity not found" });
}
// Find the specific session
const session = activity.bookingSessions.find(s => s.sessionId === sessionId);
if (!session) {
return res.status(404).json({ error: "Session not found" });
}
let bookings = session.bookingList || [];
// Apply filters
if (status) {
bookings = bookings.filter(b => b.bookingStatus === status);
}
if (search) {
const searchLower = search.toLowerCase();
bookings = bookings.filter(b =>
b.participantFirstName.toLowerCase().includes(searchLower) ||
b.participantLastName.toLowerCase().includes(searchLower) ||
b.parentFirstName.toLowerCase().includes(searchLower) ||
b.parentLastName.toLowerCase().includes(searchLower) ||
b.email.toLowerCase().includes(searchLower) ||
b.confirmationCode.toLowerCase().includes(searchLower)
);
}
// Calculate pagination
const totalBookings = bookings.length;
const totalPages = Math.ceil(totalBookings / limit);
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const paginatedBookings = bookings.slice(startIndex, endIndex);
// Calculate statistics
const stats = {
total: session.bookingList?.length || 0,
pending: bookings.filter(b => b.bookingStatus === 'pending').length,
confirmed: bookings.filter(b => b.bookingStatus === 'confirmed').length,
cancelled: bookings.filter(b => b.bookingStatus === 'cancelled').length,
completed: bookings.filter(b => b.bookingStatus === 'completed').length,
totalRevenue: bookings.filter(b => b.bookingStatus !== 'cancelled').reduce((sum, b) => sum + b.totalAmount, 0),
paidAmount: bookings.reduce((sum, b) => sum + b.paidAmount, 0)
};
return res.json({
bookings: paginatedBookings,
pagination: {
currentPage: page,
totalPages: totalPages,
totalBookings: totalBookings,
limit: limit
},
session: {
sessionId: session.sessionId,
startDate: session.startDate,
endDate: session.endDate,
totalMaleSpots: session.totalMaleSpots,
totalFemaleSpots: session.totalFemaleSpots,
bookedMaleSpots: session.bookedMaleSpots,
bookedFemaleSpots: session.bookedFemaleSpots,
isActive: session.isActive
},
stats: stats,
activity: {
id: activity._id,
name: activity.name,
price: activity.price
}
});
} catch (err) {
console.error("getSessionBookings error:", err);
return res.status(500).json({ error: "Error retrieving session bookings" });
}
};
// Get session bookings by program (wrapper)
exports.getSessionBookingsByProgram = async (req, res) => {
try {
const { program, sessionId } = req.params;
const activity = await Activity.findOne({ program: program });
if (!activity) return res.status(404).json({ error: 'Activity not found for program: ' + program });
req.params.activityId = activity._id.toString();
req.params.sessionId = sessionId;
return await exports.getSessionBookings(req, res);
} catch (err) {
console.error('getSessionBookingsByProgram error:', err);
return res.status(500).json({ error: 'Error retrieving session bookings by program' });
}
};
// Update a specific booking in a session
exports.updateSessionBooking = async (req, res) => {
try {
const { activityId, sessionId, bookingId } = req.params;
const updateData = req.body;
// Find the activity
const activity = await Activity.findById(activityId);
if (!activity) {
return res.status(404).json({ error: "Activity not found" });
}
// Find the specific session
const sessionIndex = activity.bookingSessions.findIndex(s => s.sessionId === sessionId);
if (sessionIndex === -1) {
return res.status(404).json({ error: "Session not found" });
}
// Find the specific booking
const bookingIndex = activity.bookingSessions[sessionIndex].bookingList.findIndex(
b => b._id.toString() === bookingId
);
if (bookingIndex === -1) {
return res.status(404).json({ error: "Booking not found" });
}
const currentBooking = activity.bookingSessions[sessionIndex].bookingList[bookingIndex];
// Update allowed fields
const allowedUpdates = [
'bookingStatus', 'paymentStatus', 'paidAmount', 'adminNotes',
'emergencyContact', 'emergencyPhone', 'medicalConditions',
'specialRequests', 'dietaryRestrictions'
];
for (let field of allowedUpdates) {
if (updateData[field] !== undefined) {
activity.bookingSessions[sessionIndex].bookingList[bookingIndex][field] = updateData[field];
}
}
// Handle status changes that might affect spot counts
if (updateData.bookingStatus && updateData.bookingStatus !== currentBooking.bookingStatus) {
const numberOfParticipants = currentBooking.numberOfParticipants;
const participantGender = currentBooking.participantGender;
// If booking is being cancelled, free up spots
if (updateData.bookingStatus === 'cancelled' && currentBooking.bookingStatus !== 'cancelled') {
if (participantGender === 'male') {
activity.bookingSessions[sessionIndex].bookedMaleSpots = Math.max(0,
activity.bookingSessions[sessionIndex].bookedMaleSpots - numberOfParticipants);
} else if (participantGender === 'female') {
activity.bookingSessions[sessionIndex].bookedFemaleSpots = Math.max(0,
activity.bookingSessions[sessionIndex].bookedFemaleSpots - numberOfParticipants);
}
}
// If booking is being restored from cancelled, book spots again
if (currentBooking.bookingStatus === 'cancelled' && updateData.bookingStatus !== 'cancelled') {
if (participantGender === 'male') {
const totalMale = activity.bookingSessions[sessionIndex].totalMaleSpots;
const currentMale = activity.bookingSessions[sessionIndex].bookedMaleSpots;
if (currentMale + numberOfParticipants > totalMale) {
return res.status(400).json({
error: "Not enough male spots available to restore this booking",
availableSpots: totalMale - currentMale
});
}
activity.bookingSessions[sessionIndex].bookedMaleSpots += numberOfParticipants;
} else if (participantGender === 'female') {
const totalFemale = activity.bookingSessions[sessionIndex].totalFemaleSpots;
const currentFemale = activity.bookingSessions[sessionIndex].bookedFemaleSpots;
if (currentFemale + numberOfParticipants > totalFemale) {
return res.status(400).json({
error: "Not enough female spots available to restore this booking",
availableSpots: totalFemale - currentFemale
});
}
activity.bookingSessions[sessionIndex].bookedFemaleSpots += numberOfParticipants;
}
}
}
// Save the updated activity
await activity.save();
return res.json({
message: "Booking updated successfully",
booking: activity.bookingSessions[sessionIndex].bookingList[bookingIndex]
});
} catch (err) {
console.error("updateSessionBooking error:", err);
return res.status(500).json({ error: "Error updating booking" });
}
};
// Delete a specific booking from a session
exports.deleteSessionBooking = async (req, res) => {
try {
const { activityId, sessionId, bookingId } = req.params;
// Find the activity
const activity = await Activity.findById(activityId);
if (!activity) {
return res.status(404).json({ error: "Activity not found" });
}
// Find the specific session
const sessionIndex = activity.bookingSessions.findIndex(s => s.sessionId === sessionId);
if (sessionIndex === -1) {
return res.status(404).json({ error: "Session not found" });
}
// Find the specific booking
const bookingIndex = activity.bookingSessions[sessionIndex].bookingList.findIndex(
b => b._id.toString() === bookingId
);
if (bookingIndex === -1) {
return res.status(404).json({ error: "Booking not found" });
}
const bookingToDelete = activity.bookingSessions[sessionIndex].bookingList[bookingIndex];
// Free up spots if booking is not cancelled
if (bookingToDelete.bookingStatus !== 'cancelled') {
const numberOfParticipants = bookingToDelete.numberOfParticipants;
const participantGender = bookingToDelete.participantGender;
if (participantGender === 'male') {
activity.bookingSessions[sessionIndex].bookedMaleSpots = Math.max(0,
activity.bookingSessions[sessionIndex].bookedMaleSpots - numberOfParticipants);
} else if (participantGender === 'female') {
activity.bookingSessions[sessionIndex].bookedFemaleSpots = Math.max(0,
activity.bookingSessions[sessionIndex].bookedFemaleSpots - numberOfParticipants);
}
}
// Remove the booking from the array
activity.bookingSessions[sessionIndex].bookingList.splice(bookingIndex, 1);
// Save the updated activity
await activity.save();
return res.json({
message: "Booking deleted successfully",
deletedBooking: {
id: bookingId,
confirmationCode: bookingToDelete.confirmationCode,
participantName: `${bookingToDelete.participantFirstName} ${bookingToDelete.participantLastName}`
}
});
} catch (err) {
console.error("deleteSessionBooking error:", err);
return res.status(500).json({ error: "Error deleting booking" });
}
};