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

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'
});
}
};