forked from UKSOURCE/cms.hailearning.edu.vn
558 lines
19 KiB
JavaScript
558 lines
19 KiB
JavaScript
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'
|
|
});
|
|
}
|
|
}; |