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