diff --git a/admin-frontend/src/App.js b/admin-frontend/src/App.js index d5c6dd9..4718783 100644 --- a/admin-frontend/src/App.js +++ b/admin-frontend/src/App.js @@ -23,7 +23,7 @@ import AdminNotificationPage from "./components/notification/AdminNotificationPa import VerifyEmailPage from "./components/VerifyEmailPage"; import ViewActiveBookingsPage from "./components/booking/ViewActiveBookingsPage"; import ViewPastBookingsPage from "./components/booking/ViewPastBookingsPage"; -import ActivityThemesPage from "./components/activitythemes/ActivityThemesPage"; +import ActivityThemesPage from "./components/activitytheme/ActivityThemesPage"; function App() { return ( diff --git a/admin-frontend/src/components/activityThemes/ActivityThemesPage.jsx b/admin-frontend/src/components/activitytheme/ActivityThemesPage.jsx similarity index 100% rename from admin-frontend/src/components/activityThemes/ActivityThemesPage.jsx rename to admin-frontend/src/components/activitytheme/ActivityThemesPage.jsx diff --git a/admin-frontend/src/components/activityThemes/ActivityThemesTable.jsx b/admin-frontend/src/components/activitytheme/ActivityThemesTable.jsx similarity index 100% rename from admin-frontend/src/components/activityThemes/ActivityThemesTable.jsx rename to admin-frontend/src/components/activitytheme/ActivityThemesTable.jsx diff --git a/admin-frontend/src/components/activityThemes/AddThemeModal.jsx b/admin-frontend/src/components/activitytheme/AddThemeModal.jsx similarity index 100% rename from admin-frontend/src/components/activityThemes/AddThemeModal.jsx rename to admin-frontend/src/components/activitytheme/AddThemeModal.jsx diff --git a/admin-frontend/src/components/activityThemes/EditThemeModal.jsx b/admin-frontend/src/components/activitytheme/EditThemeModal.jsx similarity index 100% rename from admin-frontend/src/components/activityThemes/EditThemeModal.jsx rename to admin-frontend/src/components/activitytheme/EditThemeModal.jsx diff --git a/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx b/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx index 2f04087..3993f4e 100644 --- a/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx +++ b/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx @@ -160,7 +160,7 @@ const ActivityDetailsPage = () => { const calculateWeekendAddOn = ( selectedDate, weekendPricing, - totalBasePrice, + totalBasePrice ) => { if ( weekendPricing.amount !== null && @@ -175,7 +175,7 @@ const ActivityDetailsPage = () => { return 0; }; - const calculateOnlineAddOn = (location, onlinePricing, totalBasePrice) => { + const calculateOfflineAddOn = (location, onlinePricing, totalBasePrice) => { if ( onlinePricing.amount !== null && (location.toLowerCase().includes("off-site") || @@ -190,7 +190,7 @@ const ActivityDetailsPage = () => { return 0; }; - const calculateOfflineAddOn = (location, offlinePricing, totalBasePrice) => { + const calculateOnlineAddOn = (location, offlinePricing, totalBasePrice) => { if ( offlinePricing.amount !== null && location.toLowerCase().includes("virtual") @@ -211,16 +211,19 @@ const ActivityDetailsPage = () => { const weekendAddOn = calculateWeekendAddOn( selectedDate, currentActivity.weekendPricing, + totalBasePrice ); - const onlineAddOn = calculateOnlineAddOn( + const offlineAddOn = calculateOfflineAddOn( location, currentActivity.offlinePricing, + totalBasePrice ); - const offlineAddOn = calculateOfflineAddOn( + const onlineAddOn = calculateOnlineAddOn( location, currentActivity.onlinePricing, + totalBasePrice ); const totalPriceCalculated = @@ -241,15 +244,24 @@ const ActivityDetailsPage = () => { const weekendAddOn = calculateWeekendAddOn( selectedDate, currentActivity.weekendPricing, + totalBasePrice ); - const onlineAddOn = calculateOnlineAddOn( + const offlineAddOn = calculateOfflineAddOn( location, currentActivity.offlinePricing, + totalBasePrice ); - const offlineAddOn = calculateOfflineAddOn( + const onlineAddOn = calculateOnlineAddOn( location, currentActivity.onlinePricing, + totalBasePrice ); + let activityPricingRule; + for (const pricingRule of currentActivity?.activityPricingRules) { + if (pax >= pricingRule.start && pax <= pricingRule.end) { + activityPricingRule = pricingRule; + } + } const timeParts = time.split(","); const cartItem = { activityId: currentActivity._id, @@ -262,6 +274,7 @@ const ActivityDetailsPage = () => { offlineAddOnCost: offlineAddOn, startDateTime: timeParts[0], endDateTime: timeParts[1], + activityPricingRule: activityPricingRule._id, }; if ( cartItem.activityId !== null && @@ -435,7 +448,7 @@ const ActivityDetailsPage = () => { format="DD/MM/YYYY" minDate={dayjs().add( currentActivity?.bookingNotice, - "days", + "days" )} shouldDisableDate={shouldDisableDate} sx={{ marginRight: "12px" }} @@ -593,8 +606,9 @@ const ActivityDetailsPage = () => { ? "-" : ""} {currentActivity?.weekendPricing?.amount?.toFixed( - 2, + 2 )} + % @@ -615,8 +629,9 @@ const ActivityDetailsPage = () => { : "+"} {""} {currentActivity?.offlinePricing?.amount?.toFixed( - 2, - )} + 2 + )}{" "} + % @@ -636,8 +651,9 @@ const ActivityDetailsPage = () => { : "+"} {""} {currentActivity?.onlinePricing?.amount?.toFixed( - 2, + 2 )} + % diff --git a/client-frontend/src/containers/Vendor/Booking/BookingDetailsForm.jsx b/client-frontend/src/containers/Vendor/Booking/BookingDetailsForm.jsx index 97242eb..e44b31f 100644 --- a/client-frontend/src/containers/Vendor/Booking/BookingDetailsForm.jsx +++ b/client-frontend/src/containers/Vendor/Booking/BookingDetailsForm.jsx @@ -5,10 +5,18 @@ import PaidIcon from "@mui/icons-material/Paid"; import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt"; import { Grid, + InputAdornment, List, ListItem, ListItemIcon, ListItemText, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, TextField, Typography, useTheme, @@ -39,6 +47,36 @@ const BookingDetailsForm = ({ appointmentData }) => { PENDING_PAYMENT: "Updated to Pending Payment", PAID: "Updated to Paid", }; + const totalPrice = () => { + return ( + appointmentData?.totalPax * + appointmentData?.activityPricingRule?.pricePerPax + ); + }; + + const addons = { + weekendPricing: { + name: "Weekend Pricing", + vendor: "vendorWeekendAddOnCost", + }, + onlinePricing: { name: "Online Pricing", vendor: "vendorOnlineAddOnCost" }, + offlinePricing: { + name: "Offline Pricing", + vendor: "vendorOfflineAddOnCost", + }, + }; + + const addOnNaming = (addon) => { + return addons[addon]?.name; + }; + + const vendorNaming = (addon) => { + return addons[addon]?.vendor; + }; + + const calculateTotal = () => { + return; + }; return ( { fullWidth /> + + + + + + + Num. pax + + + Price per Pax + + + Total Price + + + + + + + + {appointmentData?.totalPax} pax + + + + + ${appointmentData?.activityPricingRule?.pricePerPax} + + + + ${totalPrice()} + + + + {appointmentData?.activityId && + Object.keys(appointmentData?.activityId)?.map( + (addon, index) => { + return ( + appointmentData[vendorNaming(addon)] != 0 && + !isNaN(appointmentData[vendorNaming(addon)]) && + appointmentData?.activityId[addon].amount && ( + + + + + {addOnNaming(addon)} + {appointmentData?.activityId[addon].isDiscount + ? " Discount" + : " Add-on"} + + + + + {appointmentData?.activityId[addon].isDiscount + ? "-" + : "+"} + {appointmentData?.activityId[addon].amount}%{" "} + + ( + {appointmentData?.activityId[addon].isDiscount + ? "-" + : "+"}{" "} + $ + {Math.abs(appointmentData[vendorNaming(addon)])} + ) + + + + + ) + ); + } + )} + + + + Total Price + + + + ${appointmentData?.totalVendorAmount} + + + + +
+
+
Status Changelog diff --git a/server/controller/cartItemController.js b/server/controller/cartItemController.js index a1e88c9..5099ed6 100644 --- a/server/controller/cartItemController.js +++ b/server/controller/cartItemController.js @@ -8,6 +8,7 @@ import { getTimeslotAvailability, generateAllTimeslots, } from "./bookingController.js"; +import ActivityPricingRulesModel from "../model/activityPricingRules.js"; export const addCartItem = async (req, res) => { const errors = validationResult(req); @@ -49,6 +50,10 @@ export const addCartItem = async (req, res) => { }); } + const activityPricingRule = await ActivityPricingRulesModel.findById( + restBody.activityPricingRule + ); + // Get activity title, vendor name and vendorId const activityTitle = activity.title; const vendorName = activity.linkedVendor.companyName; @@ -61,6 +66,30 @@ export const addCartItem = async (req, res) => { onlineAddOnCost + offlineAddOnCost; + const vendorCost = activityPricingRule.pricePerPax * totalPax; + + const vendorWeekendAddOnCost = + weekendAddOnCost != 0 + ? ((vendorCost * activity.weekendPricing.amount) / 100) * + (activity.weekendPricing.isDiscount ? -1 : 1) + : 0; + + const vendorOnlineAddOnCost = + onlineAddOnCost != 0 + ? ((vendorCost * activity.onlinePricing.amount) / 100) * + (activity.onlinePricing.isDiscount ? -1 : 1) + : 0; + const vendorOfflineAddOnCost = + offlineAddOnCost != 0 + ? ((vendorCost * activity.offlinePricing.amount) / 100) * + (activity.offlinePricing.isDiscount ? -1 : 1) + : 0; + const totalVendorAmount = + vendorCost + + vendorWeekendAddOnCost + + vendorOnlineAddOnCost + + vendorOfflineAddOnCost; + const newCartItem = new CartItemModel({ clientId: client.id, activityId, @@ -73,6 +102,10 @@ export const addCartItem = async (req, res) => { activityTitle, vendorName, vendorId, + totalVendorAmount, + vendorOfflineAddOnCost, + vendorOnlineAddOnCost, + vendorWeekendAddOnCost, ...restBody, }); @@ -104,7 +137,9 @@ export const getCartItemsByClientId = async (req, res) => { try { const cartItems = await CartItemModel.find({ clientId: client._id, - }); + }).select( + "-vendorWeekendAddOnCost -vendorOnlineAddOnCost -vendorOfflineAddOnCost -totalVendorAmount" + ); // for each cartItem, check if the activity is still available for (const cartItem of cartItems) { const activity = await ActivityModel.findById(cartItem.activityId); @@ -113,10 +148,10 @@ export const getCartItemsByClientId = async (req, res) => { const updatedCartItems = await Promise.all( cartItems.map(async (cartItem) => { const isTimeslotAvailable = await isCartItemStillAvailable( - cartItem._id, + cartItem._id ); return { cartItem, isItemStillAvailable: isTimeslotAvailable }; // Add the 'isAvailable' field to the updated cart items - }), + }) ); res.status(200).json(updatedCartItems); @@ -175,14 +210,14 @@ export async function isCartItemStillAvailable(cartItemId) { activity.startTime.getHours(), activity.startTime.getMinutes(), 0, - 0, + 0 ); const latestStartTime = new Date(cartItem.startDateTime); latestStartTime.setHours( activity.endTime.getHours(), activity.endTime.getMinutes(), 0, - 0, + 0 ); const interval = 30; // 30 minutes @@ -218,14 +253,14 @@ export async function isCartItemStillAvailable(cartItemId) { activity.capacity, bookings, blockedTimeslots, - activity.duration, + activity.duration ); // use isTimeslotAvailable const isTimeslotAvailable = await getTimeslotAvailability( allTimeslots, cartItem.startDateTime, - cartItem.endDateTime, + cartItem.endDateTime ); return isTimeslotAvailable; } diff --git a/server/model/bookingModel.js b/server/model/bookingModel.js index 35b4180..fe29307 100644 --- a/server/model/bookingModel.js +++ b/server/model/bookingModel.js @@ -31,6 +31,10 @@ const bookingSchema = new mongoose.Schema({ type: Number, required: true, }, + totalVendorAmount: { + type: Number, + required: true, + }, totalPax: { type: Number, required: true, @@ -51,6 +55,15 @@ const bookingSchema = new mongoose.Schema({ type: Number, required: true, }, + vendorWeekendAddOnCost: { + type: Number, + }, + vendorOnlineAddOnCost: { + type: Number, + }, + vendorOfflineAddOnCost: { + type: Number, + }, activityTitle: { type: String, required: true, @@ -130,6 +143,11 @@ const bookingSchema = new mongoose.Schema({ }, }, ], + activityPricingRule: { + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "ActivityPricingRules", + }, }); const BookingModel = mongoose.model("Booking", bookingSchema, "bookings"); diff --git a/server/model/cartItemModel.js b/server/model/cartItemModel.js index 2656a61..3b022f0 100644 --- a/server/model/cartItemModel.js +++ b/server/model/cartItemModel.js @@ -29,6 +29,10 @@ const cartItemSchema = new mongoose.Schema({ type: Number, required: true, }, + totalVendorAmount: { + type: Number, + required: true, + }, totalPax: { type: Number, required: true, @@ -49,6 +53,15 @@ const cartItemSchema = new mongoose.Schema({ type: Number, required: true, }, + vendorWeekendAddOnCost: { + type: Number, + }, + vendorOnlineAddOnCost: { + type: Number, + }, + vendorOfflineAddOnCost: { + type: Number, + }, activityTitle: { type: String, required: true, @@ -68,6 +81,11 @@ const cartItemSchema = new mongoose.Schema({ preSignedImages: { type: Array, }, + activityPricingRule: { + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "ActivityPricingRules", + }, }); const cartItemModel = mongoose.model("CartItem", cartItemSchema, "cartitems"); diff --git a/server/service/bookingService.js b/server/service/bookingService.js index a245959..26274fa 100644 --- a/server/service/bookingService.js +++ b/server/service/bookingService.js @@ -3,10 +3,22 @@ import BookingModel from "../model/bookingModel.js"; export const getAllBookingsForVendor = async (vendorId) => { return await BookingModel.find({ vendorId: vendorId, - }).populate({ - path: "clientId", - select: "-password", - }); + }) + .select( + "-weekendAddOnCost -onlineAddOnCost -offlineAddOnCost -basePricePerPax -totalCost" + ) + .populate({ + path: "clientId", + select: "-password", + }) + .populate({ + path: "activityPricingRule", + select: "-clientPrice", + }) + .populate({ + path: "activityId", + select: "weekendPricing onlinePricing offlinePricing", + }); }; export const updateBookingStatusActionHistory = async ( bookingId,