From abda513d0461d4b7a5424641218862edabf939a8 Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Thu, 12 Oct 2023 18:29:34 +0800 Subject: [PATCH 01/11] Added PDF Generation --- admin-frontend/src/App.js | 149 +- client-frontend/package-lock.json | 11 + client-frontend/package.json | 1 + .../ActivityDetailsPage.jsx | 2077 +++++++++-------- client-frontend/src/zustand/ActivityStore.js | 246 +- client-frontend/src/zustand/ShopStore.js | 324 +-- server/assets/templates/InvoiceTemplate.js | 53 +- server/controller/activityController.js | 1927 +++++++-------- server/routes/gleek/shop.js | 10 +- server/server.js | 23 - 10 files changed, 2473 insertions(+), 2348 deletions(-) diff --git a/admin-frontend/src/App.js b/admin-frontend/src/App.js index a345be0..6f900e7 100644 --- a/admin-frontend/src/App.js +++ b/admin-frontend/src/App.js @@ -94,86 +94,85 @@ function App() { } /> */} - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> - - /> + } /> } /> diff --git a/client-frontend/package-lock.json b/client-frontend/package-lock.json index 71d4854..19a8d3e 100644 --- a/client-frontend/package-lock.json +++ b/client-frontend/package-lock.json @@ -20,6 +20,7 @@ "axios": "^1.5.0", "date-holidays": "^3.22.1", "dayjs": "^1.11.10", + "downloadjs": "^1.4.7", "react": "^18.2.0", "react-autosuggest": "^10.1.0", "react-dom": "^18.2.0", @@ -7740,6 +7741,11 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, + "node_modules/downloadjs": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", + "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==" + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -24019,6 +24025,11 @@ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, + "downloadjs": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", + "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==" + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", diff --git a/client-frontend/package.json b/client-frontend/package.json index ba35a05..a83180d 100644 --- a/client-frontend/package.json +++ b/client-frontend/package.json @@ -15,6 +15,7 @@ "axios": "^1.5.0", "date-holidays": "^3.22.1", "dayjs": "^1.11.10", + "downloadjs": "^1.4.7", "react": "^18.2.0", "react-autosuggest": "^10.1.0", "react-dom": "^18.2.0", diff --git a/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx b/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx index 488b928..f90b995 100644 --- a/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx +++ b/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx @@ -1,17 +1,17 @@ import { - Box, - Button, - CircularProgress, - Divider, - Grid, - TextField, - Typography, - FormControl, - FormHelperText, - Select, - MenuItem, - InputLabel, - Modal, + Box, + Button, + CircularProgress, + Divider, + Grid, + TextField, + Typography, + FormControl, + FormHelperText, + Select, + MenuItem, + InputLabel, + Modal, } from "@mui/material"; import { lighten, useTheme } from "@mui/material/styles"; import React, { useEffect, useState } from "react"; @@ -46,1048 +46,1109 @@ import "./styles.css"; import Holidays from "date-holidays"; const ActivityDetailsPage = () => { - const { - currentActivity, - getCurrentActivity, - currentActivityLoading, - getTimeSlots, - timeSlotsLoading, - timeSlots, - } = useShopStore(); - const { addToCart, addToCartLoading } = useCartStore(); - const { activityId } = useParams(); - const theme = useTheme(); - const tertiaryLighter = lighten(theme.palette.tertiary.main, 0.4); - const accent = theme.palette.accent.main; - const primary = theme.palette.primary.main; - const tertiary = theme.palette.tertiary.main; - const [selectedDate, setSelectedDate] = useState(null); - const [pax, setPax] = useState(""); - const [time, setTime] = useState(""); - const [location, setLocation] = useState(""); - const hd = new Holidays("SG"); - const [isModalOpen, setIsModalOpen] = useState(false); - const [comments, setComments] = useState(""); - const { openSnackbar } = useSnackbarStore(); + const { + currentActivity, + getCurrentActivity, + currentActivityLoading, + getTimeSlots, + timeSlotsLoading, + timeSlots, + getQuotationPdf, + } = useShopStore(); + const { addToCart, addToCartLoading } = useCartStore(); + const { activityId } = useParams(); + const theme = useTheme(); + const tertiaryLighter = lighten(theme.palette.tertiary.main, 0.4); + const accent = theme.palette.accent.main; + const primary = theme.palette.primary.main; + const tertiary = theme.palette.tertiary.main; + const [selectedDate, setSelectedDate] = useState(null); + const [pax, setPax] = useState(""); + const [time, setTime] = useState(""); + const [location, setLocation] = useState(""); + const hd = new Holidays("SG"); + const [isModalOpen, setIsModalOpen] = useState(false); + const [comments, setComments] = useState(""); + const { openSnackbar } = useSnackbarStore(); + const [fileUrl, setFileUrl] = useState(null); - const handleTimeChange = (event) => { - setTime(event.target.value); - }; + const handleTimeChange = (event) => { + setTime(event.target.value); + }; - const handleLocationChange = (event) => { - setLocation(event.target.value); - }; + const handleLocationChange = (event) => { + setLocation(event.target.value); + }; - useEffect(() => { - getCurrentActivity(activityId); - }, [activityId]); + useEffect(() => { + getCurrentActivity(activityId); + }, [activityId]); - const handlePaxChange = (event) => { - const { value } = event.target; - setPax(value); - }; + const handlePaxChange = (event) => { + const { value } = event.target; + setPax(value); + }; - const handleDateChange = (date) => { - setSelectedDate(date); - }; + const handleDateChange = (date) => { + setSelectedDate(date); + }; - const shouldDisableDate = (date) => { - const dayOfWeek = dayjs(date).day(); - const isPublicHoliday = hd.isHoliday(new Date(date)); - const conditionsToCheck = []; - const phEnabled = false; - for (const dayAvailability of currentActivity?.dayAvailabilities) { - if ( - dayAvailability.toLowerCase().includes("weekends") && - (dayOfWeek === 0 || dayOfWeek === 6) - ) { - // return true if weekend - conditionsToCheck.push(true); - } + const shouldDisableDate = (date) => { + const dayOfWeek = dayjs(date).day(); + const isPublicHoliday = hd.isHoliday(new Date(date)); + const conditionsToCheck = []; + const phEnabled = false; + for (const dayAvailability of currentActivity?.dayAvailabilities) { + if ( + dayAvailability.toLowerCase().includes("weekends") && + (dayOfWeek === 0 || dayOfWeek === 6) + ) { + // return true if weekend + conditionsToCheck.push(true); + } - if ( - dayAvailability.toLowerCase().includes("weekdays") && - dayOfWeek >= 1 && - dayOfWeek <= 5 - ) { - // return true if weekday - conditionsToCheck.push(true); - } + if ( + dayAvailability.toLowerCase().includes("weekdays") && + dayOfWeek >= 1 && + dayOfWeek <= 5 + ) { + // return true if weekday + conditionsToCheck.push(true); + } - if ( - dayAvailability.toLowerCase().includes("public holidays") && - isPublicHoliday - ) { - phEnabled = true; - // return true PH - conditionsToCheck.push(true); + if ( + dayAvailability.toLowerCase().includes("public holidays") && + isPublicHoliday + ) { + phEnabled = true; + // return true PH + conditionsToCheck.push(true); + } } - } - if (!phEnabled && isPublicHoliday) { - return true; - } - return !conditionsToCheck.some((condition) => condition); - }; + if (!phEnabled && isPublicHoliday) { + return true; + } + return !conditionsToCheck.some((condition) => condition); + }; - const clientPriceCalculated = (pax) => { - for (const pricingRule of currentActivity?.activityPricingRules) { - if (pax >= pricingRule.start && pax <= pricingRule.end) { - return pricingRule.clientPrice; + const clientPriceCalculated = (pax) => { + for (const pricingRule of currentActivity?.activityPricingRules) { + if (pax >= pricingRule.start && pax <= pricingRule.end) { + return pricingRule.clientPrice; + } } - } - }; + }; - // Function to calculate the base price - const calculateBasePrice = (pax) => { - return pax * clientPriceCalculated(pax); - }; + // Function to calculate the base price + const calculateBasePrice = (pax) => { + return pax * clientPriceCalculated(pax); + }; - const calculateWeekendAddOn = (selectedDate, weekendPricing) => { - if ( - weekendPricing.amount !== null && - (dayjs(selectedDate).day() === 0 || dayjs(selectedDate).day() === 6) - ) { - if (weekendPricing.isDiscount) { - return -weekendPricing.amount; - } else { - return weekendPricing.amount; + const calculateWeekendAddOn = (selectedDate, weekendPricing) => { + if ( + weekendPricing.amount !== null && + (dayjs(selectedDate).day() === 0 || dayjs(selectedDate).day() === 6) + ) { + if (weekendPricing.isDiscount) { + return -weekendPricing.amount; + } else { + return weekendPricing.amount; + } } - } - return 0; - }; + return 0; + }; - const calculateOnlineAddOn = (location, onlinePricing) => { - if ( - onlinePricing.amount !== null && - (location.toLowerCase().includes("off-site") || - location.toLowerCase().includes("on-site")) - ) { - if (onlinePricing.isDiscount) { - return -onlinePricing.amount; - } else { - return onlinePricing.amount; + const calculateOnlineAddOn = (location, onlinePricing) => { + if ( + onlinePricing.amount !== null && + (location.toLowerCase().includes("off-site") || + location.toLowerCase().includes("on-site")) + ) { + if (onlinePricing.isDiscount) { + return -onlinePricing.amount; + } else { + return onlinePricing.amount; + } } - } - return 0; - }; + return 0; + }; - const calculateOfflineAddOn = (location, offlinePricing) => { - if ( - offlinePricing.amount !== null && - location.toLowerCase().includes("virtual") - ) { - if (offlinePricing.isDiscount) { - return -offlinePricing.amount; - } else { - return offlinePricing.amount; + const calculateOfflineAddOn = (location, offlinePricing) => { + if ( + offlinePricing.amount !== null && + location.toLowerCase().includes("virtual") + ) { + if (offlinePricing.isDiscount) { + return -offlinePricing.amount; + } else { + return offlinePricing.amount; + } } - } - return 0; - }; + return 0; + }; - // Combined function to calculate the total price - const totalPrice = () => { - let totalPriceCalculated = calculateBasePrice(pax); + // Combined function to calculate the total price + const totalPrice = () => { + let totalPriceCalculated = calculateBasePrice(pax); - const weekendAddOn = calculateWeekendAddOn( - selectedDate, - currentActivity.weekendPricing, - ); + const weekendAddOn = calculateWeekendAddOn( + selectedDate, + currentActivity.weekendPricing + ); - const onlineAddOn = calculateOnlineAddOn( - location, - currentActivity.offlinePricing, - ); + const onlineAddOn = calculateOnlineAddOn( + location, + currentActivity.offlinePricing + ); - const offlineAddOn = calculateOfflineAddOn( - location, - currentActivity.onlinePricing, - ); + const offlineAddOn = calculateOfflineAddOn( + location, + currentActivity.onlinePricing + ); - totalPriceCalculated = - totalPriceCalculated + weekendAddOn + onlineAddOn + offlineAddOn; - return totalPriceCalculated?.toFixed(2); - }; + totalPriceCalculated = + totalPriceCalculated + weekendAddOn + onlineAddOn + offlineAddOn; + return totalPriceCalculated?.toFixed(2); + }; - const openModal = () => { - setIsModalOpen(true); - }; + const openModal = () => { + setIsModalOpen(true); + }; - const closeModal = () => { - setIsModalOpen(false); - }; + const closeModal = () => { + setIsModalOpen(false); + }; - const handleAddToCart = async (event) => { - const weekendAddOn = calculateWeekendAddOn( - selectedDate, - currentActivity.weekendPricing, - ); - const onlineAddOn = calculateOnlineAddOn( - location, - currentActivity.offlinePricing, - ); - const offlineAddOn = calculateOfflineAddOn( - location, - currentActivity.onlinePricing, - ); - const timeParts = time.split(","); - const cartItem = { - activityId: currentActivity._id, - totalPax: Number(pax), - basePricePerPax: clientPriceCalculated(pax), - eventLocationType: location, - additionalComments: comments, - weekendAddOnCost: weekendAddOn, - onlineAddOnCost: onlineAddOn, - offlineAddOnCost: offlineAddOn, - startDateTime: timeParts[0], - endDateTime: timeParts[1], - }; - if ( - cartItem.activityId !== null && - cartItem.totalPax.length > 0 && - cartItem.basePricePerPax !== null && - cartItem.eventLocationType.length > 0 - ) { - openSnackbar("There are errors in adding this booking to cart", "error"); - return; - } - try { - const responseStatus = await addToCart(cartItem); - if (responseStatus) { - openSnackbar("Booking added to cart!", "success"); - setSelectedDate(null); - setPax(""); - setTime(""); - setLocation(""); - setComments(""); + const handleAddToCart = async (event) => { + const weekendAddOn = calculateWeekendAddOn( + selectedDate, + currentActivity.weekendPricing + ); + const onlineAddOn = calculateOnlineAddOn( + location, + currentActivity.offlinePricing + ); + const offlineAddOn = calculateOfflineAddOn( + location, + currentActivity.onlinePricing + ); + const timeParts = time.split(","); + const cartItem = { + activityId: currentActivity._id, + totalPax: Number(pax), + basePricePerPax: clientPriceCalculated(pax), + eventLocationType: location, + additionalComments: comments, + weekendAddOnCost: weekendAddOn, + onlineAddOnCost: onlineAddOn, + offlineAddOnCost: offlineAddOn, + startDateTime: timeParts[0], + endDateTime: timeParts[1], + }; + if ( + cartItem.activityId !== null && + cartItem.totalPax.length > 0 && + cartItem.basePricePerPax !== null && + cartItem.eventLocationType.length > 0 + ) { + openSnackbar( + "There are errors in adding this booking to cart", + "error" + ); + return; + } + try { + const responseStatus = await addToCart(cartItem); + if (responseStatus) { + openSnackbar("Booking added to cart!", "success"); + setSelectedDate(null); + setPax(""); + setTime(""); + setLocation(""); + setComments(""); + } + } catch (error) { + openSnackbar(error.response.data.msg, "error"); } - } catch (error) { - openSnackbar(error.response.data.msg, "error"); - } - }; - useEffect(() => { - // Check if all three variables are defined - if (pax.length !== 0 && selectedDate !== null && location.length !== 0) { - getTimeSlots(currentActivity._id, selectedDate); - } - }, [pax, selectedDate, location]); + }; - return ( - - {currentActivityLoading && ( - - - - )} - {!currentActivityLoading && ( - - - - - - {currentActivity?.title} - - {currentActivity?.linkedVendor && ( - - )} - + const handleDownloadQuotation = async (event) => { + try { + const bookingData = { + title: currentActivity.title, + selectedDate, + totalPax: pax, + location, + time, + totalCost: totalPrice(), + }; + const url = await getQuotationPdf(bookingData); + } catch (err) { + console.log(err); + } + }; + useEffect(() => { + // Check if all three variables are defined + if (pax.length !== 0 && selectedDate !== null && location.length !== 0) { + getTimeSlots(currentActivity._id, selectedDate); + } + }, [pax, selectedDate, location]); - - - - - - - {currentActivity?.preSignedImages && - currentActivity?.preSignedImages.map((image, index) => ( - - {`Img - - ))} - {!currentActivity?.preSignedImages.length > 0 ? ( - - {currentActivity?.title} - - ) : null} - + return ( + + {currentActivityLoading && ( + + - + )} + {!currentActivityLoading && ( + + + + + + {currentActivity?.title} + + {currentActivity?.linkedVendor && ( + + )} + - - - - - - From - - - - $ - - - {currentActivity?.minimumPricePerPax?.toFixed(2)} - - - - per pax - - - - Prices are in $SGD - - - - - - - - - - - - + - - - - - Select Location - - - - - - {timeSlotsLoading && ( - - - - - - )} - {!timeSlotsLoading && ( - - - Select Time Slot - - - - )} - - {selectedDate && - pax.length > 0 && - time.length > 0 && - location.length > 0 && ( - - - Base Price - - - - {pax} Adults X ${" "} - {clientPriceCalculated(pax)?.toFixed(2)} - - - ${(pax * clientPriceCalculated(pax))?.toFixed(2)} + navigation={true} + modules={[Pagination, Navigation]} + className="mySwiper"> + {currentActivity?.preSignedImages && + currentActivity?.preSignedImages.map( + (image, index) => ( + + {`Img + + ) + )} + {!currentActivity?.preSignedImages.length > 0 ? ( + + {currentActivity?.title} + + ) : null} + + + + + + + + + + From + + + + $ + + + {currentActivity?.minimumPricePerPax?.toFixed( + 2 + )} + + + + per pax + + + + Prices are in $SGD - - - )} - {selectedDate && - pax.length > 0 && - time.length > 0 && - location.length > 0 && - ((currentActivity.weekendPricing.amount !== null && - (dayjs(selectedDate).day() == 0 || - dayjs(selectedDate).day() == 6)) || - (currentActivity.offlinePricing.amount !== null && - (location.toLowerCase().includes("off-site") || - location.toLowerCase().includes("on-site"))) || - (currentActivity.onlinePricing.amount !== null && - location.toLowerCase().includes("virtual"))) && ( - - - Add-ons/Discounts - - {(dayjs(selectedDate).day() == 0 || - dayjs(selectedDate).day() == 6) && - currentActivity.weekendPricing.amount !== null && ( - - Weekend Pricing - + + + + - - {currentActivity?.weekendPricing?.isDiscount - ? "-" - : ""} - - - {currentActivity?.weekendPricing?.isDiscount - ? "-" - : ""} - $ - {currentActivity?.weekendPricing?.amount?.toFixed( - 2, - )} - - - - )} - {(location.toLowerCase().includes("off-site") || - location.toLowerCase().includes("on-site")) && - currentActivity.offlinePricing.amount !== null && ( - - Offline Pricing - - - {currentActivity?.offlinePricing?.isDiscount - ? "-" - : ""} - {""}$ - {currentActivity?.offlinePricing?.amount?.toFixed( - 2, - )} + alignItems="center"> + + + + + + + + + + + + Select Location + + + + + + {timeSlotsLoading && ( + + + + + + )} + {!timeSlotsLoading && ( + + + Select Time Slot + + + + )} + + {selectedDate && + pax.length > 0 && + time.length > 0 && + location.length > 0 && ( + + + Base Price + + + + {pax} Adults X ${" "} + {clientPriceCalculated(pax)?.toFixed(2)} + + + $ + {( + pax * clientPriceCalculated(pax) + )?.toFixed(2)} + + + + )} + {selectedDate && + pax.length > 0 && + time.length > 0 && + location.length > 0 && + ((currentActivity.weekendPricing.amount !== null && + (dayjs(selectedDate).day() == 0 || + dayjs(selectedDate).day() == 6)) || + (currentActivity.offlinePricing.amount !== null && + (location.toLowerCase().includes("off-site") || + location + .toLowerCase() + .includes("on-site"))) || + (currentActivity.onlinePricing.amount !== null && + location + .toLowerCase() + .includes("virtual"))) && ( + + + Add-ons/Discounts + + {(dayjs(selectedDate).day() == 0 || + dayjs(selectedDate).day() == 6) && + currentActivity.weekendPricing.amount !== + null && ( + + + Weekend Pricing + + + + {currentActivity?.weekendPricing + ?.isDiscount + ? "-" + : ""} + + + {currentActivity?.weekendPricing + ?.isDiscount + ? "-" + : ""} + $ + {currentActivity?.weekendPricing?.amount?.toFixed( + 2 + )} + + + + )} + {(location + .toLowerCase() + .includes("off-site") || + location + .toLowerCase() + .includes("on-site")) && + currentActivity.offlinePricing.amount !== + null && ( + + + Offline Pricing + + + + {currentActivity?.offlinePricing + ?.isDiscount + ? "-" + : ""} + {""}$ + {currentActivity?.offlinePricing?.amount?.toFixed( + 2 + )} + + + + )} + {location.toLowerCase().includes("virtual") && + currentActivity.onlinePricing.amount !== + null && ( + + + Online Pricing + + + + {currentActivity?.onlinePricing + ?.isDiscount + ? "-" + : ""} + {""}$ + {currentActivity?.onlinePricing?.amount?.toFixed( + 2 + )} + + + + )} + + )} + {selectedDate && + pax.length > 0 && + time.length > 0 && + location.length > 0 && ( + + + Total Price + + + + $ {totalPrice()} + + + )} + + + + Add Additional Comments - - - )} - {location.toLowerCase().includes("virtual") && - currentActivity.onlinePricing.amount !== null && ( - - Online Pricing - - - {currentActivity?.onlinePricing?.isDiscount - ? "-" - : ""} - {""}$ - {currentActivity?.onlinePricing?.amount?.toFixed( - 2, - )} + setComments(e.target.value)} + placeholder="Enter additional comments here" + /> + + + + + + + + + + + + + + + + + + + + + + {currentActivity?.duration} mins + + + + + {currentActivity?.location.map((location, index) => ( + + {index + 1}. {location.split(" ")[0]} - - + ))} + + + + {currentActivity?.dayAvailabilities.map( + (location, index) => ( + + {index + 1}. {location} + + ) + )} + + + + + + + Overview + + + Pricing: + + + {currentActivity?.activityPricingRules.map( + (activityPricingRule, index) => ( + + + {index === + currentActivity?.activityPricingRules + .length - + 1 ? ( + + + + Most For Value Price + + + ) : null} + + + $ + + + {activityPricingRule.clientPrice.toFixed( + 2 + )} + + + ++ + + + + For {activityPricingRule.start} to{" "} + {activityPricingRule.end} pax + + + + ) )} - - )} - {selectedDate && - pax.length > 0 && - time.length > 0 && - location.length > 0 && ( - - + {(currentActivity?.offlinePricing?.isDiscount || + currentActivity?.onlinePricing?.isDiscount || + currentActivity?.weekendPricing?.isDiscount) && ( + + Discounts: + + )} + {currentActivity?.offlinePricing?.isDiscount ? ( + + + + $ + + + {currentActivity?.offlinePricing.amount.toFixed( + 2 + )}{" "} + discount for Offline option + + + ) : null} + {currentActivity?.onlinePricing?.isDiscount ? ( + + + + $ + + + {currentActivity?.onlinePricing.amount.toFixed(2)}{" "} + discount for Online option + + + ) : null} + {currentActivity?.weekendPricing?.isDiscount ? ( + + + + $ + + + {currentActivity?.weekendPricing.amount.toFixed( + 2 + )}{" "} + discount for Weekend option + + + ) : null} + {((!currentActivity?.offlinePricing?.isDiscount && + currentActivity?.offlinePricing?.amount !== null) || + (!currentActivity?.onlinePricing?.isDiscount && + currentActivity?.onlinePricing?.amount !== null) || + (!currentActivity?.weekendPricing?.isDiscount && + currentActivity?.weekendPricing?.amount !== + null)) && ( + + Add-ons: + + )} + {!currentActivity?.offlinePricing?.isDiscount && + currentActivity?.offlinePricing?.amount !== null ? ( + + + + $ + + + {currentActivity?.offlinePricing.amount.toFixed( + 2 + )}{" "} + discount for Offline option + + + ) : null} + {!currentActivity?.onlinePricing?.isDiscount && + currentActivity?.onlinePricing?.amount !== null ? ( + + + + $ + + + {currentActivity?.onlinePricing.amount.toFixed(2)}{" "} + discount for Online option + + + ) : null} + {!currentActivity?.weekendPricing?.isDiscount && + currentActivity?.weekendPricing?.amount !== null ? ( + + + + $ + + + {currentActivity?.weekendPricing.amount.toFixed( + 2 + )}{" "} + discount for Weekend option + + + ) : null} + - Total Price - - - + Requirements: + + + + + Book {currentActivity?.bookingNotice} days in + advance + + + + + + Minimum No. of Pax per booking:{" "} + {currentActivity?.minParticipants} + + + + + + Maximum No. of Pax per booking:{" "} + {currentActivity?.maxParticipants} + + + - $ {totalPrice()} - - - )} - - - - Add Additional Comments - - setComments(e.target.value)} - placeholder="Enter additional comments here" - /> - - - - - - - - - - - - - - - - - - - - - - {currentActivity?.duration} mins - - - - - {currentActivity?.location.map((location, index) => ( - - {index + 1}. {location.split(" ")[0]} - - ))} - - - - {currentActivity?.dayAvailabilities.map((location, index) => ( - - {index + 1}. {location} - - ))} - - - - - - - Overview - - - Pricing: - - - {currentActivity?.activityPricingRules.map( - (activityPricingRule, index) => ( - - + Theme: + + - {index === - currentActivity?.activityPricingRules.length - 1 ? ( - - - - Most For Value Price - - - ) : null} - - - $ - - - {activityPricingRule.clientPrice.toFixed(2)} - - - ++ - + flexDirection="row" + alignItems="center" + mt={1}> + {currentActivity?.theme?.name} + + + Subthemes: + + {currentActivity?.subtheme.map((subtheme, index) => ( + + + + {subtheme?.name.split(" ")[0]} + - - For {activityPricingRule.start} to{" "} - {activityPricingRule.end} pax - - - - ), - )} - - {(currentActivity?.offlinePricing?.isDiscount || - currentActivity?.onlinePricing?.isDiscount || - currentActivity?.weekendPricing?.isDiscount) && ( - - Discounts: - - )} - {currentActivity?.offlinePricing?.isDiscount ? ( - - - - $ - - - {currentActivity?.offlinePricing.amount.toFixed(2)} discount - for Offline option - - - ) : null} - {currentActivity?.onlinePricing?.isDiscount ? ( - - - - $ - - - {currentActivity?.onlinePricing.amount.toFixed(2)} discount - for Online option - - - ) : null} - {currentActivity?.weekendPricing?.isDiscount ? ( - - - - $ - - - {currentActivity?.weekendPricing.amount.toFixed(2)} discount - for Weekend option - - - ) : null} - {((!currentActivity?.offlinePricing?.isDiscount && - currentActivity?.offlinePricing?.amount !== null) || - (!currentActivity?.onlinePricing?.isDiscount && - currentActivity?.onlinePricing?.amount !== null) || - (!currentActivity?.weekendPricing?.isDiscount && - currentActivity?.weekendPricing?.amount !== null)) && ( - - Add-ons: - - )} - {!currentActivity?.offlinePricing?.isDiscount && - currentActivity?.offlinePricing?.amount !== null ? ( - - - - $ - - - {currentActivity?.offlinePricing.amount.toFixed(2)} discount - for Offline option - - - ) : null} - {!currentActivity?.onlinePricing?.isDiscount && - currentActivity?.onlinePricing?.amount !== null ? ( - - - - $ - - - {currentActivity?.onlinePricing.amount.toFixed(2)} discount - for Online option - - - ) : null} - {!currentActivity?.weekendPricing?.isDiscount && - currentActivity?.weekendPricing?.amount !== null ? ( - - - - $ - - - {currentActivity?.weekendPricing.amount.toFixed(2)} discount - for Weekend option - - - ) : null} - - Requirements: - - - - - Book {currentActivity?.bookingNotice} days in advance - - - - - - Minimum No. of Pax per booking:{" "} - {currentActivity?.minParticipants} - - - - - - Maximum No. of Pax per booking:{" "} - {currentActivity?.maxParticipants} - - - - Theme: - - - {currentActivity?.theme?.name} - - - Subthemes: - - {currentActivity?.subtheme.map((subtheme, index) => ( - - - - {subtheme?.name.split(" ")[0]} - - - ))} - - Activity Type: - - {currentActivity?.activityType} - - Description: - - - {currentActivity?.description} - - - Sustainable Development Goals: - - {currentActivity?.sdg.map((sdg, index) => ( - + Activity Type: + + + {currentActivity?.activityType} + + + Description: + + + {currentActivity?.description} + + + Sustainable Development Goals: + + {currentActivity?.sdg.map((sdg, index) => ( + + + + {sdg.split(" ")[0]} + + + ))} + + + + + No Ratings yet + + + - - - {sdg.split(" ")[0]} - - - ))} - - - - - No Ratings yet - - - - - )} - - ); + justifyContent="center" + flexDirection="row"> + + )} + + ); }; export default ActivityDetailsPage; diff --git a/client-frontend/src/zustand/ActivityStore.js b/client-frontend/src/zustand/ActivityStore.js index 43ece81..9d7e377 100644 --- a/client-frontend/src/zustand/ActivityStore.js +++ b/client-frontend/src/zustand/ActivityStore.js @@ -2,129 +2,129 @@ import { create } from "zustand"; import AxiosConnect from "../utils/AxiosConnect"; const useActivityStore = create((set) => ({ - activities: [], - isLoading: false, - newActivity: null, - activityDetails: {}, - selectedTab: "publishedTab", - pendingApprovalActivities: [], - selectedActivityTab: "publishedTab", - setActivities: (activities) => { - set({ activities }); - }, - setPendingApprovalActivities: (pendingApprovalActivities) => { - set({ pendingApprovalActivities }); - }, - setSelectedTab: (thing) => { - set({ selectedTab: thing }); - }, - setSelectedActivityTab: (thing) => { - set({ selectedActivityTab: thing }); - }, - getActivity: async () => { - try { - set({ isLoading: true }); - const response = await AxiosConnect.get("/activity/all"); - set({ - activities: response.data.publishedActivities, - pendingApprovalActivities: response.data.pendingApprovalActivities, - }); - set({ isLoading: false }); - } catch (error) { - console.error(error); - } - }, - getActivityForVendor: async () => { - try { - set({ isLoading: true }); - const response = await AxiosConnect.get("/gleekVendor/activity/mine"); - set({ activities: response.data }); - set({ isLoading: false }); - } catch (error) { - console.error(error); - } - }, - getSingleActivity: async (activityId) => { - try { - set({ isLoading: true }); - const response = await AxiosConnect.get( - `/gleekVendor/activity/viewActivity/${activityId}`, - ); - set({ activityDetails: response.data.data }); - set({ isLoading: false }); - } catch (error) { - console.error(error); - } - }, - saveActivity: async (activityDraftData) => { - try { - const response = await AxiosConnect.postMultiPart( - "/gleekVendor/activity/saveActivity", - activityDraftData, - ); - set({ newActivity: response.data.activity }); - } catch (error) { - throw new Error("Unexpected Server Error!"); - } - }, - deleteActivity: async (activityId) => { - try { - const updatedActivities = await AxiosConnect.delete( - `/gleekVendor/activity/deleteDraft/${activityId}`, - ); - console.log("updated activities", updatedActivities.data.activity); - set({ activities: updatedActivities.data.activity }); - set({ selectedTab: "draftTab" }); - return updatedActivities.data.message; - } catch (error) { - console.log(error); - } - }, - bulkDeleteActivity: async (activityIds) => { - try { - const updatedActivities = await AxiosConnect.delete( - "/gleekVendor/activity/bulkDelete", - activityIds, - ); - set({ - activities: updatedActivities.data.activity, - selectedTab: "draftTab", - }); - return updatedActivities.data.message; - } catch (error) { - console.log(error); - } - }, - publishActivity: async (activityId) => { - try { - const response = await AxiosConnect.patch( - `/gleekVendor/activity/publishActivity/${activityId}`, - ); - set({ - selectedActivityTab: "readyToPublishTab", - }); - return response.data; - } catch (error) { - console.log(error); - throw new Error(error.message); - } - }, - rejectActivity: async (activityId, rejectionReason, adminId) => { - try { - const updatedActivities = await AxiosConnect.patch( - "/activity/rejectActivity", - activityId, - { rejectionReason: rejectionReason, adminId: adminId }, - ); - set({ - selectedActivityTab: "pendingApprovalTab", - }); - return updatedActivities.data.message; - } catch (error) { - console.log(error); - throw new Error(error.message); - } - }, + activities: [], + isLoading: false, + newActivity: null, + activityDetails: {}, + selectedTab: "publishedTab", + pendingApprovalActivities: [], + selectedActivityTab: "publishedTab", + setActivities: (activities) => { + set({ activities }); + }, + setPendingApprovalActivities: (pendingApprovalActivities) => { + set({ pendingApprovalActivities }); + }, + setSelectedTab: (thing) => { + set({ selectedTab: thing }); + }, + setSelectedActivityTab: (thing) => { + set({ selectedActivityTab: thing }); + }, + getActivity: async () => { + try { + set({ isLoading: true }); + const response = await AxiosConnect.get("/activity/all"); + set({ + activities: response.data.publishedActivities, + pendingApprovalActivities: response.data.pendingApprovalActivities, + }); + set({ isLoading: false }); + } catch (error) { + console.error(error); + } + }, + getActivityForVendor: async () => { + try { + set({ isLoading: true }); + const response = await AxiosConnect.get("/gleekVendor/activity/mine"); + set({ activities: response.data }); + set({ isLoading: false }); + } catch (error) { + console.error(error); + } + }, + getSingleActivity: async (activityId) => { + try { + set({ isLoading: true }); + const response = await AxiosConnect.get( + `/gleekVendor/activity/viewActivity/${activityId}` + ); + set({ activityDetails: response.data.data }); + set({ isLoading: false }); + } catch (error) { + console.error(error); + } + }, + saveActivity: async (activityDraftData) => { + try { + const response = await AxiosConnect.postMultiPart( + "/gleekVendor/activity/saveActivity", + activityDraftData + ); + set({ newActivity: response.data.activity }); + } catch (error) { + throw new Error("Unexpected Server Error!"); + } + }, + deleteActivity: async (activityId) => { + try { + const updatedActivities = await AxiosConnect.delete( + `/gleekVendor/activity/deleteDraft/${activityId}` + ); + console.log("updated activities", updatedActivities.data.activity); + set({ activities: updatedActivities.data.activity }); + set({ selectedTab: "draftTab" }); + return updatedActivities.data.message; + } catch (error) { + console.log(error); + } + }, + bulkDeleteActivity: async (activityIds) => { + try { + const updatedActivities = await AxiosConnect.delete( + "/gleekVendor/activity/bulkDelete", + activityIds + ); + set({ + activities: updatedActivities.data.activity, + selectedTab: "draftTab", + }); + return updatedActivities.data.message; + } catch (error) { + console.log(error); + } + }, + publishActivity: async (activityId) => { + try { + const response = await AxiosConnect.patch( + `/gleekVendor/activity/publishActivity/${activityId}` + ); + set({ + selectedActivityTab: "readyToPublishTab", + }); + return response.data; + } catch (error) { + console.log(error); + throw new Error(error.message); + } + }, + rejectActivity: async (activityId, rejectionReason, adminId) => { + try { + const updatedActivities = await AxiosConnect.patch( + "/activity/rejectActivity", + activityId, + { rejectionReason: rejectionReason, adminId: adminId } + ); + set({ + selectedActivityTab: "pendingApprovalTab", + }); + return updatedActivities.data.message; + } catch (error) { + console.log(error); + throw new Error(error.message); + } + }, })); export default useActivityStore; diff --git a/client-frontend/src/zustand/ShopStore.js b/client-frontend/src/zustand/ShopStore.js index f6ce4bb..fe3e413 100644 --- a/client-frontend/src/zustand/ShopStore.js +++ b/client-frontend/src/zustand/ShopStore.js @@ -1,162 +1,178 @@ import { create } from "zustand"; import AxiosConnect from "../utils/AxiosConnect"; +import download from "downloadjs"; const useShopStore = create((set) => ({ - activities: [], - setActivities: (newActivities) => set({ activities: newActivities }), - currentPage: 1, - setCurrentPage: (newPage) => set({ currentPage: newPage }), - sortBy: "Newest First", - setSortBy: (newSortBy) => set({ sortBy: newSortBy }), - currentActivity: null, - setCurrentActivity: (newCurrentActivity) => - set({ currentActivity: newCurrentActivity }), - currentActivityLoading: true, - getCurrentActivity: async (activityId) => { - try { - set({ currentActivityLoading: true }); - const response = await AxiosConnect.get( - `/gleek/shop/viewActivity/${activityId}`, - ); - console.log(response.data.data); - set({ currentActivity: response.data.data }); - set({ currentActivityLoading: false }); - } catch (error) { - console.error(error); - } - }, - themes: [], - getThemes: async () => { - try { - const response = await AxiosConnect.get("/gleek/shop/getAllThemes"); - set({ themes: response.data.data.slice(1) }); - } catch (error) { - console.error(error); - } - }, - filter: { - themes: [], - locations: [], - sgs: [], - daysAvailability: [], - activityType: [], - duration: [], - priceRange: [null, null], - }, - setFilter: (newFilter) => set({ filter: newFilter }), - getFilteredActivitiesLoading: true, - setFilteredActivitiesLoading: (newFilteredActivitiesLoading) => - set({ filteredActivitiesLoading: newFilteredActivitiesLoading }), - getFilteredActivities: async (filter, searchValue) => { - try { - set({ getFilteredActivitiesLoading: true }); - const response = await AxiosConnect.post( - "/gleek/shop/getFilteredActivities", - { - filter: filter, - searchValue: searchValue, - }, - ); - set({ activities: response.data.activities }); - console.log(response.data.activities); - setTimeout(() => { - set({ getFilteredActivitiesLoading: false }); - }, 200); - } catch (error) { - console.error(error); - } - }, - getFilteredActivitiesWithSearchValue: async (filter, searchValue) => { - try { - set({ getFilteredActivitiesLoading: true }); - const response = await AxiosConnect.post( - "/gleek/shop/getFilteredActivities", - { - filter: filter, - searchValue, - }, - ); - set({ activities: response.data.activities }); - setTimeout(() => { - set({ getFilteredActivitiesLoading: false }); - }, 200); - } catch (error) { - console.error(error); - } - }, - searchValue: "", - setSearchValue: (newSearchValue) => set({ searchValue: newSearchValue }), - searchValueOnClicked: "", - setSearchValueOnClicked: (newSearchValueOnClicked) => - set({ searchValueOnClicked: newSearchValueOnClicked }), - getInitialSuggestions: async () => { - try { - const response = await AxiosConnect.get( - "/gleek/shop/getAllActivitiesNames", - ); - set({ initialSuggestions: response.data.data }); - } catch (error) { - console.error(error); - } - }, - suggestions: [], - initialSuggestions: [], - setInitialSuggestions: (newInitialSuggestions) => - set({ initialSuggestions: newInitialSuggestions }), - setSuggestions: (newSuggestions) => set({ suggestions: newSuggestions }), - minPriceValue: null, - maxPriceValue: null, - setMaxPriceValue: (newMaxPriceValue) => - set({ maxPriceValue: newMaxPriceValue }), - setMinPriceValue: (newMinPriceValue) => - set({ minPriceValue: newMinPriceValue }), - getPriceInterval: async () => { - set({ priceFilterLoading: true }); - try { - const response = await AxiosConnect.get( - "/gleek/shop/getMinAndMaxPricePerPax", - ); + activities: [], + setActivities: (newActivities) => set({ activities: newActivities }), + currentPage: 1, + setCurrentPage: (newPage) => set({ currentPage: newPage }), + sortBy: "Newest First", + setSortBy: (newSortBy) => set({ sortBy: newSortBy }), + currentActivity: null, + setCurrentActivity: (newCurrentActivity) => + set({ currentActivity: newCurrentActivity }), + currentActivityLoading: true, + getCurrentActivity: async (activityId) => { + try { + set({ currentActivityLoading: true }); + const response = await AxiosConnect.get( + `/gleek/shop/viewActivity/${activityId}` + ); + console.log(response.data.data); + set({ currentActivity: response.data.data }); + set({ currentActivityLoading: false }); + } catch (error) { + console.error(error); + } + }, + themes: [], + getThemes: async () => { + try { + const response = await AxiosConnect.get("/gleek/shop/getAllThemes"); + set({ themes: response.data.data.slice(1) }); + } catch (error) { + console.error(error); + } + }, + filter: { + themes: [], + locations: [], + sgs: [], + daysAvailability: [], + activityType: [], + duration: [], + priceRange: [null, null], + }, + setFilter: (newFilter) => set({ filter: newFilter }), + getFilteredActivitiesLoading: true, + setFilteredActivitiesLoading: (newFilteredActivitiesLoading) => + set({ filteredActivitiesLoading: newFilteredActivitiesLoading }), + getFilteredActivities: async (filter, searchValue) => { + try { + set({ getFilteredActivitiesLoading: true }); + const response = await AxiosConnect.post( + "/gleek/shop/getFilteredActivities", + { + filter: filter, + searchValue: searchValue, + } + ); + set({ activities: response.data.activities }); + console.log(response.data.activities); + setTimeout(() => { + set({ getFilteredActivitiesLoading: false }); + }, 200); + } catch (error) { + console.error(error); + } + }, + getFilteredActivitiesWithSearchValue: async (filter, searchValue) => { + try { + set({ getFilteredActivitiesLoading: true }); + const response = await AxiosConnect.post( + "/gleek/shop/getFilteredActivities", + { + filter: filter, + searchValue, + } + ); + set({ activities: response.data.activities }); + setTimeout(() => { + set({ getFilteredActivitiesLoading: false }); + }, 200); + } catch (error) { + console.error(error); + } + }, + searchValue: "", + setSearchValue: (newSearchValue) => set({ searchValue: newSearchValue }), + searchValueOnClicked: "", + setSearchValueOnClicked: (newSearchValueOnClicked) => + set({ searchValueOnClicked: newSearchValueOnClicked }), + getInitialSuggestions: async () => { + try { + const response = await AxiosConnect.get( + "/gleek/shop/getAllActivitiesNames" + ); + set({ initialSuggestions: response.data.data }); + } catch (error) { + console.error(error); + } + }, + suggestions: [], + initialSuggestions: [], + setInitialSuggestions: (newInitialSuggestions) => + set({ initialSuggestions: newInitialSuggestions }), + setSuggestions: (newSuggestions) => set({ suggestions: newSuggestions }), + minPriceValue: null, + maxPriceValue: null, + setMaxPriceValue: (newMaxPriceValue) => + set({ maxPriceValue: newMaxPriceValue }), + setMinPriceValue: (newMinPriceValue) => + set({ minPriceValue: newMinPriceValue }), + getPriceInterval: async () => { + set({ priceFilterLoading: true }); + try { + const response = await AxiosConnect.get( + "/gleek/shop/getMinAndMaxPricePerPax" + ); - set({ minPriceValue: response.data.minPrice }); - set({ maxPriceValue: response.data.maxPrice }); - setTimeout(() => { - set({ priceFilterLoading: false }); - }, 200); - } catch (error) { - console.error(error); - } - }, - priceFilterLoading: true, - setPriceFilterLoading: (newPriceFilterLoading) => - set({ priceFilterLoading: newPriceFilterLoading }), - timeSlots: null, - setTimeSlots: (newTimeSlots) => set({ timeSlots: newTimeSlots }), - timeSlotsLoading: false, - setTimeSlotsLoading: (newTimeSlotsLoading) => - set({ timeSlotsLoading: newTimeSlotsLoading }), - getTimeSlots: async (activityId, selectedDate) => { - set({ timeSlotsLoading: true }); - try { - const response = await AxiosConnect.get( - `/gleek/booking/getAvailableBookingTimeslots/${activityId}/${selectedDate}`, - ); - set({ - timeSlots: response.data.allTimeslots.filter( - (timeslot) => timeslot.isAvailable === true, - ), - }); - setTimeout(() => { - set({ timeSlotsLoading: false }); - }, 200); - } catch (error) { - console.error(error); - } - }, - parentChecked: [], - setParentChecked: (newParentChecked) => - set({ parentChecked: newParentChecked }), - childChecked: [], - setChildChecked: (newChildChecked) => set({ childChecked: newChildChecked }), + set({ minPriceValue: response.data.minPrice }); + set({ maxPriceValue: response.data.maxPrice }); + setTimeout(() => { + set({ priceFilterLoading: false }); + }, 200); + } catch (error) { + console.error(error); + } + }, + priceFilterLoading: true, + setPriceFilterLoading: (newPriceFilterLoading) => + set({ priceFilterLoading: newPriceFilterLoading }), + timeSlots: null, + setTimeSlots: (newTimeSlots) => set({ timeSlots: newTimeSlots }), + timeSlotsLoading: false, + setTimeSlotsLoading: (newTimeSlotsLoading) => + set({ timeSlotsLoading: newTimeSlotsLoading }), + getTimeSlots: async (activityId, selectedDate) => { + set({ timeSlotsLoading: true }); + try { + const response = await AxiosConnect.get( + `/gleek/booking/getAvailableBookingTimeslots/${activityId}/${selectedDate}` + ); + set({ + timeSlots: response.data.allTimeslots.filter( + (timeslot) => timeslot.isAvailable === true + ), + }); + setTimeout(() => { + set({ timeSlotsLoading: false }); + }, 200); + } catch (error) { + console.error(error); + } + }, + parentChecked: [], + setParentChecked: (newParentChecked) => + set({ parentChecked: newParentChecked }), + childChecked: [], + setChildChecked: (newChildChecked) => set({ childChecked: newChildChecked }), + getQuotationPdf: async (bookingData) => { + try { + const response = await AxiosConnect.post( + "/gleek/shop/getQuotationPdfUrl", + bookingData + ); + window.open( + `http://localhost:5000/gleek/shop/getQuotationPdf/${response.data}` + ); + return; + } catch (err) { + console.log(err); + throw new Error(err.message); + } + }, })); export default useShopStore; diff --git a/server/assets/templates/InvoiceTemplate.js b/server/assets/templates/InvoiceTemplate.js index f8f1166..4fac1a9 100644 --- a/server/assets/templates/InvoiceTemplate.js +++ b/server/assets/templates/InvoiceTemplate.js @@ -1,24 +1,25 @@ import { header } from "./header.js"; import { footer } from "./footer.js"; export const InvoiceTemplate = (booking) => { - let { startDateTime, endDateTime } = booking; + let { selectedDate, time } = booking; - const getDateTime = (datetime) => { - datetime = new Date(datetime); - const date = datetime.toLocaleDateString(undefined, { - day: "2-digit", - month: "short", - }); - const time = datetime.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - hour12: true, - }); - return [date, time]; - }; + selectedDate = new Date(selectedDate); - const [startDate, startTime] = getDateTime(startDateTime); - const [endDate, endTime] = getDateTime(endDateTime); + const startDate = selectedDate.toLocaleDateString(undefined, { + day: "2-digit", + month: "short", + }); + const startTime = selectedDate.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: true, + }); + let endTime = new Date(time.split(",")[1]); + endTime = endTime.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: true, + }); return ` @@ -64,17 +65,7 @@ export const InvoiceTemplate = (booking) => { flex-direction: row; justify-content: space-between; " - > -

- Attn: ${ - booking.client.companyName - }
Checkout APAC
${ - booking.billingAddress - }
Singapore ${booking.billingPostalCode} -

-

- Status: ${booking.status} -

+ >
{ diff --git a/server/controller/activityController.js b/server/controller/activityController.js index 25f26de..51aba4a 100644 --- a/server/controller/activityController.js +++ b/server/controller/activityController.js @@ -4,1009 +4,1031 @@ import ActivityPricingRulesModel from "../model/activityPricingRules.js"; import ApprovalStatusChangeLog from "../model/approvalStatusChangeLog.js"; import ThemeModel from "../model/themeModel.js"; import { - findMinimumPricePerPax, - getAllVendorActivities, - prepareActivityMinimumPricePerPaxAndSingleImage, + findMinimumPricePerPax, + getAllVendorActivities, + prepareActivityMinimumPricePerPaxAndSingleImage, } from "../service/activityService.js"; import { s3GetImages, s3RemoveImages } from "../service/s3ImageServices.js"; import { ActivityApprovalStatusEnum } from "../util/activityApprovalStatusEnum.js"; +import { InvoiceTemplate } from "../assets/templates/InvoiceTemplate.js"; +import pdf from "html-pdf"; +import fs from "fs"; +import path from "path"; // yt: this endpoint retrieves and returns PUBLISHED & PENDING APPROVAL activities only export const getAllActivities = async (req, res) => { - try { - const activities = await ActivityModel.find() - .populate("activityPricingRules") - .populate("linkedVendor") - .populate("theme") - .populate("subtheme") - .populate({ - path: "approvalStatusChangeLog", - populate: { path: "admin", model: "Admin" }, + try { + const activities = await ActivityModel.find() + .populate("activityPricingRules") + .populate("linkedVendor") + .populate("theme") + .populate("subtheme") + .populate({ + path: "approvalStatusChangeLog", + populate: { path: "admin", model: "Admin" }, + }); + const publishedActivities = activities.filter((row) => { + return row.approvalStatus === "Published" && row.isDraft === false; + }); + const pendingApprovalActivities = activities.filter((row) => { + return ( + row.approvalStatus === "Pending Approval" && row.isDraft === false + ); }); - const publishedActivities = activities.filter((row) => { - return row.approvalStatus === "Published" && row.isDraft === false; - }); - const pendingApprovalActivities = activities.filter((row) => { - return row.approvalStatus === "Pending Approval" && row.isDraft === false; - }); - res.status(200).json({ - publishedActivities, - pendingApprovalActivities, - }); - } catch (error) { - res.status(500).json({ error: error.message }); - } + res.status(200).json({ + publishedActivities, + pendingApprovalActivities, + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } }; // yt: this endpoint retrieves ALL activities (both drafts and published) for an admin export const getAllActivitiesForAdmin = async (req, res) => { - try { - const adminId = req.params.id; - const activities = await ActivityModel.find() - .populate({ - path: "adminCreated", - match: { _id: adminId }, - select: "_id name", - }) - .populate("activityPricingRules") - .populate({ - path: "approvalStatusChangeLog", - populate: { path: "admin", model: "Admin", select: "_id name" }, - }) - .populate("theme") - .populate("subtheme") - .populate({ - path: "linkedVendor", - select: "-password", + try { + const adminId = req.params.id; + const activities = await ActivityModel.find() + .populate({ + path: "adminCreated", + match: { _id: adminId }, + select: "_id name", + }) + .populate("activityPricingRules") + .populate({ + path: "approvalStatusChangeLog", + populate: { path: "admin", model: "Admin", select: "_id name" }, + }) + .populate("theme") + .populate("subtheme") + .populate({ + path: "linkedVendor", + select: "-password", + }); + res.status(200).json({ + data: activities, }); - res.status(200).json({ - data: activities, - }); - } catch (error) { - res.status(500).json({ error: error.message }); - } + } catch (error) { + res.status(500).json({ error: error.message }); + } }; export const getPreSignedImgs = async (req, res) => { - try { - const foundActivity = await ActivityModel.findById(req.params.id).populate( - "linkedVendor", - ); - let preSignedUrlArr = await s3GetImages(foundActivity.images); - let vendorProfile; - if (foundActivity.linkedVendor.companyLogo) { - vendorProfile = await s3GetImages(foundActivity.linkedVendor.companyLogo); - } else { - vendorProfile = null; - } - res.status(200).json({ - activityImages: preSignedUrlArr, - vendorProfileImage: vendorProfile, - }); - } catch (error) { - res.status(500).json({ message: error.message }); - } + try { + const foundActivity = await ActivityModel.findById( + req.params.id + ).populate("linkedVendor"); + let preSignedUrlArr = await s3GetImages(foundActivity.images); + let vendorProfile; + if (foundActivity.linkedVendor.companyLogo) { + vendorProfile = await s3GetImages( + foundActivity.linkedVendor.companyLogo + ); + } else { + vendorProfile = null; + } + res.status(200).json({ + activityImages: preSignedUrlArr, + vendorProfileImage: vendorProfile, + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } }; export const getActivity = async (req, res) => { - try { - const foundActivity = await ActivityModel.findById(req.params.id) - .populate("activityPricingRules") - .populate("linkedVendor") - .populate("theme") - .populate("subtheme"); - let preSignedUrlArr = await s3GetImages(foundActivity.images); - foundActivity.preSignedImages = preSignedUrlArr; - console.log("each push:", foundActivity.preSignedImages); - - // Populate the minimum price per pax for each activity - foundActivity.minimumPricePerPax = - await findMinimumPricePerPax(foundActivity); - if (foundActivity.linkedVendor && foundActivity.linkedVendor.companyLogo) { - let preSignedUrl = await s3GetImages( - foundActivity.linkedVendor.companyLogo, - ); - foundActivity.linkedVendor.preSignedPhoto = preSignedUrl; - } - - res.status(200).json({ - data: foundActivity, - }); - } catch (error) { - res.status(500).json({ message: error.message }); - } + try { + const foundActivity = await ActivityModel.findById(req.params.id) + .populate("activityPricingRules") + .populate("linkedVendor") + .populate("theme") + .populate("subtheme"); + let preSignedUrlArr = await s3GetImages(foundActivity.images); + foundActivity.preSignedImages = preSignedUrlArr; + console.log("each push:", foundActivity.preSignedImages); + + // Populate the minimum price per pax for each activity + foundActivity.minimumPricePerPax = + await findMinimumPricePerPax(foundActivity); + if ( + foundActivity.linkedVendor && + foundActivity.linkedVendor.companyLogo + ) { + let preSignedUrl = await s3GetImages( + foundActivity.linkedVendor.companyLogo + ); + foundActivity.linkedVendor.preSignedPhoto = preSignedUrl; + } + + res.status(200).json({ + data: foundActivity, + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } }; export const getActivitiesByVendorId = async (req, res) => { - try { - const { vendorId } = req.params; - const activities = await ActivityModel.find({ - linkedVendor: vendorId, - approvalStatus: "Published", - }) - .populate("activityPricingRules") - .populate("theme") - .populate("subtheme") - .populate("linkedVendor"); + try { + const { vendorId } = req.params; + const activities = await ActivityModel.find({ + linkedVendor: vendorId, + approvalStatus: "Published", + }) + .populate("activityPricingRules") + .populate("theme") + .populate("subtheme") + .populate("linkedVendor"); - const preSignedPromises = activities.map(async (activity) => { - await prepareActivityMinimumPricePerPaxAndSingleImage(activity); - }); + const preSignedPromises = activities.map(async (activity) => { + await prepareActivityMinimumPricePerPaxAndSingleImage(activity); + }); - await Promise.all(preSignedPromises); + await Promise.all(preSignedPromises); - res.status(200).json(activities); - } catch (error) { - console.error(error); - res.status(500).json({ message: "Server Error" }); - } + res.status(200).json(activities); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Server Error" }); + } }; const saveActivityPricingRules = async ( - activityPricingRules, - session, - savedActivity, - validateBeforeSave, + activityPricingRules, + session, + savedActivity, + validateBeforeSave ) => { - const activitypriceobjects = []; - if (Array.isArray(activityPricingRules)) { - for (const jsonString of activityPricingRules) { - try { - const pricingObject = JSON.parse(jsonString); - - const activitypriceobject = { - start: pricingObject.start, - end: pricingObject.end, - pricePerPax: pricingObject.pricePerPax, - clientPrice: pricingObject.clientPrice, - activity: savedActivity._id, - }; - activitypriceobjects.push(activitypriceobject); - } catch (error) { - throw new Error(`Error parsing activity pricing rules`); + const activitypriceobjects = []; + if (Array.isArray(activityPricingRules)) { + for (const jsonString of activityPricingRules) { + try { + const pricingObject = JSON.parse(jsonString); + + const activitypriceobject = { + start: pricingObject.start, + end: pricingObject.end, + pricePerPax: pricingObject.pricePerPax, + clientPrice: pricingObject.clientPrice, + activity: savedActivity._id, + }; + activitypriceobjects.push(activitypriceobject); + } catch (error) { + throw new Error(`Error parsing activity pricing rules`); + } } - } - } else { - console.log(activityPricingRules); - const pricingObject = JSON.parse(activityPricingRules); - - const activitypriceobject = { - start: pricingObject.start, - end: pricingObject.end, - pricePerPax: pricingObject.pricePerPax, - clientPrice: pricingObject.clientPrice, - activity: savedActivity._id, - }; - activitypriceobjects.push(activitypriceobject); - } - await Promise.all( - activitypriceobjects.map(async (pricingRule) => { - try { - const newPricingRule = await ActivityPricingRulesModel.create( - [{ ...pricingRule }], - { - session, - validateBeforeSave, - }, - ); - await ActivityModel.findByIdAndUpdate( - savedActivity._id, - { - $push: { - activityPricingRules: newPricingRule[0]._id, - }, - }, - { new: true, session }, - ); - } catch (error) { - throw new Error("Error when creating activity pricing rules!"); - } - }), - ); + } else { + console.log(activityPricingRules); + const pricingObject = JSON.parse(activityPricingRules); + + const activitypriceobject = { + start: pricingObject.start, + end: pricingObject.end, + pricePerPax: pricingObject.pricePerPax, + clientPrice: pricingObject.clientPrice, + activity: savedActivity._id, + }; + activitypriceobjects.push(activitypriceobject); + } + await Promise.all( + activitypriceobjects.map(async (pricingRule) => { + try { + const newPricingRule = await ActivityPricingRulesModel.create( + [{ ...pricingRule }], + { + session, + validateBeforeSave, + } + ); + await ActivityModel.findByIdAndUpdate( + savedActivity._id, + { + $push: { + activityPricingRules: newPricingRule[0]._id, + }, + }, + { new: true, session } + ); + } catch (error) { + throw new Error("Error when creating activity pricing rules!"); + } + }) + ); }; const saveApprovalStatusChangeLog = async ( - approvalStatus, - rejectionReason, - activityId, - adminId, - session, + approvalStatus, + rejectionReason, + activityId, + adminId, + session ) => { - try { - const newChangeLogEntry = new ApprovalStatusChangeLog({ - approvalStatus, - date: Date.now(), - rejectionReason, - activity: activityId, - admin: adminId, - }); - const thing = await newChangeLogEntry.save({ session }); - return thing; - } catch (error) { - console.error("Error saving Change Log", error); - throw new Error("Error saving Change Log"); - } + try { + const newChangeLogEntry = new ApprovalStatusChangeLog({ + approvalStatus, + date: Date.now(), + rejectionReason, + activity: activityId, + admin: adminId, + }); + const thing = await newChangeLogEntry.save({ session }); + return thing; + } catch (error) { + console.error("Error saving Change Log", error); + throw new Error("Error saving Change Log"); + } }; //yt: this endpoint is for when admin saves/edits/submits an activity draft export const saveActivity = async (req, res) => { - const session = await mongoose.startSession(); - session.startTransaction(); - try { - console.log("save activity body:", req.body); - const { - activityPricingRules, - weekendPricing, - onlinePricing, - offlinePricing, - activityId, - title, - description, - location, - approvalStatus, - activityType, - isFood, - maxParticipants, - minparticipants, - clientMarkupPercentage, - duration, - theme, - bookingNotice, - startTime, - endTime, - capacity, - dayAvailabilities, - subtheme, - sdg, - popupItemsSold, - foodCertDate, - foodCategory, - isFoodCertPending, - linkedVendor, - pendingCertificateType, - updatedImageList, - ...remainderActivity - } = req.body; - const parsedWeekend = JSON.parse(weekendPricing); - const parsedOnline = JSON.parse(onlinePricing); - const parsedOffline = JSON.parse(offlinePricing); - const activity = { - ...remainderActivity, - title: title ?? null, - description: description ?? null, - location: location ?? [], - approvalStatus, - activityType: activityType ?? null, - isFood: isFood ?? null, - maxParticipants: maxParticipants ?? null, - minparticipants: minparticipants ?? null, - clientMarkupPercentage: clientMarkupPercentage ?? null, - duration: duration ?? null, - theme: theme ?? null, - bookingNotice, - startTime: startTime ?? null, - endTime: endTime ?? null, - capacity: capacity ?? null, - dayAvailabilities: dayAvailabilities ?? [], - subtheme: subtheme ?? [], - sdg: sdg ?? [], - popupItemsSold: popupItemsSold ?? null, - foodCertDate: foodCertDate ?? null, - foodCategory: foodCategory ?? [], - isFoodCertPending: isFoodCertPending ?? null, - linkedVendor: linkedVendor ?? req?.user?._id, - pendingCertificateType: pendingCertificateType ?? null, - modifiedDate: Date.now(), - }; - - console.log("linked vendor: ", req.user); - - activity["weekendPricing"] = { - amount: parsedWeekend?.amount, - isDiscount: parsedWeekend?.isDiscount, - }; - activity["onlinePricing"] = { - amount: parsedOnline?.amount, - isDiscount: parsedOnline?.isDiscount, - }; - activity["offlinePricing"] = { - amount: parsedOffline?.amount, - isDiscount: parsedOffline?.isDiscount, - }; - let savedActivity; - if (approvalStatus === "Rejected") { - const foundActivity = - await ActivityModel.findById(activityId).session(session); - - // this is a reject draft (child), update existing - if (!foundActivity.rejectedDraft && foundActivity.parent) { - const updatedRejectedDraft = await ActivityModel.findByIdAndUpdate( - activityId, - { ...activity, activityPricingRules: [] }, - { - new: true, - session, - }, - ); - savedActivity = updatedRejectedDraft; - await ActivityPricingRulesModel.deleteMany( - { activity: activityId }, - { session } - ); - // this is a parent, create a new reject draft (child) + const session = await mongoose.startSession(); + session.startTransaction(); + try { + console.log("save activity body:", req.body); + const { + activityPricingRules, + weekendPricing, + onlinePricing, + offlinePricing, + activityId, + title, + description, + location, + approvalStatus, + activityType, + isFood, + maxParticipants, + minparticipants, + clientMarkupPercentage, + duration, + theme, + bookingNotice, + startTime, + endTime, + capacity, + dayAvailabilities, + subtheme, + sdg, + popupItemsSold, + foodCertDate, + foodCategory, + isFoodCertPending, + linkedVendor, + pendingCertificateType, + updatedImageList, + ...remainderActivity + } = req.body; + const parsedWeekend = JSON.parse(weekendPricing); + const parsedOnline = JSON.parse(onlinePricing); + const parsedOffline = JSON.parse(offlinePricing); + const activity = { + ...remainderActivity, + title: title ?? null, + description: description ?? null, + location: location ?? [], + approvalStatus, + activityType: activityType ?? null, + isFood: isFood ?? null, + maxParticipants: maxParticipants ?? null, + minparticipants: minparticipants ?? null, + clientMarkupPercentage: clientMarkupPercentage ?? null, + duration: duration ?? null, + theme: theme ?? null, + bookingNotice, + startTime: startTime ?? null, + endTime: endTime ?? null, + capacity: capacity ?? null, + dayAvailabilities: dayAvailabilities ?? [], + subtheme: subtheme ?? [], + sdg: sdg ?? [], + popupItemsSold: popupItemsSold ?? null, + foodCertDate: foodCertDate ?? null, + foodCategory: foodCategory ?? [], + isFoodCertPending: isFoodCertPending ?? null, + linkedVendor: linkedVendor ?? req?.user?._id, + pendingCertificateType: pendingCertificateType ?? null, + modifiedDate: Date.now(), + }; + + console.log("linked vendor: ", req.user); + + activity["weekendPricing"] = { + amount: parsedWeekend?.amount, + isDiscount: parsedWeekend?.isDiscount, + }; + activity["onlinePricing"] = { + amount: parsedOnline?.amount, + isDiscount: parsedOnline?.isDiscount, + }; + activity["offlinePricing"] = { + amount: parsedOffline?.amount, + isDiscount: parsedOffline?.isDiscount, + }; + let savedActivity; + if (approvalStatus === "Rejected") { + const foundActivity = + await ActivityModel.findById(activityId).session(session); + + // this is a reject draft (child), update existing + if (!foundActivity.rejectedDraft && foundActivity.parent) { + const updatedRejectedDraft = await ActivityModel.findByIdAndUpdate( + activityId, + { ...activity, activityPricingRules: [] }, + { + new: true, + session, + } + ); + savedActivity = updatedRejectedDraft; + await ActivityPricingRulesModel.deleteMany( + { activity: activityId }, + { session } + ); + // this is a parent, create a new reject draft (child) + } else { + const a = foundActivity.toObject(); + const thing = { + ...a, + ...activity, + }; + const { _id, __v, ...rest } = thing; + rest.parent = activityId; + rest.activityPricingRules = []; + const newActivity = new ActivityModel(rest); + const rejectDraft = await newActivity.save({ + validateBeforeSave: false, + session, + }); + await ActivityModel.findByIdAndUpdate( + activityId, + { rejectedDraft: rejectDraft._id }, + { + new: true, + session, + } + ); + savedActivity = rejectDraft; + console.log("New saved activity", savedActivity); + } + // submit + } else if (activityId) { + try { + const foundActivity = + await ActivityModel.findById(activityId).session(session); + if (!foundActivity) { + throw new Error( + "Activity draft you are trying to save does not exist!" + ); + } else { + let parentId; + // this is rejected draft + if (!foundActivity.rejectedDraft && foundActivity.parent) { + parentId = foundActivity.parent; + await ActivityModel.findByIdAndDelete(foundActivity._id, { + session, + }); + // this is regular draft + } else { + parentId = activityId; + } + savedActivity = await ActivityModel.findByIdAndUpdate( + parentId, + { + ...activity, + activityPricingRules: [], + rejectedDraft: null, + parent: null, + }, + { + new: true, + session, + } + ); + await ActivityPricingRulesModel.deleteMany( + { activity: activityId }, + { session } + ); + } + } catch (error) { + console.error(error); + res.status(500).json({ + error: "Error when trying to update activity draft", + message: "Invalid activity Id!", + }); + } + // new draft -> straightaway submit } else { - const a = foundActivity.toObject(); - const thing = { - ...a, - ...activity, - }; - const { _id, __v, ...rest } = thing; - rest.parent = activityId; - rest.activityPricingRules = []; - const newActivity = new ActivityModel(rest); - const rejectDraft = await newActivity.save({ - validateBeforeSave: false, - session, - }); - await ActivityModel.findByIdAndUpdate( - activityId, - { rejectedDraft: rejectDraft._id }, - { - new: true, + const newActivity = new ActivityModel({ + ...activity, + }); + savedActivity = await newActivity.save({ + validateBeforeSave: false, session, - }, - ); - savedActivity = rejectDraft; - console.log("New saved activity", savedActivity); + }); } - // submit - } else if (activityId) { - try { - const foundActivity = - await ActivityModel.findById(activityId).session(session); - if (!foundActivity) { - throw new Error( - "Activity draft you are trying to save does not exist!", - ); - } else { - let parentId; - // this is rejected draft - if (!foundActivity.rejectedDraft && foundActivity.parent) { - parentId = foundActivity.parent; - await ActivityModel.findByIdAndDelete(foundActivity._id, { - session, - }); - // this is regular draft - } else { - parentId = activityId; - } - savedActivity = await ActivityModel.findByIdAndUpdate( - parentId, - { - ...activity, - activityPricingRules: [], - rejectedDraft: null, - parent: null, - }, - { - new: true, - session, - }, - ); - await ActivityPricingRulesModel.deleteMany( - { activity: activityId }, - { session }, - ); - } - } catch (error) { - console.error(error); - res.status(500).json({ - error: "Error when trying to update activity draft", - message: "Invalid activity Id!", - }); + + console.log("Saved Activity is: ", savedActivity); + + const processedS3ImageUrlToBeKept = []; + console.log("updatedImageList yoo", updatedImageList); + + if (updatedImageList !== undefined && updatedImageList.length > 0) { + for (let i = 0; i < updatedImageList.length; i++) { + processedS3ImageUrlToBeKept.push(updatedImageList[i].split("?")[0]); + } } - // new draft -> straightaway submit - } else { - const newActivity = new ActivityModel({ - ...activity, - }); - savedActivity = await newActivity.save({ - validateBeforeSave: false, - session, - }); - } - console.log("Saved Activity is: ", savedActivity); + const srcS3ToBeKeptImageList = savedActivity.images.filter((item) => + processedS3ImageUrlToBeKept.includes(item) + ); + const srcS3ToBeRemovedImageList = savedActivity.images.filter( + (item) => !processedS3ImageUrlToBeKept.includes(item) + ); + + const fileBody = req.files; + const imagesPathArr = []; - const processedS3ImageUrlToBeKept = []; - console.log("updatedImageList yoo", updatedImageList); + if (fileBody.length !== 0 || fileBody.length !== undefined) { + await s3RemoveImages(srcS3ToBeRemovedImageList); + } - if (updatedImageList !== undefined && updatedImageList.length > 0) { - for (let i = 0; i < updatedImageList.length; i++) { - processedS3ImageUrlToBeKept.push(updatedImageList[i].split("?")[0]); + if (fileBody.length === 0 || fileBody.length === undefined) { + console.log("No image files uploaded"); + } else { + let fileArray = req.files, + fileLocation; + for (let i = 0; i < fileArray.length; i++) { + fileLocation = fileArray[i].location; + imagesPathArr.push(fileLocation); + } } - } - - const srcS3ToBeKeptImageList = savedActivity.images.filter((item) => - processedS3ImageUrlToBeKept.includes(item), - ); - const srcS3ToBeRemovedImageList = savedActivity.images.filter( - (item) => !processedS3ImageUrlToBeKept.includes(item), - ); - - const fileBody = req.files; - const imagesPathArr = []; - - if (fileBody.length !== 0 || fileBody.length !== undefined) { - await s3RemoveImages(srcS3ToBeRemovedImageList); - } - - if (fileBody.length === 0 || fileBody.length === undefined) { - console.log("No image files uploaded"); - } else { - let fileArray = req.files, - fileLocation; - for (let i = 0; i < fileArray.length; i++) { - fileLocation = fileArray[i].location; - imagesPathArr.push(fileLocation); + + for (let i = 0; i < imagesPathArr.length; i++) { + srcS3ToBeKeptImageList.push(imagesPathArr[i]); } - } - - for (let i = 0; i < imagesPathArr.length; i++) { - srcS3ToBeKeptImageList.push(imagesPathArr[i]); - } - - await ActivityModel.findByIdAndUpdate( - savedActivity._id, - { images: srcS3ToBeKeptImageList }, - { new: true, session }, - ); - - if (activityPricingRules) { - await saveActivityPricingRules( - activityPricingRules, - session, - savedActivity, - false, + + await ActivityModel.findByIdAndUpdate( + savedActivity._id, + { images: srcS3ToBeKeptImageList }, + { new: true, session } ); - } - - await session.commitTransaction(); - res.status(201).json({ - message: "Activity draft saved successfully", - activity: savedActivity, - }); - } catch (error) { - console.log("Error caught", error); - await session.abortTransaction(); - res - .status(500) - .json({ error: "Activity cannot be added", message: error.message }); - } finally { - session.endSession(); - } + + if (activityPricingRules) { + await saveActivityPricingRules( + activityPricingRules, + session, + savedActivity, + false + ); + } + + await session.commitTransaction(); + res.status(201).json({ + message: "Activity draft saved successfully", + activity: savedActivity, + }); + } catch (error) { + console.log("Error caught", error); + await session.abortTransaction(); + res.status(500).json({ + error: "Activity cannot be added", + message: error.message, + }); + } finally { + session.endSession(); + } }; export const approveActivity = async (req, res) => { - const session = await mongoose.startSession(); - session.startTransaction(); - try { - console.log("approve activity body:", req.params.activityId, req.body); - const { activityId } = req.params; - const { adminId, markup } = req.body; - - const approvalStatusChangeLog = await saveApprovalStatusChangeLog( - ActivityApprovalStatusEnum.READY_TO_PUBLISH, - null, - activityId, - adminId, - session, - ); - - const savedActivity = await ActivityModel.findByIdAndUpdate( - activityId, - { - approvalStatus: ActivityApprovalStatusEnum.READY_TO_PUBLISH, - clientMarkupPercentage: markup, - approvedDate: Date.now(), - $push: { - approvalStatusChangeLog: approvalStatusChangeLog._id, - }, - }, - { - new: true, - session, - }, - ); - for (const ruleId of savedActivity.activityPricingRules) { - try { - const rule = - await ActivityPricingRulesModel.findById(ruleId).session(session); - if (!rule) { - throw new Error(`Pricing rule not found for ruleId: ${ruleId}`); - } - const { pricePerPax } = rule; - const clientPrice = Math.ceil( - parseFloat(pricePerPax) * (parseFloat(markup) / 100) + - parseFloat(pricePerPax), - ); - - const updatedRule = await ActivityPricingRulesModel.findByIdAndUpdate( - ruleId, - { clientPrice }, - { new: true, session }, - ); - } catch (error) { - throw new Error(`Error processing ruleId: ${ruleId}`, error); + const session = await mongoose.startSession(); + session.startTransaction(); + try { + console.log("approve activity body:", req.params.activityId, req.body); + const { activityId } = req.params; + const { adminId, markup } = req.body; + + const approvalStatusChangeLog = await saveApprovalStatusChangeLog( + ActivityApprovalStatusEnum.READY_TO_PUBLISH, + null, + activityId, + adminId, + session + ); + + const savedActivity = await ActivityModel.findByIdAndUpdate( + activityId, + { + approvalStatus: ActivityApprovalStatusEnum.READY_TO_PUBLISH, + clientMarkupPercentage: markup, + approvedDate: Date.now(), + $push: { + approvalStatusChangeLog: approvalStatusChangeLog._id, + }, + }, + { + new: true, + session, + } + ); + for (const ruleId of savedActivity.activityPricingRules) { + try { + const rule = + await ActivityPricingRulesModel.findById(ruleId).session( + session + ); + if (!rule) { + throw new Error(`Pricing rule not found for ruleId: ${ruleId}`); + } + const { pricePerPax } = rule; + const clientPrice = Math.ceil( + parseFloat(pricePerPax) * (parseFloat(markup) / 100) + + parseFloat(pricePerPax) + ); + + const updatedRule = + await ActivityPricingRulesModel.findByIdAndUpdate( + ruleId, + { clientPrice }, + { new: true, session } + ); + } catch (error) { + throw new Error(`Error processing ruleId: ${ruleId}`, error); + } } - } - - await session.commitTransaction(); - - res.status(201).json({ - message: `${savedActivity.title} Activity approved successfully`, - activity: savedActivity, - }); - } catch (error) { - await session.abortTransaction(); - res.status(500).json({ - error: "Unexpected Server Error occured!", - message: error.message, - }); - } finally { - session.endSession(); - } + + await session.commitTransaction(); + + res.status(201).json({ + message: `${savedActivity.title} Activity approved successfully`, + activity: savedActivity, + }); + } catch (error) { + await session.abortTransaction(); + res.status(500).json({ + error: "Unexpected Server Error occured!", + message: error.message, + }); + } finally { + session.endSession(); + } }; export const rejectActivity = async (req, res) => { - const session = await mongoose.startSession(); - session.startTransaction(); - try { - console.log("reject activity body:", req.params.activityId); - const { activityId } = req.params; - const { rejectionReason, adminId } = req.body; - - const approvalStatusChangeLog = await saveApprovalStatusChangeLog( - ActivityApprovalStatusEnum.REJECTED, - rejectionReason, - activityId, - adminId, - session, - ); - - const savedActivity = await ActivityModel.findByIdAndUpdate( - activityId, - { - approvalStatus: ActivityApprovalStatusEnum.REJECTED, - rejectionReason, - $push: { - approvalStatusChangeLog: approvalStatusChangeLog._id, - }, - }, - { - new: true, - session, - }, - ); - - await session.commitTransaction(); - - res.status(201).json({ - message: `${savedActivity.title} rejected successfully`, - activity: savedActivity, - }); - } catch (error) { - await session.abortTransaction(); - res.status(500).json({ - error: "Unexpected Server Error occured!", - message: error.message, - }); - } finally { - session.endSession(); - } + const session = await mongoose.startSession(); + session.startTransaction(); + try { + console.log("reject activity body:", req.params.activityId); + const { activityId } = req.params; + const { rejectionReason, adminId } = req.body; + + const approvalStatusChangeLog = await saveApprovalStatusChangeLog( + ActivityApprovalStatusEnum.REJECTED, + rejectionReason, + activityId, + adminId, + session + ); + + const savedActivity = await ActivityModel.findByIdAndUpdate( + activityId, + { + approvalStatus: ActivityApprovalStatusEnum.REJECTED, + rejectionReason, + $push: { + approvalStatusChangeLog: approvalStatusChangeLog._id, + }, + }, + { + new: true, + session, + } + ); + + await session.commitTransaction(); + + res.status(201).json({ + message: `${savedActivity.title} rejected successfully`, + activity: savedActivity, + }); + } catch (error) { + await session.abortTransaction(); + res.status(500).json({ + error: "Unexpected Server Error occured!", + message: error.message, + }); + } finally { + session.endSession(); + } }; export const publishActivity = async (req, res) => { - const session = await mongoose.startSession(); - session.startTransaction(); - try { - console.log("publish activity body:", req.params.id); - const { id } = req.params; - - const savedActivity = await ActivityModel.findByIdAndUpdate( - id, - { - approvalStatus: ActivityApprovalStatusEnum.PUBLISHED, - createdDate: Date.now(), - }, - { - new: true, - session, - }, - ) - .populate({ - path: "approvalStatusChangeLog", - populate: { path: "admin", model: "Admin", select: "_id name" }, - }) - .populate("activityPricingRules") - .populate("theme") - .populate("subtheme"); - - console.log("savedActivity", savedActivity); - - await session.commitTransaction(); - - res.status(201).json({ - message: `${savedActivity.title} published successfully`, - activity: savedActivity, - }); - } catch (error) { - await session.abortTransaction(); - res.status(500).json({ - error: "Unexpected Server Error occured!", - message: error.message, - }); - } finally { - session.endSession(); - } + const session = await mongoose.startSession(); + session.startTransaction(); + try { + console.log("publish activity body:", req.params.id); + const { id } = req.params; + + const savedActivity = await ActivityModel.findByIdAndUpdate( + id, + { + approvalStatus: ActivityApprovalStatusEnum.PUBLISHED, + createdDate: Date.now(), + }, + { + new: true, + session, + } + ) + .populate({ + path: "approvalStatusChangeLog", + populate: { path: "admin", model: "Admin", select: "_id name" }, + }) + .populate("activityPricingRules") + .populate("theme") + .populate("subtheme"); + + console.log("savedActivity", savedActivity); + + await session.commitTransaction(); + + res.status(201).json({ + message: `${savedActivity.title} published successfully`, + activity: savedActivity, + }); + } catch (error) { + await session.abortTransaction(); + res.status(500).json({ + error: "Unexpected Server Error occured!", + message: error.message, + }); + } finally { + session.endSession(); + } }; export const deleteActivityDraft = async (req, res) => { - try { - const activityId = req.params.id; - const deletedActivity = await ActivityModel.findByIdAndDelete(activityId); - await ActivityPricingRulesModel.deleteMany( - { activity: activityId }, - { session } - ); - let activities; - if (deletedActivity.adminCreated) { - activities = await retrieveActivities(deletedActivity.adminCreated); - } else { - if (deletedActivity.parent) { - const parentId = deletedActivity.parent; - const newParent = await ActivityModel.findByIdAndUpdate( - parentId, - { - rejectedDraft: null, - }, - { new: true }, - ); - console.log("new parent", newParent); + try { + const activityId = req.params.id; + const deletedActivity = await ActivityModel.findByIdAndDelete(activityId); + await ActivityPricingRulesModel.deleteMany( + { activity: activityId }, + { session } + ); + let activities; + if (deletedActivity.adminCreated) { + activities = await retrieveActivities(deletedActivity.adminCreated); + } else { + if (deletedActivity.parent) { + const parentId = deletedActivity.parent; + const newParent = await ActivityModel.findByIdAndUpdate( + parentId, + { + rejectedDraft: null, + }, + { new: true } + ); + console.log("new parent", newParent); + } + + console.log("Retrieving all vendor activities", req.user); + activities = await getAllVendorActivities(req.user._id); } - - console.log("Retrieving all vendor activities", req.user); - activities = await getAllVendorActivities(req.user._id); - } - res.status(201).json({ - message: "Activity draft deleted successfully!", - activity: activities, - }); - } catch (e) { - console.error("error when deleting draft", e); - res.status(500).json({ - error: "Error when deleting activity draft!", - message: e.message, - }); - } + res.status(201).json({ + message: "Activity draft deleted successfully!", + activity: activities, + }); + } catch (e) { + console.error("error when deleting draft", e); + res.status(500).json({ + error: "Error when deleting activity draft!", + message: e.message, + }); + } }; export const bulkDeleteActivityDraft = async (req, res) => { - try { - console.log("bulkDeleteActivityDraft", req.body); - const activityIds = req.body; - const deletedActivity = await ActivityModel.findOne({ _id: activityIds }); - const activitiesToDelete = await ActivityModel.find({ - _id: { $in: activityIds }, - }); - const { deletedCount } = await ActivityModel.deleteMany({ - _id: activityIds, - }); - const thing = await ActivityPricingRulesModel.find({ - activity: { $in: activityIds }, - }); - const ting = await ActivityPricingRulesModel.deleteMany({ - activity: { $in: activityIds }, - }); - let activities; - if (deletedActivity.adminCreated) { - activities = await retrieveActivities(deletedActivity.adminCreated); - } else { - activitiesToDelete.forEach(async (deletedActivity) => { - if (deletedActivity.parent) { - const parentId = deletedActivity.parent; - await ActivityModel.findByIdAndUpdate( - parentId, - { - rejectedDraft: null, - }, - { new: true }, - ); - } + try { + console.log("bulkDeleteActivityDraft", req.body); + const activityIds = req.body; + const deletedActivity = await ActivityModel.findOne({ _id: activityIds }); + const activitiesToDelete = await ActivityModel.find({ + _id: { $in: activityIds }, + }); + const { deletedCount } = await ActivityModel.deleteMany({ + _id: activityIds, }); - activities = await getAllVendorActivities(req.user._id); - } - - res.status(201).json({ - message: `Deleted ${deletedCount} Activity${ - deletedCount > 1 ? " Drafts" : " Draft" - } successfully!`, - activity: activities, - }); - } catch (e) { - res.status(500).json({ - error: "Error when bulk deleting activity drafts!", - message: e.message, - }); - } + const thing = await ActivityPricingRulesModel.find({ + activity: { $in: activityIds }, + }); + const ting = await ActivityPricingRulesModel.deleteMany({ + activity: { $in: activityIds }, + }); + let activities; + if (deletedActivity.adminCreated) { + activities = await retrieveActivities(deletedActivity.adminCreated); + } else { + activitiesToDelete.forEach(async (deletedActivity) => { + if (deletedActivity.parent) { + const parentId = deletedActivity.parent; + await ActivityModel.findByIdAndUpdate( + parentId, + { + rejectedDraft: null, + }, + { new: true } + ); + } + }); + activities = await getAllVendorActivities(req.user._id); + } + + res.status(201).json({ + message: `Deleted ${deletedCount} Activity${ + deletedCount > 1 ? " Drafts" : " Draft" + } successfully!`, + activity: activities, + }); + } catch (e) { + res.status(500).json({ + error: "Error when bulk deleting activity drafts!", + message: e.message, + }); + } }; const retrieveActivities = async (adminId) => { - return await ActivityModel.find() - .populate({ - path: "adminCreated", - match: { _id: adminId }, - }) - .populate("activityPricingRules") - .populate("theme") - .populate("subtheme") - .populate("linkedVendor") - .populate({ - path: "approvalStatusChangeLog", - populate: { path: "admin", model: "Admin", select: "_id name" }, - }); + return await ActivityModel.find() + .populate({ + path: "adminCreated", + match: { _id: adminId }, + }) + .populate("activityPricingRules") + .populate("theme") + .populate("subtheme") + .populate("linkedVendor") + .populate({ + path: "approvalStatusChangeLog", + populate: { path: "admin", model: "Admin", select: "_id name" }, + }); }; export const bulkAddThemes = async (req, res) => { - try { - const { data } = req.body; - processThemes(data) - .then(() => { - console.log("Themes added successfully"); - res.status(201).json({ - message: "Themes added successfully", - }); - }) - .catch((err) => { - console.error(err); - res - .status(500) - .json({ err: "Themes cannot be added", message: err.message }); + try { + const { data } = req.body; + processThemes(data) + .then(() => { + console.log("Themes added successfully"); + res.status(201).json({ + message: "Themes added successfully", + }); + }) + .catch((err) => { + console.error(err); + res.status(500).json({ + err: "Themes cannot be added", + message: err.message, + }); + }); + } catch (error) { + console.log(error); + res.status(500).json({ + error: "Themes cannot be added", + message: error.message, }); - } catch (error) { - console.log(error); - res - .status(500) - .json({ error: "Themes cannot be added", message: error.message }); - } + } }; const processThemes = async (data) => { - for (const { name, parent } of data) { - let parentTheme = null; - - if (parent) { - console.log(parent); - parentTheme = await ThemeModel.findOne({ name: parent }); - } - - if (!parentTheme) { - parentTheme = new ThemeModel({ name: parent }); - await parentTheme.save(); - } - - const childTheme = new ThemeModel({ name, parent: parentTheme }); - await childTheme.save(); - } + for (const { name, parent } of data) { + let parentTheme = null; + + if (parent) { + console.log(parent); + parentTheme = await ThemeModel.findOne({ name: parent }); + } + + if (!parentTheme) { + parentTheme = new ThemeModel({ name: parent }); + await parentTheme.save(); + } + + const childTheme = new ThemeModel({ name, parent: parentTheme }); + await childTheme.save(); + } }; export const getAllThemes = async (req, res) => { - try { - const themes = await ThemeModel.find().populate("parent"); + try { + const themes = await ThemeModel.find().populate("parent"); - const parentThemesWithChildren = {}; + const parentThemesWithChildren = {}; - themes.forEach((theme) => { - const parentId = theme.parent ? theme.parent._id.toString() : null; + themes.forEach((theme) => { + const parentId = theme.parent ? theme.parent._id.toString() : null; - if (!parentThemesWithChildren[parentId]) { - parentThemesWithChildren[parentId] = { - parent: theme.parent, - children: [], - }; - } + if (!parentThemesWithChildren[parentId]) { + parentThemesWithChildren[parentId] = { + parent: theme.parent, + children: [], + }; + } - parentThemesWithChildren[parentId].children.push(theme); - }); - const parentThemes = Object.values(parentThemesWithChildren); - res.status(200).json({ - data: parentThemes, - }); - } catch (error) { - console.log(error); - res - .status(500) - .json({ error: "Themes cannot be added", message: error.message }); - } + parentThemesWithChildren[parentId].children.push(theme); + }); + const parentThemes = Object.values(parentThemesWithChildren); + res.status(200).json({ + data: parentThemes, + }); + } catch (error) { + console.log(error); + res.status(500).json({ + error: "Themes cannot be added", + message: error.message, + }); + } }; export const getActivitiesWithFilters = async (req, res) => { - try { - const { filter, searchValue } = req.body; - - // Initialize the query - const query = {}; + try { + const { filter, searchValue } = req.body; + + // Initialize the query + const query = {}; + + if (searchValue != null && searchValue.length > 0) { + // If searchValue is provided, add title search to the query + query.title = { + $regex: new RegExp(searchValue, "i"), // "i" makes the regex case-insensitive + }; + console.log(searchValue); + } - if (searchValue != null && searchValue.length > 0) { - // If searchValue is provided, add title search to the query - query.title = { - $regex: new RegExp(searchValue, "i"), // "i" makes the regex case-insensitive - }; - console.log(searchValue); - } + if (filter.locations.length > 0) { + // Add location filter when locations is not empty + query.location = { + $in: filter.locations, + }; + } - if (filter.locations.length > 0) { - // Add location filter when locations is not empty - query.location = { - $in: filter.locations, - }; - } + // Convert string IDs to ObjectId instances + const subthemeIds = filter.themes.map( + (id) => new mongoose.Types.ObjectId(id) + ); - // Convert string IDs to ObjectId instances - const subthemeIds = filter.themes.map( - (id) => new mongoose.Types.ObjectId(id), - ); + if (subthemeIds.length > 0) { + // Add subtheme filter when subthemes is not empty + query.subtheme = { + $in: subthemeIds, + }; + } - if (subthemeIds.length > 0) { - // Add subtheme filter when subthemes is not empty - query.subtheme = { - $in: subthemeIds, - }; - } + if (filter.sgs.length > 0) { + query.sdg = { + $in: filter.sgs, + }; + } - if (filter.sgs.length > 0) { - query.sdg = { - $in: filter.sgs, - }; - } + if (filter.daysAvailability.length > 0) { + query.dayAvailabilities = { + $in: filter.daysAvailability, + }; + } - if (filter.daysAvailability.length > 0) { - query.dayAvailabilities = { - $in: filter.daysAvailability, - }; - } + if (filter.activityType.length > 0) { + query.activityType = { + $in: filter.activityType, + }; + } - if (filter.activityType.length > 0) { - query.activityType = { - $in: filter.activityType, - }; - } + if (filter.duration.length > 0) { + query.duration = { + $in: filter.duration, + }; + } - if (filter.duration.length > 0) { - query.duration = { - $in: filter.duration, - }; - } - - if (filter.priceRange[0] !== null && filter.priceRange[1] !== null) { - const pricingRules = await ActivityPricingRulesModel.find({ - clientPrice: { - $gte: filter.priceRange[0], // Greater than or equal to minPrice - $lte: filter.priceRange[1], // Less than or equal to maxPrice - }, - }); - const pricingRuleIds = pricingRules.map((rule) => rule._id); - query.activityPricingRules = { - $in: pricingRuleIds, - }; - } + if (filter.priceRange[0] !== null && filter.priceRange[1] !== null) { + const pricingRules = await ActivityPricingRulesModel.find({ + clientPrice: { + $gte: filter.priceRange[0], // Greater than or equal to minPrice + $lte: filter.priceRange[1], // Less than or equal to maxPrice + }, + }); + const pricingRuleIds = pricingRules.map((rule) => rule._id); + query.activityPricingRules = { + $in: pricingRuleIds, + }; + } - // Add the condition activities on client - query.isDraft = false; - query.disabled = false; - query.approvalStatus = "Published"; + // Add the condition activities on client + query.isDraft = false; + query.disabled = false; + query.approvalStatus = "Published"; + + const activities = await ActivityModel.find(query) + .populate("activityPricingRules") + .populate("linkedVendor"); + + // Create a function to find the minimum price per pax for each activity + async function findMinimumPricePerPax(activity) { + let minPricePerPax = Infinity; + for (const pricingRule of activity.activityPricingRules) { + if (pricingRule.clientPrice < minPricePerPax) { + minPricePerPax = pricingRule.clientPrice; + } + } + return minPricePerPax; + } - const activities = await ActivityModel.find(query) - .populate("activityPricingRules") - .populate("linkedVendor"); - - // Create a function to find the minimum price per pax for each activity - async function findMinimumPricePerPax(activity) { - let minPricePerPax = Infinity; - for (const pricingRule of activity.activityPricingRules) { - if (pricingRule.clientPrice < minPricePerPax) { - minPricePerPax = pricingRule.clientPrice; - } + // Populate the minimum price per pax for each activity + for (const activity of activities) { + activity.minimumPricePerPax = await findMinimumPricePerPax(activity); + activity.preSignedImages = await s3GetImages(activity.images); } - return minPricePerPax; - } - - // Populate the minimum price per pax for each activity - for (const activity of activities) { - activity.minimumPricePerPax = await findMinimumPricePerPax(activity); - activity.preSignedImages = await s3GetImages(activity.images); - } - // console.log( - // "********************************************************************************" - // ); - // console.log(activities); - - return res.status(200).json({ - success: true, - message: "Filtered activities fetched!", - activities: activities, - }); - } catch (error) { - console.log(error); - res.status(500).json({ error: true, msg: "Server error" }); - } + // console.log( + // "********************************************************************************" + // ); + // console.log(activities); + + return res.status(200).json({ + success: true, + message: "Filtered activities fetched!", + activities: activities, + }); + } catch (error) { + console.log(error); + res.status(500).json({ error: true, msg: "Server error" }); + } }; export const getAllActivitiesNames = async (req, res) => { - try { - // Query the collection to get titles of all documents - const activityTitles = await ActivityModel.find( - { isDraft: false }, - "title", - ); - - // Extract the titles from the result - const titles = activityTitles.map((activity) => activity.title); - - res.status(200).json({ - success: true, - data: titles, - message: "Activity titles fetched!", - }); - } catch (error) { - console.log(error); - res.status(500).json({ error: true, msg: "Server error" }); - } + try { + // Query the collection to get titles of all documents + const activityTitles = await ActivityModel.find( + { isDraft: false }, + "title" + ); + + // Extract the titles from the result + const titles = activityTitles.map((activity) => activity.title); + + res.status(200).json({ + success: true, + data: titles, + message: "Activity titles fetched!", + }); + } catch (error) { + console.log(error); + res.status(500).json({ error: true, msg: "Server error" }); + } }; export const getMinAndMaxPricePerPax = async (req, res) => { - try { - const activities = await ActivityModel.find({}).populate( - "activityPricingRules", - ); - if (activities.length === 0) { - return res.status(200).send({ - success: true, - msg: "No activities found!", - minPrice: null, - maxPrice: null, - }); - } - - const pricingRules = activities.flatMap( - (activity) => activity.activityPricingRules, - ); - - if (pricingRules.length === 0) { - return res.status(200).send({ - success: true, - msg: "No pricing rules found!", - minPrice: null, - maxPrice: null, + try { + const activities = await ActivityModel.find({}).populate( + "activityPricingRules" + ); + if (activities.length === 0) { + return res.status(200).send({ + success: true, + msg: "No activities found!", + minPrice: null, + maxPrice: null, + }); + } + + const pricingRules = activities.flatMap( + (activity) => activity.activityPricingRules + ); + + if (pricingRules.length === 0) { + return res.status(200).send({ + success: true, + msg: "No pricing rules found!", + minPrice: null, + maxPrice: null, + }); + } + + const minPrice = Math.min( + ...pricingRules.map((rule) => rule.clientPrice) + ); + const maxPrice = Math.max( + ...pricingRules.map((rule) => rule.clientPrice) + ); + + // console.log("Minimum Price Per Pax:", minPrice); + // console.log("Maximum Price Per Pax:", maxPrice); + + res.status(200).json({ + success: true, + message: "Maximum and minimum prices fetched!", + minPrice: minPrice, + maxPrice: maxPrice, }); - } - - const minPrice = Math.min(...pricingRules.map((rule) => rule.clientPrice)); - const maxPrice = Math.max(...pricingRules.map((rule) => rule.clientPrice)); - - // console.log("Minimum Price Per Pax:", minPrice); - // console.log("Maximum Price Per Pax:", maxPrice); - - res.status(200).json({ - success: true, - message: "Maximum and minimum prices fetched!", - minPrice: minPrice, - maxPrice: maxPrice, - }); - } catch (error) { - console.log(error); - res.status(500).json({ error: "Server error", message: error.message }); - } + } catch (error) { + console.log(error); + res.status(500).json({ error: "Server error", message: error.message }); + } }; /** @@ -1016,38 +1038,81 @@ export const getMinAndMaxPricePerPax = async (req, res) => { */ export const getVendorActivities = async (req, res) => { - try { - const vendor = req.user; - const vendorId = vendor._id; - console.log("getVendorActivities vendor _id", vendorId); - - const activities = await getAllVendorActivities(vendorId); - const preSignedPromises = activities.map(async (activity) => { - await findMinimumPricePerPax(activity); - }); - - await Promise.all(preSignedPromises); - - res.status(200).json(activities); - } catch (error) { - console.error(error); - res.status(500).json({ message: "Server Error" }); - } + try { + const vendor = req.user; + const vendorId = vendor._id; + console.log("getVendorActivities vendor _id", vendorId); + + const activities = await getAllVendorActivities(vendorId); + const preSignedPromises = activities.map(async (activity) => { + await findMinimumPricePerPax(activity); + }); + + await Promise.all(preSignedPromises); + + res.status(200).json(activities); + } catch (error) { + console.error(error); + res.status(500).json({ message: "Server Error" }); + } }; export const getActivityTitle = async (req, res) => { - try { - const foundActivity = await ActivityModel.findById( - req.params.activityId, - "title", - ); - - if (!foundActivity) { - return res.status(404).json({ message: "Activity not found" }); - } - - res.status(200).json(foundActivity.title); - } catch (error) { - res.status(500).json({ message: error.message }); - } + try { + const foundActivity = await ActivityModel.findById( + req.params.activityId, + "title" + ); + + if (!foundActivity) { + return res.status(404).json({ message: "Activity not found" }); + } + + res.status(200).json(foundActivity.title); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export const getQuotationPdfUrl = async (req, res) => { + const data = req.body; + const pdfContent = InvoiceTemplate(data); + const filename = "quotation" + Date.now() + ".pdf"; + const pdfFilePath = path.join(process.cwd(), "temp", filename); + pdf.create(pdfContent, {}).toFile(pdfFilePath, (err) => { + if (err) { + // Handle errors appropriately + console.error(err); + res.status(500).send("Error generating PDF"); + } else { + // Respond with a success message or the file path + res.status(200).send(filename); + } + }); +}; + +export const getQuotationPdf = async (req, res) => { + const filePath = req.params; + const file = path.join(process.cwd(), "temp", filePath.path); + fs.access(file, fs.constants.F_OK, (err) => { + if (err) { + console.error("File does not exist or cannot be accessed:", err); + } else { + // File exists, proceed to serve and then delete + res.download(file, "quotation.pdf", (downloadError) => { + if (downloadError) { + console.error("Error downloading the file:", downloadError); + } else { + // File has been successfully sent to the client, now delete it + fs.unlink(file, (deleteError) => { + if (deleteError) { + console.error("Error deleting the file:", deleteError); + } else { + console.log("File deleted successfully."); + } + }); + } + }); + } + }); }; diff --git a/server/routes/gleek/shop.js b/server/routes/gleek/shop.js index a7ef25d..8290c44 100644 --- a/server/routes/gleek/shop.js +++ b/server/routes/gleek/shop.js @@ -1,6 +1,10 @@ import express from "express"; import { check } from "express-validator"; -import { getActivitiesWithFilters } from "../../controller/activityController.js"; +import { + getActivitiesWithFilters, + getQuotationPdf, + getQuotationPdfUrl, +} from "../../controller/activityController.js"; import { getAllThemes } from "../../controller/activityController.js"; import { getAllActivitiesNames } from "../../controller/activityController.js"; import { getMinAndMaxPricePerPax } from "../../controller/activityController.js"; @@ -21,4 +25,8 @@ router.get("/getAllActivitiesNames", verifyToken, getAllActivitiesNames); router.get("/getMinAndMaxPricePerPax", verifyToken, getMinAndMaxPricePerPax); router.get("/viewActivity/:id", verifyToken, getActivity); + +router.post("/getQuotationPdfUrl", verifyToken, getQuotationPdfUrl); + +router.get("/getQuotationPdf/:path", verifyToken, getQuotationPdf); export default router; diff --git a/server/server.js b/server/server.js index 7eb0107..860bc73 100644 --- a/server/server.js +++ b/server/server.js @@ -60,29 +60,6 @@ app.use("/gleekVendor", gleekVendorRoutes); app.use("/testActivity", activityTestController); app.use("/notification", notificationRoutes); -app.get("/pdf", (req, res, next) => { - const booking = { - client: { - name: "Yunus", - }, - startDateTime: "2023-10-20T01:00:00.000+00:00", - endDateTime: "2023-10-20T04:00:00.000+00:00", - totalCost: 900, - totalPax: 20, - activityTitle: "Coffee Grounds", - vendorName: "Sustainability Project", - status: "PENDING_CONFIRMATION", - billingAddress: "test", - billingPostalCode: "1", - }; - - pdf.create(InvoiceTemplate(booking), {}).toStream(function (err, stream) { - res.setHeader("Content-Type", "appplication/pdf"); - res.setHeader("Content-Disposition", "inline;filename=test.pdf"); - stream.pipe(res); - }); -}); - app.listen(port, () => { console.log(`Server is running on port: ${port}`); }); From c3a2f3749d7818753a1c10bdbeb5fe4cdca6b909 Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:36:22 +0800 Subject: [PATCH 02/11] Enhanced quotation --- client-frontend/package-lock.json | 906 ++++++++++++------ .../ActivityDetailsPage.jsx | 24 + server/assets/templates/QuotationTemplate.js | 148 +++ server/assets/templates/header.js | 6 +- server/controller/activityController.js | 6 +- server/server.js | 2 - 6 files changed, 793 insertions(+), 299 deletions(-) create mode 100644 server/assets/templates/QuotationTemplate.js diff --git a/client-frontend/package-lock.json b/client-frontend/package-lock.json index 19a8d3e..1936dea 100644 --- a/client-frontend/package-lock.json +++ b/client-frontend/package-lock.json @@ -8,6 +8,9 @@ "name": "client-frontend", "version": "0.1.0", "dependencies": { + "@devexpress/dx-react-core": "^4.0.5", + "@devexpress/dx-react-scheduler": "^4.0.5", + "@devexpress/dx-react-scheduler-material-ui": "^4.0.5", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.14.7", @@ -24,6 +27,7 @@ "react": "^18.2.0", "react-autosuggest": "^10.1.0", "react-dom": "^18.2.0", + "react-moment": "^1.1.3", "react-router-dom": "^6.15.0", "react-scripts": "5.0.1", "swiper": "^10.3.1", @@ -31,6 +35,8 @@ "zustand": "^4.4.1" }, "devDependencies": { + "eslint": "^8.49.0", + "eslint-plugin-auto-import": "^0.1.1", "prettier": "^3.0.3" } }, @@ -2282,6 +2288,111 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@date-io/core": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" + }, + "node_modules/@date-io/moment": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.3.13.tgz", + "integrity": "sha512-3kJYusJtQuOIxq6byZlzAHoW/18iExJer9qfRF5DyyzdAk074seTuJfdofjz4RFfTd/Idk8WylOQpWtERqvFuQ==", + "dependencies": { + "@date-io/core": "^1.3.13" + }, + "peerDependencies": { + "moment": "^2.24.0" + } + }, + "node_modules/@devexpress/dx-core": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@devexpress/dx-core/-/dx-core-4.0.5.tgz", + "integrity": "sha512-z/d4rRSDxZKGRW/24ncvIqj1ba6Jyjv08pipqY1xTsQCDiZJwSGlSrY02Al9tQ5tF0u4kWMGo3khh/ml/cKR9g==" + }, + "node_modules/@devexpress/dx-react-core": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@devexpress/dx-react-core/-/dx-react-core-4.0.5.tgz", + "integrity": "sha512-gaYUhmR65PeuWR/E+tpyHwCVIbdAeVJwiXu+ly9h6YdEcOKaLjOO52lWut3VBW7WhQCsCGq59gezmBVeNAv5oQ==", + "dependencies": { + "@devexpress/dx-core": "4.0.5", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=17.0.2", + "react-dom": ">=17.0.2" + } + }, + "node_modules/@devexpress/dx-react-scheduler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@devexpress/dx-react-scheduler/-/dx-react-scheduler-4.0.5.tgz", + "integrity": "sha512-JllbcA0Mll+AKjIXr9Gn7Mm6DkWib7CbkUPZ6UPON28KjMGxxkQU65PCEa1VFIBqYBfYC1ulgjnj5jrxKLwKRg==", + "dependencies": { + "@devexpress/dx-scheduler-core": "4.0.5" + }, + "peerDependencies": { + "@devexpress/dx-core": "4.0.5", + "@devexpress/dx-react-core": "4.0.5", + "moment": "^2.24.0", + "react": ">=17.0.2", + "react-dom": ">=17.0.2" + } + }, + "node_modules/@devexpress/dx-react-scheduler-material-ui": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@devexpress/dx-react-scheduler-material-ui/-/dx-react-scheduler-material-ui-4.0.5.tgz", + "integrity": "sha512-h1ogj2G99V3AhgXS+WIr6sndKvGNcdsRmlqHRlA1BVy2pNoK6lecHbJagrDF0GRHoDt3D68xWLuxDCmL/RlJUA==", + "dependencies": { + "@date-io/moment": "^1.3.11", + "clsx": "^1.0.4", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@devexpress/dx-react-core": "4.0.5", + "@devexpress/dx-react-scheduler": "4.0.5", + "@devexpress/dx-scheduler-core": "4.0.5", + "@emotion/react": ">=11.4.1", + "@emotion/styled": ">=11.3.0", + "@mui/icons-material": ">=5.0.0", + "@mui/lab": "^5.0.0-alpha.117", + "@mui/material": ">=5.0.0", + "@mui/x-date-pickers": "^5.0.15", + "moment": "^2.24.0", + "react": ">=17.0.2" + } + }, + "node_modules/@devexpress/dx-react-scheduler-material-ui/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@devexpress/dx-scheduler-core": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@devexpress/dx-scheduler-core/-/dx-scheduler-core-4.0.5.tgz", + "integrity": "sha512-quMQcF9sAlGKJ+08QLmTzpWivB/pmuJtMvsz4pasM3HZOAw4gdDgmMerrxXYgbmDAp9YkH9Pxvs4iw5oAv7Bbw==", + "dependencies": { + "moment": "^2.24.0", + "rrule": "2.7.1" + }, + "peerDependencies": { + "@devexpress/dx-core": "4.0.5" + } + }, + "node_modules/@dsherret/to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha512-InCaQ/KEOcFtAFztn47wadritBLP2nT6m/ucbBnIgI5YwxuMzKKCHtqazR2+D1yR6y1ZTnPea9aLFEUrTttUSQ==", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -2520,9 +2631,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2562,11 +2673,11 @@ "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -2587,9 +2698,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==" }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -4157,104 +4268,6 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@testing-library/dom": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", - "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/dom/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "peer": true, - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/@testing-library/dom/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/dom/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "peer": true - }, - "node_modules/@testing-library/dom/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/dom/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/jest-dom": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", @@ -4475,6 +4488,45 @@ "node": ">=10.13.0" } }, + "node_modules/@ts-morph/common": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.7.5.tgz", + "integrity": "sha512-nlFunSKAsFWI0Ol/uPxJcpVqXxTGNuaWXTmoQDhcnwj1UM4QmBSUVWzqoQ0OzUlqo4sV1gobfFBkMHuZVemMAQ==", + "dev": true, + "dependencies": { + "@dsherret/to-absolute-glob": "^2.0.2", + "fast-glob": "^3.2.5", + "is-negated-glob": "^1.0.0", + "mkdirp": "^1.0.4", + "multimatch": "^5.0.0", + "typescript": "~4.1.3" + } + }, + "node_modules/@ts-morph/common/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ts-morph/common/node_modules/typescript": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", + "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", @@ -4894,6 +4946,12 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, "node_modules/@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", @@ -5270,6 +5328,11 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -5667,6 +5730,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -5799,6 +5871,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -6659,6 +6740,12 @@ "node": ">= 4.0" } }, + "node_modules/code-block-writer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-10.1.1.tgz", + "integrity": "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==", + "dev": true + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -8038,17 +8125,18 @@ } }, "node_modules/eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -8159,6 +8247,19 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-auto-import": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-auto-import/-/eslint-plugin-auto-import-0.1.1.tgz", + "integrity": "sha512-H3RX0wmRq/MLIf3ZMLEy9OyvQKUGxWIsi+TJenicgGptJzdQnQe6XheGlFlqnqUjpy3CQW/1KiF7UNTNxM6Xhw==", + "dev": true, + "dependencies": { + "requireindex": "~1.1.0", + "ts-morph": "^9.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint-plugin-flowtype": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", @@ -10040,6 +10141,19 @@ "node": ">= 10" } }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -10247,6 +10361,15 @@ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -10335,6 +10458,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-root": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", @@ -10420,6 +10555,18 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -10451,6 +10598,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -13188,6 +13344,25 @@ "multicast-dns": "cli.js" } }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -15481,6 +15656,16 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-moment": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.1.3.tgz", + "integrity": "sha512-8EPvlUL8u6EknPp1ISF5MQ3wx2OHJVXIP/iZc4wRh3iV3XozftZERDv9ANZeAtMlhNNQHdFoqcZHFUkBSTONfA==", + "peerDependencies": { + "moment": "^2.29.0", + "prop-types": "^15.7.0", + "react": "^16.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15817,6 +16002,15 @@ "node": ">=0.10.0" } }, + "node_modules/requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==", + "dev": true, + "engines": { + "node": ">=0.10.5" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -16026,6 +16220,14 @@ "node": ">=8" } }, + "node_modules/rrule": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.1.tgz", + "integrity": "sha512-4p20u/1U7WqR3Nb1hOUrm0u1nSI7sO93ZUVZEZ5HeF6Gr5OlJuyhwEGRvUHq8ZfrPsq5gfa5b9dqnUs/kPqpIw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -17282,6 +17484,17 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/ts-morph": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-9.1.0.tgz", + "integrity": "sha512-sei4u651MBenr27sD6qLDXN3gZ4thiX71E3qV7SuVtDas0uvK2LtgZkIYUf9DKm/fLJ6AB/+yhRJ1vpEBJgy7Q==", + "dev": true, + "dependencies": { + "@dsherret/to-absolute-glob": "^2.0.2", + "@ts-morph/common": "~0.7.0", + "code-block-writer": "^10.1.1" + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -17447,19 +17660,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -17474,6 +17674,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -19079,8 +19288,7 @@ "@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "requires": {} + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==" }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", @@ -20089,14 +20297,83 @@ "@csstools/postcss-unset-value": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", - "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", - "requires": {} + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==" }, "@csstools/selector-specificity": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", - "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", - "requires": {} + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==" + }, + "@date-io/core": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" + }, + "@date-io/moment": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.3.13.tgz", + "integrity": "sha512-3kJYusJtQuOIxq6byZlzAHoW/18iExJer9qfRF5DyyzdAk074seTuJfdofjz4RFfTd/Idk8WylOQpWtERqvFuQ==", + "requires": { + "@date-io/core": "^1.3.13" + } + }, + "@devexpress/dx-core": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@devexpress/dx-core/-/dx-core-4.0.5.tgz", + "integrity": "sha512-z/d4rRSDxZKGRW/24ncvIqj1ba6Jyjv08pipqY1xTsQCDiZJwSGlSrY02Al9tQ5tF0u4kWMGo3khh/ml/cKR9g==" + }, + "@devexpress/dx-react-core": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@devexpress/dx-react-core/-/dx-react-core-4.0.5.tgz", + "integrity": "sha512-gaYUhmR65PeuWR/E+tpyHwCVIbdAeVJwiXu+ly9h6YdEcOKaLjOO52lWut3VBW7WhQCsCGq59gezmBVeNAv5oQ==", + "requires": { + "@devexpress/dx-core": "4.0.5", + "prop-types": "^15.7.2" + } + }, + "@devexpress/dx-react-scheduler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@devexpress/dx-react-scheduler/-/dx-react-scheduler-4.0.5.tgz", + "integrity": "sha512-JllbcA0Mll+AKjIXr9Gn7Mm6DkWib7CbkUPZ6UPON28KjMGxxkQU65PCEa1VFIBqYBfYC1ulgjnj5jrxKLwKRg==", + "requires": { + "@devexpress/dx-scheduler-core": "4.0.5" + } + }, + "@devexpress/dx-react-scheduler-material-ui": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@devexpress/dx-react-scheduler-material-ui/-/dx-react-scheduler-material-ui-4.0.5.tgz", + "integrity": "sha512-h1ogj2G99V3AhgXS+WIr6sndKvGNcdsRmlqHRlA1BVy2pNoK6lecHbJagrDF0GRHoDt3D68xWLuxDCmL/RlJUA==", + "requires": { + "@date-io/moment": "^1.3.11", + "clsx": "^1.0.4", + "prop-types": "^15.7.2" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } + } + }, + "@devexpress/dx-scheduler-core": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@devexpress/dx-scheduler-core/-/dx-scheduler-core-4.0.5.tgz", + "integrity": "sha512-quMQcF9sAlGKJ+08QLmTzpWivB/pmuJtMvsz4pasM3HZOAw4gdDgmMerrxXYgbmDAp9YkH9Pxvs4iw5oAv7Bbw==", + "requires": { + "moment": "^2.24.0", + "rrule": "2.7.1" + } + }, + "@dsherret/to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha512-InCaQ/KEOcFtAFztn47wadritBLP2nT6m/ucbBnIgI5YwxuMzKKCHtqazR2+D1yR6y1ZTnPea9aLFEUrTttUSQ==", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } }, "@emotion/babel-plugin": { "version": "11.11.0", @@ -20211,8 +20488,7 @@ "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", - "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", - "requires": {} + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==" }, "@emotion/utils": { "version": "1.2.1", @@ -20282,9 +20558,9 @@ } }, "@eslint/js": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", - "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==" + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", + "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==" }, "@floating-ui/core": { "version": "1.4.1", @@ -20317,11 +20593,11 @@ "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" }, "@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "requires": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" } @@ -20332,9 +20608,9 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==" }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -20987,8 +21263,7 @@ "@mui/types": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.5.tgz", - "integrity": "sha512-S2BwfNczr7VwS6ki8GoAXJyARoeSJDLuxOEPs3vEMyTALlf9PrdHv+sluX7kk3iKrCg/ML2mIWwapZvWbkMCQA==", - "requires": {} + "integrity": "sha512-S2BwfNczr7VwS6ki8GoAXJyARoeSJDLuxOEPs3vEMyTALlf9PrdHv+sluX7kk3iKrCg/ML2mIWwapZvWbkMCQA==" }, "@mui/utils": { "version": "5.14.12", @@ -21317,82 +21592,6 @@ "loader-utils": "^2.0.0" } }, - "@testing-library/dom": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", - "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", - "peer": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "peer": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "peer": true, - "requires": { - "deep-equal": "^2.0.5" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "peer": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "peer": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "peer": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "peer": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "peer": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "@testing-library/jest-dom": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", @@ -21550,6 +21749,34 @@ "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" }, + "@ts-morph/common": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.7.5.tgz", + "integrity": "sha512-nlFunSKAsFWI0Ol/uPxJcpVqXxTGNuaWXTmoQDhcnwj1UM4QmBSUVWzqoQ0OzUlqo4sV1gobfFBkMHuZVemMAQ==", + "dev": true, + "requires": { + "@dsherret/to-absolute-glob": "^2.0.2", + "fast-glob": "^3.2.5", + "is-negated-glob": "^1.0.0", + "mkdirp": "^1.0.4", + "multimatch": "^5.0.0", + "typescript": "~4.1.3" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "typescript": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", + "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==", + "dev": true + } + } + }, "@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", @@ -21916,6 +22143,12 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, "@types/node": { "version": "20.5.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", @@ -22189,6 +22422,11 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -22368,14 +22606,12 @@ "acorn-import-assertions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "requires": {} + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==" }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "requires": {} + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" }, "acorn-walk": { "version": "7.2.0", @@ -22444,8 +22680,7 @@ "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" }, "ansi-escapes": { "version": "4.3.2", @@ -22517,6 +22752,12 @@ "is-array-buffer": "^3.0.1" } }, + "array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true + }, "array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -22610,6 +22851,12 @@ "is-shared-array-buffer": "^1.0.2" } }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -22820,8 +23067,7 @@ "babel-plugin-named-asset-import": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz", - "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==", - "requires": {} + "integrity": "sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==" }, "babel-plugin-polyfill-corejs2": { "version": "0.4.5", @@ -23235,6 +23481,12 @@ "q": "^1.1.2" } }, + "code-block-writer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-10.1.1.tgz", + "integrity": "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==", + "dev": true + }, "collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -23437,8 +23689,7 @@ "css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", - "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", - "requires": {} + "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==" }, "css-has-pseudo": { "version": "3.0.4", @@ -23521,8 +23772,7 @@ "css-prefers-color-scheme": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", - "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", - "requires": {} + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==" }, "css-select": { "version": "4.3.0", @@ -23626,8 +23876,7 @@ "cssnano-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", - "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "requires": {} + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==" }, "csso": { "version": "4.2.0", @@ -24264,17 +24513,18 @@ } }, "eslint": { - "version": "8.48.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", - "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "version": "8.52.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", + "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.48.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint/js": "8.52.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -24442,6 +24692,16 @@ } } }, + "eslint-plugin-auto-import": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-auto-import/-/eslint-plugin-auto-import-0.1.1.tgz", + "integrity": "sha512-H3RX0wmRq/MLIf3ZMLEy9OyvQKUGxWIsi+TJenicgGptJzdQnQe6XheGlFlqnqUjpy3CQW/1KiF7UNTNxM6Xhw==", + "dev": true, + "requires": { + "requireindex": "~1.1.0", + "ts-morph": "^9.1.0" + } + }, "eslint-plugin-flowtype": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz", @@ -24587,8 +24847,7 @@ "eslint-plugin-react-hooks": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "requires": {} + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==" }, "eslint-plugin-testing-library": { "version": "5.11.1", @@ -25611,8 +25870,7 @@ "icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "requires": {} + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==" }, "idb": { "version": "7.1.1", @@ -25706,6 +25964,16 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==" }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, "is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -25838,6 +26106,12 @@ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true + }, "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -25890,6 +26164,15 @@ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==" }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, "is-root": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", @@ -25942,6 +26225,15 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, "is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -25964,6 +26256,12 @@ "get-intrinsic": "^1.1.1" } }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -26749,8 +27047,7 @@ "jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "requires": {} + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==" }, "jest-regex-util": { "version": "27.5.1", @@ -27987,6 +28284,19 @@ "thunky": "^1.0.2" } }, + "multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "requires": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + } + }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -28485,8 +28795,7 @@ "postcss-browser-comments": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", - "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", - "requires": {} + "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==" }, "postcss-calc": { "version": "8.2.4", @@ -28584,26 +28893,22 @@ "postcss-discard-comments": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", - "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", - "requires": {} + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==" }, "postcss-discard-duplicates": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "requires": {} + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==" }, "postcss-discard-empty": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "requires": {} + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==" }, "postcss-discard-overridden": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "requires": {} + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==" }, "postcss-double-position-gradients": { "version": "3.1.2", @@ -28625,8 +28930,7 @@ "postcss-flexbugs-fixes": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "requires": {} + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==" }, "postcss-focus-visible": { "version": "6.0.4", @@ -28647,14 +28951,12 @@ "postcss-font-variant": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "requires": {} + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==" }, "postcss-gap-properties": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", - "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", - "requires": {} + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==" }, "postcss-image-set-function": { "version": "4.0.7", @@ -28677,8 +28979,7 @@ "postcss-initial": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "requires": {} + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==" }, "postcss-js": { "version": "4.0.1", @@ -28726,14 +29027,12 @@ "postcss-logical": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", - "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "requires": {} + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==" }, "postcss-media-minmax": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "requires": {} + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==" }, "postcss-merge-longhand": { "version": "5.1.7", @@ -28794,8 +29093,7 @@ "postcss-modules-extract-imports": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "requires": {} + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==" }, "postcss-modules-local-by-default": { "version": "4.0.3", @@ -28853,8 +29151,7 @@ "postcss-normalize-charset": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "requires": {} + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==" }, "postcss-normalize-display-values": { "version": "5.1.0", @@ -28925,8 +29222,7 @@ "postcss-opacity-percentage": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", - "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", - "requires": {} + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==" }, "postcss-ordered-values": { "version": "5.1.3", @@ -28948,8 +29244,7 @@ "postcss-page-break": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "requires": {} + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==" }, "postcss-place": { "version": "7.0.5", @@ -29043,8 +29338,7 @@ "postcss-replace-overflow-wrap": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "requires": {} + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==" }, "postcss-selector-not": { "version": "6.0.1", @@ -29457,6 +29751,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "react-moment": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.1.3.tgz", + "integrity": "sha512-8EPvlUL8u6EknPp1ISF5MQ3wx2OHJVXIP/iZc4wRh3iV3XozftZERDv9ANZeAtMlhNNQHdFoqcZHFUkBSTONfA==" + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -29712,6 +30011,12 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, + "requireindex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.1.0.tgz", + "integrity": "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==", + "dev": true + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -29853,6 +30158,14 @@ } } }, + "rrule": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.1.tgz", + "integrity": "sha512-4p20u/1U7WqR3Nb1hOUrm0u1nSI7sO93ZUVZEZ5HeF6Gr5OlJuyhwEGRvUHq8ZfrPsq5gfa5b9dqnUs/kPqpIw==", + "requires": { + "tslib": "^2.4.0" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -30409,8 +30722,7 @@ "style-loader": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", - "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", - "requires": {} + "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==" }, "stylehacks": { "version": "5.1.1", @@ -30779,6 +31091,17 @@ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "ts-morph": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-9.1.0.tgz", + "integrity": "sha512-sei4u651MBenr27sD6qLDXN3gZ4thiX71E3qV7SuVtDas0uvK2LtgZkIYUf9DKm/fLJ6AB/+yhRJ1vpEBJgy7Q==", + "dev": true, + "requires": { + "@dsherret/to-absolute-glob": "^2.0.2", + "@ts-morph/common": "~0.7.0", + "code-block-writer": "^10.1.1" + } + }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -30903,12 +31226,6 @@ "is-typedarray": "^1.0.0" } }, - "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true - }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -30920,6 +31237,12 @@ "which-boxed-primitive": "^1.0.2" } }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "dev": true + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -31001,8 +31324,7 @@ "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "requires": {} + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" }, "util-deprecate": { "version": "1.0.2", @@ -31272,8 +31594,7 @@ "ws": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "requires": {} + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==" } } }, @@ -31744,8 +32065,7 @@ "ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "requires": {} + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==" }, "xml-name-validator": { "version": "3.0.0", diff --git a/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx b/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx index 3bbc358..b88f628 100644 --- a/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx +++ b/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx @@ -44,6 +44,7 @@ import ActivityBookmarkButton from "../../components/Bookmark/ActivityBookmarkBu import VendorProfileItem from "../../components/Vendor/VendorProfileItem"; import "./styles.css"; import Holidays from "date-holidays"; +import useClientStore from "../../zustand/ClientStore"; const ActivityDetailsPage = () => { const { @@ -71,6 +72,7 @@ const ActivityDetailsPage = () => { const [comments, setComments] = useState(""); const { openSnackbar } = useSnackbarStore(); const [fileUrl, setFileUrl] = useState(null); + const { client } = useClientStore(); const handleTimeChange = (event) => { setTime(event.target.value); @@ -281,6 +283,19 @@ const ActivityDetailsPage = () => { const handleDownloadQuotation = async (event) => { try { + const weekendAddOn = calculateWeekendAddOn( + selectedDate, + currentActivity.weekendPricing + ); + const onlineAddOn = calculateOnlineAddOn( + location, + currentActivity.offlinePricing + ); + const offlineAddOn = calculateOfflineAddOn( + location, + currentActivity.onlinePricing + ); + const bookingData = { title: currentActivity.title, selectedDate, @@ -288,6 +303,15 @@ const ActivityDetailsPage = () => { location, time, totalCost: totalPrice(), + theme: currentActivity.theme, + subtheme: currentActivity.subtheme, + activityType: currentActivity.activityType, + client: client, + comments: comments, + weekendAddOnCost: weekendAddOn, + onlineAddOnCost: onlineAddOn, + offlineAddOnCost: offlineAddOn, + basePrice: calculateBasePrice(pax), }; const url = await getQuotationPdf(bookingData); } catch (err) { diff --git a/server/assets/templates/QuotationTemplate.js b/server/assets/templates/QuotationTemplate.js new file mode 100644 index 0000000..45088ea --- /dev/null +++ b/server/assets/templates/QuotationTemplate.js @@ -0,0 +1,148 @@ +import { header } from "./header.js"; +import { footer } from "./footer.js"; +export const QuotationTemplate = (booking) => { + const getFormattedDate = (dateTime) => { + dateTime = new Date(dateTime); + + return dateTime.toLocaleDateString(undefined, { + day: "2-digit", + month: "short", + year: "numeric", + }); + }; + + const getFormattedTime = (dateTime) => { + dateTime = new Date(dateTime); + return dateTime.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: true, + }); + }; + return ` + + + + + +
+ + + + ${header("Quotation")} +
+
+
+
+ Attn: ${ + booking.client.name + }
${booking.client.companyName}
+ ${booking.client.officeAddress}
+ Singapore, ${booking.client.officePostalCode}
+
+

${getFormattedDate( + Date.now() + )}

+
+
+
- ${booking.activityTitle} by
${ - booking.vendorName - }
+ ${booking.title}
  • Date: ${startDate}
  • -
  • Time: ${startTime} to ${endTime}
  • +
  • Time: ${startTime} - ${endTime}
${booking.totalPax}
+ + + + + + + + +
+ Description + PaxPrice/PaxAdd-OnsDiscountAmount SGD
+
+ + + + + + + +
+ ${booking.title} +
    +
  • Type: ${booking.activityType}
  • +
  • Theme: ${booking.theme.name}
  • +
  • Sub-Theme: ${booking.subtheme[0].name}
  • +
  • Date: ${getFormattedDate( + booking.selectedDate + )}
  • +
  • Time: ${getFormattedTime( + booking.time.split("-")[0] + )} - ${getFormattedTime(booking.time.split("-")[1])}
  • +
  • Comments : ${booking.comments}
  • +
+
${booking.totalPax}$${( + booking.basePrice / booking.totalPax + ).toFixed(2)}$${ + booking.totalCost > booking.basePrice + ? booking.weekendAddOnCost + + booking.onlineAddOnCost + + booking.offlineAddOnCost + : 0 + } + $${ + booking.totalCost < booking.basePrice + ? -( + booking.weekendAddOnCost + + booking.onlineAddOnCost + + booking.offlineAddOnCost + ) + : 0 + } + $${booking.totalCost}
+
+
+

+ Total price: $${booking.totalCost} +

+
+ + + ${footer} + + +`; +}; diff --git a/server/assets/templates/header.js b/server/assets/templates/header.js index 6bd3b79..78e0b12 100644 --- a/server/assets/templates/header.js +++ b/server/assets/templates/header.js @@ -1,4 +1,5 @@ -export const header = `
{ + return `
Gleek -
invoice CHECKOUT001
+
${type}
@@ -33,3 +34,4 @@ export const header = `
202120661D

`; +}; diff --git a/server/controller/activityController.js b/server/controller/activityController.js index bb3156b..afec9d8 100644 --- a/server/controller/activityController.js +++ b/server/controller/activityController.js @@ -16,10 +16,11 @@ import { NotificationEvent, } from "../util/notificationRelatedEnum.js"; import { createNotification } from "./notificationController.js"; -import { InvoiceTemplate } from "../assets/templates/InvoiceTemplate.js"; +import { QuotationTemplate } from "../assets/templates/QuotationTemplate.js"; import pdf from "html-pdf"; import fs from "fs"; import path from "path"; +import Client from "../model/clientModel.js"; // yt: this endpoint retrieves and returns PUBLISHED & PENDING APPROVAL activities only export const getAllActivities = async (req, res) => { @@ -1118,7 +1119,8 @@ export const getActivityTitle = async (req, res) => { export const getQuotationPdfUrl = async (req, res) => { const data = req.body; - const pdfContent = InvoiceTemplate(data); + + const pdfContent = QuotationTemplate(data); const filename = "quotation" + Date.now() + ".pdf"; const pdfFilePath = path.join(process.cwd(), "temp", filename); pdf.create(pdfContent, {}).toFile(pdfFilePath, (err) => { diff --git a/server/server.js b/server/server.js index 860bc73..4b098a2 100644 --- a/server/server.js +++ b/server/server.js @@ -12,8 +12,6 @@ import bookingRoutes from "./routes/gleekAdmin/bookingRoute.js"; import client from "./routes/gleekAdmin/client.js"; import activityTestController from "./controller/activityTestController.js"; import notificationRoutes from "./routes/notificationRoute.js"; -import pdf from "html-pdf"; -import { InvoiceTemplate } from "./assets/templates/InvoiceTemplate.js"; const app = express(); From cff54b6cd14b712c7b3e795c26c0ba997da391ac Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:37:26 +0800 Subject: [PATCH 03/11] Changed Invoice to Quotation --- server/assets/templates/InvoiceTemplate.js | 132 --------------------- 1 file changed, 132 deletions(-) delete mode 100644 server/assets/templates/InvoiceTemplate.js diff --git a/server/assets/templates/InvoiceTemplate.js b/server/assets/templates/InvoiceTemplate.js deleted file mode 100644 index 4fac1a9..0000000 --- a/server/assets/templates/InvoiceTemplate.js +++ /dev/null @@ -1,132 +0,0 @@ -import { header } from "./header.js"; -import { footer } from "./footer.js"; -export const InvoiceTemplate = (booking) => { - let { selectedDate, time } = booking; - - selectedDate = new Date(selectedDate); - - const startDate = selectedDate.toLocaleDateString(undefined, { - day: "2-digit", - month: "short", - }); - const startTime = selectedDate.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - hour12: true, - }); - let endTime = new Date(time.split(",")[1]); - endTime = endTime.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - hour12: true, - }); - - return ` - - - - - -
- - - -
-
- ${header} -
-
-
-
- - - - - - - -
- Description - PaxPrice per PaxAmount SGD
-
- - - - - - - -
- ${booking.title} -
    -
  • Date: ${startDate}
  • -
  • Time: ${startTime} - ${endTime}
  • -
-
${booking.totalPax}$${ - booking.totalCost / booking.totalPax - }$${ - booking.totalCost - }
-
-
-

- Total price -

-

- $${booking.totalCost} -

-
-
-
- ${footer} - - -`; -}; From 5bb8cb5dbb32ce86231617ff767ee85d7781ce42 Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Wed, 25 Oct 2023 16:24:25 +0800 Subject: [PATCH 04/11] Merge Bug Fix --- .../ActivityDetailsPage/ActivityDetailsPage.jsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx b/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx index d370e7e..8d8ce21 100644 --- a/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx +++ b/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx @@ -212,28 +212,16 @@ const ActivityDetailsPage = () => { const totalPrice = () => { let totalBasePrice = calculateBasePrice(pax); - const weekendAddOn = calculateWeekendAddOn( - selectedDate, - currentActivity.weekendPricing - ); const weekendAddOn = calculateWeekendAddOn( selectedDate, currentActivity.weekendPricing ); - const onlineAddOn = calculateOnlineAddOn( - location, - currentActivity.offlinePricing - ); const onlineAddOn = calculateOnlineAddOn( location, currentActivity.offlinePricing ); - const offlineAddOn = calculateOfflineAddOn( - location, - currentActivity.onlinePricing - ); const offlineAddOn = calculateOfflineAddOn( location, currentActivity.onlinePricing From dc0b3bfb237b1d99957effdf9dd11b11d52a5241 Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Wed, 25 Oct 2023 23:53:23 +0800 Subject: [PATCH 05/11] Booking Summary email for Client --- .../assets/templates/BookingSummaryClient.js | 186 +++ server/controller/bookingController.js | 1050 +++++++++-------- 2 files changed, 738 insertions(+), 498 deletions(-) create mode 100644 server/assets/templates/BookingSummaryClient.js diff --git a/server/assets/templates/BookingSummaryClient.js b/server/assets/templates/BookingSummaryClient.js new file mode 100644 index 0000000..634dcb0 --- /dev/null +++ b/server/assets/templates/BookingSummaryClient.js @@ -0,0 +1,186 @@ +import { header } from "./header.js"; +import { footer } from "./footer.js"; + +export const BookingSummaryClient = (data) => { + const getFormattedDate = (dateTime) => { + dateTime = new Date(dateTime); + + return dateTime.toLocaleDateString(undefined, { + day: "2-digit", + month: "short", + }); + }; + + const getFormattedTime = (dateTime) => { + dateTime = new Date(dateTime); + return dateTime.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: true, + }); + }; + let grandTotal = 0; + const getBookings = (data) => { + // console.log(data); + let htmlString = ""; + + data.forEach((booking) => { + htmlString += ` + + + ${booking.activityTitle} +
    +
  • Date: ${getFormattedDate( + booking.startDateTime + )}
  • +
  • Time: ${getFormattedTime( + booking.startDateTime + )} - ${getFormattedTime(booking.endDateTime)}
  • +
  • Type: ${ + booking.activityId.activityType + }
  • +
  • Theme: ${booking.activityId.theme.name}
  • +
  • Sub-Theme: ${ + booking.activityId.subtheme[0].name + }
  • +
+ + ${booking.totalPax} + $${ + booking.basePricePerPax + } + $${ + booking.totalCost > + booking.basePricePerPax * booking.totalPax + ? booking.weekendAddOnCost + + booking.onlineAddOnCost + + booking.offlineAddOnCost + : 0 + } + $${ + booking.totalCost < + booking.basePricePerPax * booking.totalPax + ? -( + booking.weekendAddOnCost + + booking.onlineAddOnCost + + booking.offlineAddOnCost + ) + : 0 + } + $${ + booking.totalCost + } + + `; + grandTotal += booking.totalCost; + }); + console.log(htmlString); + return htmlString; + }; + + return ` + + + + + +
+ + + +
+
+ ${header("Booking Summary")} +
+
+
+
+ Attn: ${ + data[0].billingPartyName + }
${data[0].billingEmail}
+ ${data[0].billingAddress}
+ Singapore, ${data[0].billingOfficePostalCode}
+
+

${getFormattedDate( + Date.now() + )}

+
+
+ + + + + + + + + +
+ Description + PaxPrice/PaxAdd-OnsDiscountAmount SGD
+
+ + ${getBookings(data)} +
+
+
+

+ Total price +

+

+ $${grandTotal} +

+
+
+
+ ${footer} + + +`; +}; diff --git a/server/controller/bookingController.js b/server/controller/bookingController.js index 438df67..c58ed6a 100644 --- a/server/controller/bookingController.js +++ b/server/controller/bookingController.js @@ -6,592 +6,646 @@ import { validationResult } from "express-validator"; import { isCartItemStillAvailable } from "./cartItemController.js"; import mongoose from "mongoose"; import { getAllPendingAndConfirmedBookingsForVendor } from "../service/bookingService.js"; +import { BookingSummaryClient } from "../assets/templates/BookingSummaryClient.js"; +import sendMail from "../util/sendMail.js"; +import fs from "fs"; +import path from "path"; +import pdf from "html-pdf"; // GET /booking/getAllBookings export const getAllBookings = async (req, res) => { - try { - const bookings = await BookingModel.find(); - // if (bookings.length === 0) { - // return res.status(404).json({ message: "No bookings found!" }); - // } - res.status(200).json({ bookings }); - } catch (error) { - console.error(error); - res.status(500).json({ - message: "Server Error! Unable to get bookings.", - error: error.message, - }); - } + try { + const bookings = await BookingModel.find(); + // if (bookings.length === 0) { + // return res.status(404).json({ message: "No bookings found!" }); + // } + res.status(200).json({ bookings }); + } catch (error) { + console.error(error); + res.status(500).json({ + message: "Server Error! Unable to get bookings.", + error: error.message, + }); + } }; // GET /booking/getBookingById/:id export const getBookingById = async (req, res) => { - try { - const booking = await BookingModel.findById(req.params.id); - if (!booking) { - return res - .status(404) - .json({ message: "No booking found with this ID!" }); - } - res.status(200).json({ booking }); - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server Error! Unable to get booking.", - error: error.message, - }); - } + try { + const booking = await BookingModel.findById(req.params.id); + if (!booking) { + return res + .status(404) + .json({ message: "No booking found with this ID!" }); + } + res.status(200).json({ booking }); + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Server Error! Unable to get booking.", + error: error.message, + }); + } }; function generateStartTimes(earliestStartTime, latestStartTime, interval) { - const startTimes = []; - let currentTime = new Date(earliestStartTime); - const endTimeObj = new Date(latestStartTime); + const startTimes = []; + let currentTime = new Date(earliestStartTime); + const endTimeObj = new Date(latestStartTime); - while (currentTime <= endTimeObj) { - startTimes.push(new Date(currentTime)); - currentTime.setTime(currentTime.getTime() + interval * 60 * 1000); // Add interval in milliseconds - } + while (currentTime <= endTimeObj) { + startTimes.push(new Date(currentTime)); + currentTime.setTime(currentTime.getTime() + interval * 60 * 1000); // Add interval in milliseconds + } - return startTimes; + return startTimes; } function getTimeslotCapacities( - startTimes, - capacity, - bookings, - blockedTimeslots, - duration, + startTimes, + capacity, + bookings, + blockedTimeslots, + duration ) { - // Create a hashmap to store capacities for each starttime slot - const capacities = new Map(startTimes.map((slot) => [slot, capacity])); - - // Iterate through blockedTimeslots and update capacities - for (const blockedTimeslot of blockedTimeslots) { - const { blockedStartDateTime, blockedEndDateTime } = blockedTimeslot; - const adjustedBlockedStartDateTime = - blockedStartDateTime - duration * 60 * 1000; // Adjust for duration - - // Iterate through start times and update capacities - for (const startTime of startTimes) { - if ( - startTime > adjustedBlockedStartDateTime && - startTime < blockedEndDateTime - ) { - // Check if the start time falls within the blockedTimeslot - capacities.set(startTime, 0); + // Create a hashmap to store capacities for each starttime slot + const capacities = new Map(startTimes.map((slot) => [slot, capacity])); + + // Iterate through blockedTimeslots and update capacities + for (const blockedTimeslot of blockedTimeslots) { + const { blockedStartDateTime, blockedEndDateTime } = blockedTimeslot; + const adjustedBlockedStartDateTime = + blockedStartDateTime - duration * 60 * 1000; // Adjust for duration + + // Iterate through start times and update capacities + for (const startTime of startTimes) { + if ( + startTime > adjustedBlockedStartDateTime && + startTime < blockedEndDateTime + ) { + // Check if the start time falls within the blockedTimeslot + capacities.set(startTime, 0); + } } - } - } - - // Iterate through bookings and update capacities - for (const booking of bookings) { - const { startDateTime, endDateTime } = booking; - const adjustedStartDateTime = startDateTime - duration * 60 * 1000; // Adjust for duration - - // Iterate through start times and update capacities - for (const startTime of startTimes) { - if (startTime >= adjustedStartDateTime && startTime <= endDateTime) { - // Check if the start time falls within the booking - capacities.set(startTime, capacities.get(startTime) - 1); + } + + // Iterate through bookings and update capacities + for (const booking of bookings) { + const { startDateTime, endDateTime } = booking; + const adjustedStartDateTime = startDateTime - duration * 60 * 1000; // Adjust for duration + + // Iterate through start times and update capacities + for (const startTime of startTimes) { + if (startTime >= adjustedStartDateTime && startTime <= endDateTime) { + // Check if the start time falls within the booking + capacities.set(startTime, capacities.get(startTime) - 1); + } } - } - } + } - // Convert capacities back to an array - const finalCapacities = Array.from(capacities.values()); + // Convert capacities back to an array + const finalCapacities = Array.from(capacities.values()); - return finalCapacities; + return finalCapacities; } export function generateAllTimeslots( - earliestStartTime, - latestStartTime, - interval, - capacity, - bookings, - blockedTimeslots, - duration, + earliestStartTime, + latestStartTime, + interval, + capacity, + bookings, + blockedTimeslots, + duration ) { - const startTimes = generateStartTimes( - earliestStartTime, - latestStartTime, - interval, - ); - - const timeslotCapacities = getTimeslotCapacities( - startTimes, - capacity, - bookings, - blockedTimeslots, - duration, - ); - - const allTimeslots = startTimes.map((startTime, index) => { - return { - startTime: startTime, - endTime: new Date(startTime.getTime() + duration * 60 * 1000), - isAvailable: timeslotCapacities[index] > 0, - }; - }); - - console.log("Existing bookings on selected day", bookings); - console.log("Blocked timeslots on selected day: ", blockedTimeslots); - console.log("All timeslots: ", allTimeslots); - - return allTimeslots; + const startTimes = generateStartTimes( + earliestStartTime, + latestStartTime, + interval + ); + + const timeslotCapacities = getTimeslotCapacities( + startTimes, + capacity, + bookings, + blockedTimeslots, + duration + ); + + const allTimeslots = startTimes.map((startTime, index) => { + return { + startTime: startTime, + endTime: new Date(startTime.getTime() + duration * 60 * 1000), + isAvailable: timeslotCapacities[index] > 0, + }; + }); + + console.log("Existing bookings on selected day", bookings); + console.log("Blocked timeslots on selected day: ", blockedTimeslots); + console.log("All timeslots: ", allTimeslots); + + return allTimeslots; } // GET /gleek/booking/getAvailableBookingTimeslots/:activityId/:selectedDate // Eg: /gleek/booking/getAvailableBookingTimeslots/60b9b6b9e6b3a83a3c3b3b3b/2023-10-07T00:00:00.000Z export const getAvailableBookingTimeslots = async (req, res) => { - const errors = validationResult(req); - - const client = req.user; - // console.log("Client", client); - - if (!errors.isEmpty()) { - // 422 status due to validation errors - return res.status(422).json({ errors: errors.array() }); - } - - try { - const { activityId, selectedDate } = req.params; - // Validate that activity exists - const activity = - await ActivityModel.findById(activityId).populate("linkedVendor"); - if (activity === null) { - return res.status(404).json({ - error: "Activity not found with the provided ID.", - activityId: activityId, - }); - } - // Validate date parameter - const dateParam = new Date(selectedDate); - if (dateParam.toString() === "Invalid Date") { - return res.status(400).json({ - error: "Invalid date parameter.", - selectedDate: selectedDate, - }); - } + const errors = validationResult(req); + + const client = req.user; + // console.log("Client", client); + + if (!errors.isEmpty()) { + // 422 status due to validation errors + return res.status(422).json({ errors: errors.array() }); + } + + try { + const { activityId, selectedDate } = req.params; + // Validate that activity exists + const activity = + await ActivityModel.findById(activityId).populate("linkedVendor"); + if (activity === null) { + return res.status(404).json({ + error: "Activity not found with the provided ID.", + activityId: activityId, + }); + } + // Validate date parameter + const dateParam = new Date(selectedDate); + if (dateParam.toString() === "Invalid Date") { + return res.status(400).json({ + error: "Invalid date parameter.", + selectedDate: selectedDate, + }); + } - // Validate that date parameter is within activity's available dates (weekend/weekday) - const dayOfWeek = dateParam.getDay(); // 0 (Sunday) to 6 (Saturday) - const isWeekday = dayOfWeek >= 1 && dayOfWeek <= 5; // Monday to Friday - const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; // Sunday or Saturday + // Validate that date parameter is within activity's available dates (weekend/weekday) + const dayOfWeek = dateParam.getDay(); // 0 (Sunday) to 6 (Saturday) + const isWeekday = dayOfWeek >= 1 && dayOfWeek <= 5; // Monday to Friday + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; // Sunday or Saturday + + if (!activity.dayAvailabilities.includes("Weekdays") && isWeekday) { + return res.status(400).json({ + error: "This activity is not available on weekdays.", + selectedDate: dateParam, + }); + } + + if (!activity.dayAvailabilities.includes("Weekends") && isWeekend) { + return res.status(400).json({ + error: "This activity is not available on weekends.", + selectedDate: dateParam, + }); + } - if (!activity.dayAvailabilities.includes("Weekdays") && isWeekday) { - return res.status(400).json({ - error: "This activity is not available on weekdays.", - selectedDate: dateParam, + // Validate that date parameter is XX days in advance + const today = new Date(); + const daysInAdvance = activity.bookingNotice; + const minDate = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + daysInAdvance + ); + if (dateParam < minDate) { + return res.status(400).json({ + error: `Booking can only be made at least ${daysInAdvance} days in advance.`, + selectedDate: selectedDate, + }); + } + + // Validate that date parameter is not a public holiday + // (public holidays are not available for booking) + + // Generate timeslots in 30 min intervals from earliest start to latest start time + console.log("SELECTED DATE: ", dateParam.toLocaleDateString()); + const earliestStartTime = new Date(dateParam); + earliestStartTime.setHours( + activity.startTime.getHours(), + activity.startTime.getMinutes(), + 0, + 0 + ); + const latestStartTime = new Date(dateParam); + latestStartTime.setHours( + activity.endTime.getHours(), + activity.endTime.getMinutes(), + 0, + 0 + ); + console.log( + "EARLIEST START TIME: ", + earliestStartTime.toLocaleDateString(), + earliestStartTime.toLocaleTimeString() + ); + console.log( + "LATEST START TIME: ", + latestStartTime.toLocaleDateString(), + latestStartTime.toLocaleTimeString() + ); + + const interval = 30; // 30 minutes + + const startOfDay = new Date(dateParam); + startOfDay.setHours(0, 0, 0, 0); // Set time to 00:00:00.000 of the selected day + + const endOfDay = new Date(dateParam); + endOfDay.setHours(23, 59, 59, 999); // Set time to 23:59:59.999 of the selected day + + // Get activity's bookings for the selected date + const bookings = await BookingModel.find({ + activityId, + startDateTime: { + $gte: startOfDay, + $lt: endOfDay, + }, }); - } - if (!activity.dayAvailabilities.includes("Weekends") && isWeekend) { - return res.status(400).json({ - error: "This activity is not available on weekends.", - selectedDate: dateParam, + // Get activities blocked timeslots for the selected date + const blockedTimeslots = await BlockedTimeslotModel.find({ + activityId, + blockedStartDateTime: { + $gte: startOfDay, + $lt: endOfDay, + }, }); - } - - // Validate that date parameter is XX days in advance - const today = new Date(); - const daysInAdvance = activity.bookingNotice; - const minDate = new Date( - today.getFullYear(), - today.getMonth(), - today.getDate() + daysInAdvance, - ); - if (dateParam < minDate) { - return res.status(400).json({ - error: `Booking can only be made at least ${daysInAdvance} days in advance.`, - selectedDate: selectedDate, + + const allTimeslots = generateAllTimeslots( + earliestStartTime, + latestStartTime, + interval, + activity.capacity, + bookings, + blockedTimeslots, + activity.duration + ); + + res.status(200).json({ + allTimeslots, }); - } - - // Validate that date parameter is not a public holiday - // (public holidays are not available for booking) - - // Generate timeslots in 30 min intervals from earliest start to latest start time - console.log("SELECTED DATE: ", dateParam.toLocaleDateString()); - const earliestStartTime = new Date(dateParam); - earliestStartTime.setHours( - activity.startTime.getHours(), - activity.startTime.getMinutes(), - 0, - 0, - ); - const latestStartTime = new Date(dateParam); - latestStartTime.setHours( - activity.endTime.getHours(), - activity.endTime.getMinutes(), - 0, - 0, - ); - console.log( - "EARLIEST START TIME: ", - earliestStartTime.toLocaleDateString(), - earliestStartTime.toLocaleTimeString(), - ); - console.log( - "LATEST START TIME: ", - latestStartTime.toLocaleDateString(), - latestStartTime.toLocaleTimeString(), - ); - - const interval = 30; // 30 minutes - - const startOfDay = new Date(dateParam); - startOfDay.setHours(0, 0, 0, 0); // Set time to 00:00:00.000 of the selected day - - const endOfDay = new Date(dateParam); - endOfDay.setHours(23, 59, 59, 999); // Set time to 23:59:59.999 of the selected day - - // Get activity's bookings for the selected date - const bookings = await BookingModel.find({ - activityId, - startDateTime: { - $gte: startOfDay, - $lt: endOfDay, - }, - }); - - // Get activities blocked timeslots for the selected date - const blockedTimeslots = await BlockedTimeslotModel.find({ - activityId, - blockedStartDateTime: { - $gte: startOfDay, - $lt: endOfDay, - }, - }); - - const allTimeslots = generateAllTimeslots( - earliestStartTime, - latestStartTime, - interval, - activity.capacity, - bookings, - blockedTimeslots, - activity.duration, - ); - - res.status(200).json({ - allTimeslots, - }); - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server Error! Unable to get available booking timeslots.", - error: error.message, - }); - } + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Server Error! Unable to get available booking timeslots.", + error: error.message, + }); + } }; export function getTimeslotAvailability( - allTimeslots, - selectedStartDateTime, - selectedEndDateTime, + allTimeslots, + selectedStartDateTime, + selectedEndDateTime ) { - const timeslot = allTimeslots.find( - (timeslot) => - timeslot.startTime.getTime() === selectedStartDateTime.getTime() && - timeslot.endTime.getTime() === selectedEndDateTime.getTime() && - timeslot.isAvailable, - ); - - return timeslot !== undefined; + const timeslot = allTimeslots.find( + (timeslot) => + timeslot.startTime.getTime() === selectedStartDateTime.getTime() && + timeslot.endTime.getTime() === selectedEndDateTime.getTime() && + timeslot.isAvailable + ); + + return timeslot !== undefined; } export const createBookings = async (req, res) => { - const errors = validationResult(req); - - const client = req.user; - let cartItemsToCheckOut = req.body; - console.log(cartItemsToCheckOut); - const cartIds = []; - let cartItems = []; - - if (!errors.isEmpty()) { - // 422 status due to validation errors - return res.status(422).json({ errors: errors.array() }); - } - - try { - // Check if client is approved to make bookings - if (client.status !== "APPROVED") { - return res.status(400).json({ - error: "Client is not approved to make bookings yet.", - }); - } - - // Get Client's billing details - const { - billingEmail, - billingOfficePostalCode, - billingPartyName, - billingAddress, - } = client; - - for (const item of cartItemsToCheckOut) { - cartIds.push(item._id); - } - - try { - cartItems = await CartItemModel.find({ _id: { $in: cartIds } }); - console.log(cartItems); - } catch (error) { - console.error("Error finding cart items:", error); - } - - // Check if cart is empty - if (cartItems.length === 0) { - return res.status(400).json({ - error: "Cart is empty. Please add activities to cart before booking.", - }); - } - - // Check if cart items are still available - let isBookingValid = true; - let bookings = []; - for (const cartItem of cartItems) { - isBookingValid = await isCartItemStillAvailable(cartItem._id); - if (!isBookingValid) { - return res.status(400).json({ - error: `Selected booking timeslot for ${cartItem.activityTitle} is no longer available. Please book another timeslot!`, - }); + const errors = validationResult(req); + + const client = req.user; + let cartItemsToCheckOut = req.body; + console.log(cartItemsToCheckOut); + const cartIds = []; + let cartItems = []; + + if (!errors.isEmpty()) { + // 422 status due to validation errors + return res.status(422).json({ errors: errors.array() }); + } + + try { + // Check if client is approved to make bookings + if (client.status !== "APPROVED") { + return res.status(400).json({ + error: "Client is not approved to make bookings yet.", + }); + } + + // Get Client's billing details + const { + billingEmail, + billingOfficePostalCode, + billingPartyName, + billingAddress, + } = client; + + for (const item of cartItemsToCheckOut) { + cartIds.push(item._id); } - const cartItemPlainObject = cartItem.toObject(); - delete cartItemPlainObject._id; + try { + cartItems = await CartItemModel.find({ _id: { $in: cartIds } }); + console.log(cartItems); + } catch (error) { + console.error("Error finding cart items:", error); + } + + // Check if cart is empty + if (cartItems.length === 0) { + return res.status(400).json({ + error: "Cart is empty. Please add activities to cart before booking.", + }); + } + + // Check if cart items are still available + let isBookingValid = true; + let bookings = []; + for (const cartItem of cartItems) { + isBookingValid = await isCartItemStillAvailable(cartItem._id); + if (!isBookingValid) { + return res.status(400).json({ + error: `Selected booking timeslot for ${cartItem.activityTitle} is no longer available. Please book another timeslot!`, + }); + } + + const cartItemPlainObject = cartItem.toObject(); + delete cartItemPlainObject._id; + + bookings.push({ + billingEmail, + billingOfficePostalCode, + billingPartyName, + billingAddress, + ...cartItemPlainObject, + }); + } + console.log(bookings); + let createdBookings = []; + // Start transaction to create bookings + const session = await mongoose.startSession(); + session.startTransaction(); + for (const bookingDetails of bookings) { + // Create booking + const booking = new BookingModel(bookingDetails); + await booking.save(); + await booking.populate("vendorId"); + await booking.populate({ + path: "activityId", + populate: [{ path: "theme" }, { path: "subtheme" }], + }); + createdBookings.push(booking); + } - bookings.push({ - billingEmail, - billingOfficePostalCode, - billingPartyName, - billingAddress, - ...cartItemPlainObject, + // Delete cart items + await CartItemModel.deleteMany({ _id: { $in: cartIds } }); + + await session.commitTransaction(); + session.endSession(); + await sendBookingSummaryEmail(createdBookings, billingEmail); + res.status(200).json({ + message: "Successfully created bookings!", + bookings: createdBookings, }); - } - - let createdBookings = []; - // Start transaction to create bookings - const session = await mongoose.startSession(); - session.startTransaction(); - for (const bookingDetails of bookings) { - // Create booking - const booking = new BookingModel(bookingDetails); - await booking.save(); - createdBookings.push(booking); - } - - // Delete cart items - await CartItemModel.deleteMany({ _id: { $in: cartIds } }); - - await session.commitTransaction(); - session.endSession(); - - res.status(200).json({ - message: "Successfully created bookings!", - bookings: createdBookings, - }); - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server Error! Unable to create booking.", - error: error.message, - }); - } + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Server Error! Unable to create booking.", + error: error.message, + }); + } }; // DELETE /booking/deleteBooking/:id export const deleteBooking = async (req, res) => { - try { - const booking = await BookingModel.findByIdAndDelete(req.params.id); - if (!booking) { - return res - .status(404) - .json({ message: "No booking found with this ID!" }); - } - res.status(200).json({ message: "Booking deleted successfully!" }); - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server Error! Unable to delete booking.", - error: error.message, - }); - } + try { + const booking = await BookingModel.findByIdAndDelete(req.params.id); + if (!booking) { + return res + .status(404) + .json({ message: "No booking found with this ID!" }); + } + res.status(200).json({ message: "Booking deleted successfully!" }); + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Server Error! Unable to delete booking.", + error: error.message, + }); + } }; // PATCH /booking/confirmBooking/:id export const confirmBooking = async (req, res) => { - try { - const bookingId = req.params.id; - const vendorId = req.user; - const newBooking = await BookingModel.findByIdAndUpdate( - bookingId, - { status: "CONFIRMED" }, - { new: true }, - ); - const updatedBookings = - await getAllPendingAndConfirmedBookingsForVendor(vendorId); - res.status(200).json({ - bookings: updatedBookings, - message: `Booking for ${newBooking.activityTitle} confirmed successfully!`, - }); - } catch (error) { - console.error(error); - res.status(500).json({ - message: "Server Error! Unable to confirm booking.", - error: error.message, - }); - } + try { + const bookingId = req.params.id; + const vendorId = req.user; + const newBooking = await BookingModel.findByIdAndUpdate( + bookingId, + { status: "CONFIRMED" }, + { new: true } + ); + const updatedBookings = + await getAllPendingAndConfirmedBookingsForVendor(vendorId); + res.status(200).json({ + bookings: updatedBookings, + message: `Booking for ${newBooking.activityTitle} confirmed successfully!`, + }); + } catch (error) { + console.error(error); + res.status(500).json({ + message: "Server Error! Unable to confirm booking.", + error: error.message, + }); + } }; // PATCH /booking/rejectBooking/:id export const rejectBooking = async (req, res) => { - try { - const bookingId = req.params.id; - const vendorId = req.user; - const { rejectionReason } = req.body; - const newBooking = await BookingModel.findByIdAndUpdate( - bookingId, - { status: "REJECTED", rejectionReason: rejectionReason }, - { new: true }, - ); - const updatedBookings = - await getAllPendingAndConfirmedBookingsForVendor(vendorId); - res.status(200).json({ - bookings: updatedBookings, - message: `Booking for ${newBooking.activityTitle} rejected successfully!`, - }); - } catch (error) { - console.error(error); - res.status(500).json({ - message: "Server Error! Unable to reject booking.", - error: error.message, - }); - } + try { + const bookingId = req.params.id; + const vendorId = req.user; + const { rejectionReason } = req.body; + const newBooking = await BookingModel.findByIdAndUpdate( + bookingId, + { status: "REJECTED", rejectionReason: rejectionReason }, + { new: true } + ); + const updatedBookings = + await getAllPendingAndConfirmedBookingsForVendor(vendorId); + res.status(200).json({ + bookings: updatedBookings, + message: `Booking for ${newBooking.activityTitle} rejected successfully!`, + }); + } catch (error) { + console.error(error); + res.status(500).json({ + message: "Server Error! Unable to reject booking.", + error: error.message, + }); + } }; // PATCH /booking/cancelBooking/:id export const cancelBooking = async (req, res) => { - try { - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server Error! Unable to cancel booking.", - error: error.message, - }); - } + try { + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Server Error! Unable to cancel booking.", + error: error.message, + }); + } }; // PATCH /booking/updateBooking/:id export const updateBooking = async (req, res) => { - try { - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server Error! Unable to update booking.", - error: error.message, - }); - } + try { + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Server Error! Unable to update booking.", + error: error.message, + }); + } }; // PATCH /booking/updateToPaid/:id export const updateToPaid = async (req, res) => { - try { - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server Error! Unable to update booking to PAID.", - error: error.message, - }); - } + try { + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Server Error! Unable to update booking to PAID.", + error: error.message, + }); + } }; // GET /booking/getAllBookingsByClientId/:id export const getAllBookingsByClientId = async (req, res) => { - try { - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server Error! Unable to get bookings by client ID.", - error: error.message, - }); - } + try { + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Server Error! Unable to get bookings by client ID.", + error: error.message, + }); + } }; // GET /booking/getAllBookingsByVendorId/:id export const getAllBookingsByVendorId = async (req, res) => { - try { - const vendorId = req.user; - const bookings = await getAllPendingAndConfirmedBookingsForVendor(vendorId); - res.status(200).json({ - bookings: bookings, - }); - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server Error! Unable to get bookings by vendor ID.", - error: error.message, - }); - } + try { + const vendorId = req.user; + const bookings = + await getAllPendingAndConfirmedBookingsForVendor(vendorId); + res.status(200).json({ + bookings: bookings, + }); + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Server Error! Unable to get bookings by vendor ID.", + error: error.message, + }); + } }; // GET /booking/getAllBookingsByActivityId/:activityId export const getAllBookingsByActivityId = async (req, res) => { - try { - const { activityId } = req.params; - const bookings = await BookingModel.find({ activityId }); - res.status(200).json({ bookings }); - } catch (error) { - console.log(error); - res.status(500).json({ - message: "Server Error! Unable to get bookings by activity ID.", - error: error.message, - }); - } + try { + const { activityId } = req.params; + const bookings = await BookingModel.find({ activityId }); + res.status(200).json({ bookings }); + } catch (error) { + console.log(error); + res.status(500).json({ + message: "Server Error! Unable to get bookings by activity ID.", + error: error.message, + }); + } }; // POST /booking/updateCompletedBookings export const updateCompletedBookings = async (req, res) => { - try { - const currentDate = new Date(); - //Find bookings of "confirmed" status and date passed current date - const confirmedBookingsToUpdate = await BookingModel.find({ - status: "CONFIRMED", - endDateTime: { $lt: currentDate }, - }); - - if (confirmedBookingsToUpdate.length > 0) { - console.log("In condition to update bookings"); - // Update the status of each booking to "PENDING_PAYMENT" - confirmedBookingsToUpdate.map(async (booking) => { - const newActionHistory = { - newStatus: "PENDING_PAYMENT", - actionByUserType: "ADMIN", - actionByUserName: "SCHEDULED UPDATE", - actionTimestamp: new Date(), - }; - booking.status = "PENDING_PAYMENT"; - booking.actionHistory.push(newActionHistory); - await booking.save(); + try { + const currentDate = new Date(); + //Find bookings of "confirmed" status and date passed current date + const confirmedBookingsToUpdate = await BookingModel.find({ + status: "CONFIRMED", + endDateTime: { $lt: currentDate }, }); - console.log(confirmedBookingsToUpdate); + if (confirmedBookingsToUpdate.length > 0) { + console.log("In condition to update bookings"); + // Update the status of each booking to "PENDING_PAYMENT" + confirmedBookingsToUpdate.map(async (booking) => { + const newActionHistory = { + newStatus: "PENDING_PAYMENT", + actionByUserType: "ADMIN", + actionByUserName: "SCHEDULED UPDATE", + actionTimestamp: new Date(), + }; + booking.status = "PENDING_PAYMENT"; + booking.actionHistory.push(newActionHistory); + await booking.save(); + }); + + console.log(confirmedBookingsToUpdate); + + res.status(200).json({ + message: "Bookings updated.", + data: confirmedBookingsToUpdate, + }); + } else { + console.log("No bookings to update."); + res.status(200).json({ + message: "No bookings to update.", + }); + } + } catch (error) { + console.error("Error updating bookings:", error); + res.status(500).json({ error: "Server error", message: error.message }); + } +}; - res.status(200).json({ - message: "Bookings updated.", - data: confirmedBookingsToUpdate, - }); - } else { - console.log("No bookings to update."); - res.status(200).json({ - message: "No bookings to update.", +//Generate the PDF for the Client +export const sendBookingSummaryEmail = async (data, email) => { + const pdfContent = BookingSummaryClient(data); + const filename = "bookingSummary" + Date.now() + ".pdf"; + const pdfFilePath = path.join(process.cwd(), "temp", filename); + + pdf.create(pdfContent, {}).toFile(pdfFilePath, (err) => { + if (err) { + // Handle errors appropriately + console.error(err); + res.status(500).send("Error generating PDF"); + } + const options = { + to: email, + subject: "Your Booking is Confirmed!", + text: "Your booking is confirmed", + attachments: [ + { + filename: "BookingSummary.pdf", + path: pdfFilePath, + contentType: "application/pdf", + }, + ], + }; + + sendMail(options).then(() => { + fs.access(pdfFilePath, fs.constants.F_OK, (err) => { + if (err) { + console.error("File does not exist or cannot be accessed:", err); + } else { + fs.unlink(pdfFilePath, (deleteError) => { + if (deleteError) { + console.error("Error deleting the file:", deleteError); + } else { + console.log("File deleted successfully."); + } + }); + } + }); }); - } - } catch (error) { - console.error("Error updating bookings:", error); - res.status(500).json({ error: "Server error", message: error.message }); - } + }); }; From 50904888440535d542ba3a2da61b2f24e468aed2 Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:44:18 +0800 Subject: [PATCH 06/11] Added Booking Summary Email for Vendor --- .../assets/templates/BookingSummaryClient.js | 2 - .../assets/templates/BookingSummaryVendor.js | 151 ++++++++++++++++++ server/controller/bookingController.js | 70 ++++++-- 3 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 server/assets/templates/BookingSummaryVendor.js diff --git a/server/assets/templates/BookingSummaryClient.js b/server/assets/templates/BookingSummaryClient.js index 634dcb0..5090c86 100644 --- a/server/assets/templates/BookingSummaryClient.js +++ b/server/assets/templates/BookingSummaryClient.js @@ -21,7 +21,6 @@ export const BookingSummaryClient = (data) => { }; let grandTotal = 0; const getBookings = (data) => { - // console.log(data); let htmlString = ""; data.forEach((booking) => { @@ -74,7 +73,6 @@ export const BookingSummaryClient = (data) => { `; grandTotal += booking.totalCost; }); - console.log(htmlString); return htmlString; }; diff --git a/server/assets/templates/BookingSummaryVendor.js b/server/assets/templates/BookingSummaryVendor.js new file mode 100644 index 0000000..b6d3182 --- /dev/null +++ b/server/assets/templates/BookingSummaryVendor.js @@ -0,0 +1,151 @@ +import { header } from "./header.js"; +import { footer } from "./footer.js"; + +export const BookingSummaryVendor = (booking) => { + const getFormattedDate = (dateTime) => { + dateTime = new Date(dateTime); + + return dateTime.toLocaleDateString(undefined, { + day: "2-digit", + month: "short", + year: "numeric", + }); + }; + + const getFormattedTime = (dateTime) => { + dateTime = new Date(dateTime); + return dateTime.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: true, + }); + }; + return ` + + + + + +
+ + + + ${header("Booking Summary")} +
+
+
+
+ Attn: ${ + booking.vendorId.companyName + }
${booking.vendorId.companyEmail}
+ ${booking.vendorId.companyAddress}
+ Singapore, ${booking.vendorId.companyPostalCode}
+
+

${getFormattedDate( + Date.now() + )}

+
+
+ + + + + + + + + +
+ Description + PaxPrice/PaxAdd-OnsDiscountAmount SGD
+
+ + + + + + + +
+ ${booking.activityTitle} +
    +
  • Date: ${getFormattedDate( + booking.startDateTime + )}
  • +
  • Time: ${getFormattedTime( + booking.startDateTime + )} - ${getFormattedTime(booking.endDateTime)}
  • +
  • Type: ${ + booking.activityId.activityType + }
  • +
  • Theme: ${booking.activityId.theme.name}
  • +
  • Sub-Theme: ${booking.activityId.subtheme[0].name}
  • +
  • Comments : ${booking.activityId.comments}
  • +
+
${booking.totalPax}$${ + booking.basePricePerPax + }$${ + booking.totalCost > booking.basePricePerPax * booking.totalPax + ? booking.weekendAddOnCost + + booking.onlineAddOnCost + + booking.offlineAddOnCost + : 0 + } + $${ + booking.totalCost < booking.basePricePerPax * booking.totalPax + ? -( + booking.weekendAddOnCost + + booking.onlineAddOnCost + + booking.offlineAddOnCost + ) + : 0 + } + $${booking.totalCost}
+
+
+

+ Total price: $${booking.totalCost} +

+
+
+
+ ${footer} + + +`; +}; diff --git a/server/controller/bookingController.js b/server/controller/bookingController.js index c58ed6a..559b345 100644 --- a/server/controller/bookingController.js +++ b/server/controller/bookingController.js @@ -7,6 +7,7 @@ import { isCartItemStillAvailable } from "./cartItemController.js"; import mongoose from "mongoose"; import { getAllPendingAndConfirmedBookingsForVendor } from "../service/bookingService.js"; import { BookingSummaryClient } from "../assets/templates/BookingSummaryClient.js"; +import { BookingSummaryVendor } from "../assets/templates/BookingSummaryVendor.js"; import sendMail from "../util/sendMail.js"; import fs from "fs"; import path from "path"; @@ -377,7 +378,6 @@ export const createBookings = async (req, res) => { ...cartItemPlainObject, }); } - console.log(bookings); let createdBookings = []; // Start transaction to create bookings const session = await mongoose.startSession(); @@ -386,11 +386,13 @@ export const createBookings = async (req, res) => { // Create booking const booking = new BookingModel(bookingDetails); await booking.save(); - await booking.populate("vendorId"); - await booking.populate({ - path: "activityId", - populate: [{ path: "theme" }, { path: "subtheme" }], - }); + await booking.populate([ + { + path: "activityId", + populate: [{ path: "theme" }, { path: "subtheme" }], + }, + { path: "vendorId" }, + ]); createdBookings.push(booking); } @@ -399,7 +401,8 @@ export const createBookings = async (req, res) => { await session.commitTransaction(); session.endSession(); - await sendBookingSummaryEmail(createdBookings, billingEmail); + await sendBookingSummaryEmailClient(createdBookings, billingEmail); + await sendBookingSummaryEmailVendor(createdBookings); res.status(200).json({ message: "Successfully created bookings!", bookings: createdBookings, @@ -607,8 +610,8 @@ export const updateCompletedBookings = async (req, res) => { } }; -//Generate the PDF for the Client -export const sendBookingSummaryEmail = async (data, email) => { +//Generate the PDF for the Client And Send Email +export const sendBookingSummaryEmailClient = async (data, email) => { const pdfContent = BookingSummaryClient(data); const filename = "bookingSummary" + Date.now() + ".pdf"; const pdfFilePath = path.join(process.cwd(), "temp", filename); @@ -649,3 +652,52 @@ export const sendBookingSummaryEmail = async (data, email) => { }); }); }; + +//Generate the PDF for the vendor And send Email +export const sendBookingSummaryEmailVendor = async (data) => { + data.forEach((booking) => { + const pdfContent = BookingSummaryVendor(booking); + const filename = + "bookingSummary" + booking.vendorId.companyName + Date.now() + ".pdf"; + const pdfFilePath = path.join(process.cwd(), "temp", filename); + + pdf.create(pdfContent, {}).toFile(pdfFilePath, (err) => { + if (err) { + // Handle errors appropriately + console.error(err); + res.status(500).send("Error generating PDF"); + } + const options = { + to: booking.vendorId.companyEmail, + subject: "You have a New Booking!", + text: "You have a New Booking!", + attachments: [ + { + filename: "BookingSummary.pdf", + path: pdfFilePath, + contentType: "application/pdf", + }, + ], + }; + + sendMail(options).then(() => { + fs.access(pdfFilePath, fs.constants.F_OK, (err) => { + if (err) { + console.error( + "File does not exist or cannot be accessed:", + err + ); + } else { + fs.unlink(pdfFilePath, (deleteError) => { + if (deleteError) { + console.error("Error deleting the file:", deleteError); + } else { + console.log("File deleted successfully."); + } + }); + } + }); + }); + }); + }); +}; From 9fbeb0297bf2924b8a1b76dce7e1752f3ef3e699 Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:49:21 +0800 Subject: [PATCH 07/11] Removed Comments from pdf --- server/assets/templates/BookingSummaryVendor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/assets/templates/BookingSummaryVendor.js b/server/assets/templates/BookingSummaryVendor.js index b6d3182..a4eaaa8 100644 --- a/server/assets/templates/BookingSummaryVendor.js +++ b/server/assets/templates/BookingSummaryVendor.js @@ -102,7 +102,6 @@ export const BookingSummaryVendor = (booking) => { }
  • Theme: ${booking.activityId.theme.name}
  • Sub-Theme: ${booking.activityId.subtheme[0].name}
  • -
  • Comments : ${booking.activityId.comments}
  • ${booking.totalPax} From 94b19430f26a9852a6c52e849f25e356e3162c34 Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Mon, 30 Oct 2023 21:36:38 +0800 Subject: [PATCH 08/11] Client details for Vendor Template --- server/assets/templates/BookingSummaryVendor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/assets/templates/BookingSummaryVendor.js b/server/assets/templates/BookingSummaryVendor.js index a4eaaa8..6cd22bb 100644 --- a/server/assets/templates/BookingSummaryVendor.js +++ b/server/assets/templates/BookingSummaryVendor.js @@ -101,7 +101,8 @@ export const BookingSummaryVendor = (booking) => { booking.activityId.activityType }
  • Theme: ${booking.activityId.theme.name}
  • -
  • Sub-Theme: ${booking.activityId.subtheme[0].name}
  • +
  • Sub-Theme: ${booking.activityId.subtheme[0].name}
  • +
  • Booked By: ${booking.clientId.companyName}
  • ${booking.totalPax} From 0103306e9bcd94f8dd4f6c67050aa8b908e6b47d Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Wed, 1 Nov 2023 17:34:13 +0800 Subject: [PATCH 09/11] Added Booking Summary Download Endpoint --- server/controller/bookingController.js | 89 +++++++++++++++++++++++++- server/routes/gleek/booking.js | 30 ++++++--- 2 files changed, 110 insertions(+), 9 deletions(-) diff --git a/server/controller/bookingController.js b/server/controller/bookingController.js index a6e6ced..5ad9192 100644 --- a/server/controller/bookingController.js +++ b/server/controller/bookingController.js @@ -11,7 +11,7 @@ import { } from "../service/bookingService.js"; import VendorModel from "../model/vendorModel.js"; import { - getAllPendingAndConfirmedBookingsForVendor, + //getAllPendingAndConfirmedBookingsForVendor, getAllBookingsForClientService, } from "../service/bookingService.js"; import { s3GetImages } from "../service/s3ImageServices.js"; @@ -779,3 +779,90 @@ export const sendBookingSummaryEmailVendor = async (data) => { }); }); }; + +export const getBookingSummaryPdfUrl = async (req, res) => { + try { + const userRole = req.cookies.userRole; + const bookingId = req.params.id; + + const booking = await BookingModel.findById(bookingId); + + if (!booking) { + return res.status(400).send("Booking Not Found"); + } + await booking.populate([ + { + path: "activityId", + populate: [{ path: "theme" }, { path: "subtheme" }], + }, + { path: "vendorId" }, + { path: "clientId" }, + ]); + const pdfContent = + userRole == "Vendor" + ? BookingSummaryVendor([booking]) + : BookingSummaryClient([booking]); + + const filename = + userRole == "Vendor" + ? "bookingSummary" + + booking.vendorId.companyName + + Date.now() + + ".pdf" + : "bookingSummary" + + booking.clientId.companyName + + Date.now() + + ".pdf"; + const pdfFilePath = path.join(process.cwd(), "temp", filename); + + console.log("Reached PDF"); + + pdf.create(pdfContent, {}).toFile(pdfFilePath, (err) => { + if (err) { + // Handle errors appropriately + console.error(err); + res.status(500).send("Error generating PDF"); + } else { + // Respond with a success message or the file path + res.status(200).send(filename); + } + }); + } catch (error) { + res.status(500).json({ error: "Server error", message: error.message }); + } +}; + +export const getBookingSummaryPdf = async (req, res) => { + const filePath = req.params; + const file = path.join(process.cwd(), "temp", filePath.path); + fs.access(file, fs.constants.F_OK, (err) => { + if (err) { + res.status(500).json({ + err: "Error Accessing File: ", + message: err.message, + }); + } else { + // File exists, proceed to serve and then delete + res.download(file, "BookingSummary.pdf", (downloadError) => { + if (downloadError) { + res.status(500).json({ + err: "Error downloading the file:", + message: err.message, + }); + } else { + // File has been successfully sent to the client, now delete it + fs.unlink(file, (deleteError) => { + if (deleteError) { + res.status(500).json({ + err: "Error deleting the file:", + message: deleteError.message, + }); + } else { + console.log("File deleted successfully."); + } + }); + } + }); + } + }); +}; diff --git a/server/routes/gleek/booking.js b/server/routes/gleek/booking.js index 39e42d2..c2972b1 100644 --- a/server/routes/gleek/booking.js +++ b/server/routes/gleek/booking.js @@ -1,10 +1,12 @@ import express from "express"; import { - getAvailableBookingTimeslots, - createBookings, - getAllBookingsForClient, - getBookingById, - updateBookingStatus, + getAvailableBookingTimeslots, + createBookings, + getAllBookingsForClient, + getBookingById, + updateBookingStatus, + getBookingSummaryPdfUrl, + getBookingSummaryPdf, } from "../../controller/bookingController.js"; import { verifyToken } from "../../middleware/clientAuth.js"; @@ -12,9 +14,9 @@ const router = express.Router(); // Booking router.get( - "/getAvailableBookingTimeslots/:activityId/:selectedDate", - verifyToken, - getAvailableBookingTimeslots + "/getAvailableBookingTimeslots/:activityId/:selectedDate", + verifyToken, + getAvailableBookingTimeslots ); // /gleek/booking/createBookings @@ -26,4 +28,16 @@ router.get("/getAllBookingsForClient", verifyToken, getAllBookingsForClient); router.get("/viewBooking/:id", verifyToken, getBookingById); // /gleek/booking/updateBookingStatus/:id router.patch("/updateBookingStatus/:id", verifyToken, updateBookingStatus); + +router.post( + "/downloadBookingSummaryUrl/:id", + verifyToken, + getBookingSummaryPdfUrl +); + +router.get( + "/downloadBookingSummaryPdf/:path", + verifyToken, + getBookingSummaryPdf +); export default router; From 186888858c630bf48751bb02828a28015e159f3b Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Wed, 1 Nov 2023 21:06:20 +0800 Subject: [PATCH 10/11] Added Download Summary to Frontend --- .../ActivityDetailsPage.jsx | 1 + .../Client/Booking/BookingsDetails.jsx | 708 +++++++++--------- client-frontend/src/zustand/BookingStore.js | 258 ++++--- server/controller/bookingController.js | 2 - 4 files changed, 504 insertions(+), 465 deletions(-) diff --git a/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx b/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx index 5f5ee53..dee9cc7 100644 --- a/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx +++ b/client-frontend/src/containers/ActivityDetailsPage/ActivityDetailsPage.jsx @@ -46,6 +46,7 @@ import VendorProfileItem from "../../components/Vendor/VendorProfileItem"; import "./styles.css"; import Holidays from "date-holidays"; import VendorChatButton from "../../components/Chat/VendorChatButton"; +import useClientStore from "../../zustand/ClientStore"; const ActivityDetailsPage = () => { const { diff --git a/client-frontend/src/containers/Client/Booking/BookingsDetails.jsx b/client-frontend/src/containers/Client/Booking/BookingsDetails.jsx index 9020e4b..5fb264e 100644 --- a/client-frontend/src/containers/Client/Booking/BookingsDetails.jsx +++ b/client-frontend/src/containers/Client/Booking/BookingsDetails.jsx @@ -1,13 +1,13 @@ import { - Box, - Tab, - Chip, - Tabs, - Typography, - Badge, - Divider, - Grid, - Button, + Box, + Tab, + Chip, + Tabs, + Typography, + Badge, + Divider, + Grid, + Button, } from "@mui/material"; import React, { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; @@ -17,352 +17,376 @@ import { lighten, useTheme } from "@mui/material/styles"; import notFound from "../../../assets/not_found.png"; import VendorProfileItem from "../../../components/Vendor/VendorProfileItem"; import { Link } from "react-router-dom"; +import useSnackbarStore from "../../../zustand/SnackbarStore"; const BookingsDetails = () => { - const { bookingId } = useParams(); - const theme = useTheme(); - const { getBookingForClient, currentBooking } = useBookingStore(); - const primary = theme.palette.primary.main; - const secondary = theme.palette.secondary.main; - let color = "default"; // Default color + const { bookingId } = useParams(); + const theme = useTheme(); + const { getBookingForClient, currentBooking, getBookingSummaryPdf } = + useBookingStore(); + const primary = theme.palette.primary.main; + const secondary = theme.palette.secondary.main; + let color = "default"; // Default color + const { openSnackbar } = useSnackbarStore(); - let containerStyle = { - height: "10rem", - width: "10rem", - objectFit: "cover", - borderTopLeftRadius: "4px", - borderTopRightRadius: "4px", - }; + let containerStyle = { + height: "10rem", + width: "10rem", + objectFit: "cover", + borderTopLeftRadius: "4px", + borderTopRightRadius: "4px", + }; - if ( - currentBooking?.status === "PENDING_CONFIRMATION" || - currentBooking?.status === "PENDING_PAYMENT" - ) { - color = "warning"; - } else if ( - currentBooking?.status === "REJECTED" || - currentBooking?.status === "CANCELLED" - ) { - color = "error"; - } else if ( - currentBooking?.status === "CONFIRMED" || - currentBooking?.status === "PAID" - ) { - color = "success"; - } + if ( + currentBooking?.status === "PENDING_CONFIRMATION" || + currentBooking?.status === "PENDING_PAYMENT" + ) { + color = "warning"; + } else if ( + currentBooking?.status === "REJECTED" || + currentBooking?.status === "CANCELLED" + ) { + color = "error"; + } else if ( + currentBooking?.status === "CONFIRMED" || + currentBooking?.status === "PAID" + ) { + color = "success"; + } - useEffect(() => { - getBookingForClient(bookingId); - }, [bookingId]); + const handleBookingSummaryDownload = async (event) => { + try { + await getBookingSummaryPdf(bookingId); + } catch (err) { + openSnackbar(err.response.data.msg, "error"); + } + }; - const isMediumScreen = useMediaQuery((theme) => theme.breakpoints.down("md")); - const activityPath = `/shop/activity/${currentBooking?.activityId._id}`; + useEffect(() => { + getBookingForClient(bookingId); + }, [bookingId]); - return ( - - - Booking Overview - - - - - BOOKING ID: {currentBooking?._id} - - - - {!isMediumScreen && ( - - - - )} - {isMediumScreen && ( - - - - )} - - - - Order placed on{" "} - {new Date(currentBooking?.creationDateTime).toLocaleTimeString()} - {", "} - {new Date(currentBooking?.creationDateTime).toLocaleDateString()} - - - - {!isMediumScreen && ( - - - - )} - {isMediumScreen && ( - - - - )} - - - - - - - Billing information - - - - + theme.breakpoints.down("md") + ); + const activityPath = `/shop/activity/${currentBooking?.activityId._id}`; + + return ( + + + Booking Overview + + + + + BOOKING ID: {currentBooking?._id} + + + + {!isMediumScreen && ( + + + + )} + {isMediumScreen && ( + + + + )} + + + + Order placed on{" "} + {new Date( + currentBooking?.creationDateTime + ).toLocaleTimeString()} + {", "} + {new Date( + currentBooking?.creationDateTime + ).toLocaleDateString()} + + + + {!isMediumScreen && ( + + + + )} + {isMediumScreen && ( + + + + )} + + + + + + + Billing information + + + + + + + + Billing Party Name + + + {currentBooking?.billingPartyName} + + + + + Billing Email + + + {currentBooking?.billingEmail} + + + + + Billing Postal Code + + + {currentBooking?.billingOfficePostalCode} + + + + + Billing Postal Code + + + {currentBooking?.billingOfficePostalCode} + + + + + + + + Booking Details + + + {currentBooking?.vendorId && ( + + )} + + - - - - Billing Party Name - - - {currentBooking?.billingPartyName} - - - - - Billing Email - - - {currentBooking?.billingEmail} - - - - - Billing Postal Code - - - {currentBooking?.billingOfficePostalCode} - - - - - Billing Postal Code - - - {currentBooking?.billingOfficePostalCode} - - - - - - - - Booking Details - - - {currentBooking?.vendorId && ( - - )} - - - - - {currentBooking?.activityId?.preSignedImages && - currentBooking?.activityId?.preSignedImages?.length > 0 && ( - {currentBooking?.activityTitle} - )} - {!currentBooking?.activityId?.preSignedImages?.length > 0 && ( - {currentBooking?.activityTitle} + p={3}> + + + {currentBooking?.activityId?.preSignedImages && + currentBooking?.activityId?.preSignedImages?.length > + 0 && ( + {currentBooking?.activityTitle} + )} + {!currentBooking?.activityId?.preSignedImages?.length > 0 && ( + {currentBooking?.activityTitle} + )} + + + + {currentBooking?.activityTitle} + + + + Date: + {new Date( + currentBooking?.startDateTime + ).toLocaleDateString()} + + + Start Time: + {new Date( + currentBooking?.startDateTime + ).toLocaleTimeString()} + + + End Time: + {new Date( + currentBooking?.endDateTime + ).toLocaleTimeString()} + + + Event Location Type: + {currentBooking?.eventLocationType} + + {currentBooking?.additionalComments && ( + + Comments: + {currentBooking?.additionalComments} + + )} + + + + + Adult: {currentBooking?.totalPax} pax + + + + + + + + + Base Price + + + S$ {currentBooking?.basePricePerPax?.toFixed(2)} x{" "} + {currentBooking?.totalPax} Pax + + + {currentBooking?.weekendAddOnCost !== 0 && ( + + + Weekend Discounts/Add-ons + + + S$ {currentBooking?.weekendAddOnCost?.toFixed(2)} + + + )} + {currentBooking?.onlineAddOnCost !== 0 && ( + + + Online Discounts/Add-ons + + + S$ {currentBooking?.onlineAddOnCost?.toFixed(2)} + + + )} + {currentBooking?.offlineAddOnCost !== 0 && ( + + + Offline Discounts/Add-ons + + + S$ {currentBooking?.offlineAddOnCost?.toFixed(2)} + + )} - - - - {currentBooking?.activityTitle} - - - - Date: - {new Date(currentBooking?.startDateTime).toLocaleDateString()} - - - Start Time: - {new Date(currentBooking?.startDateTime).toLocaleTimeString()} - - - End Time: - {new Date(currentBooking?.endDateTime).toLocaleTimeString()} - - - Event Location Type: - {currentBooking?.eventLocationType} - - {currentBooking?.additionalComments && ( - - Comments: - {currentBooking?.additionalComments} - - )} + + + Total + + + S$ {currentBooking?.totalCost?.toFixed(2)} + - - - - Adult: {currentBooking?.totalPax} pax - - - - - - - - - Base Price - - - S$ {currentBooking?.basePricePerPax?.toFixed(2)} x{" "} - {currentBooking?.totalPax} Pax - - - {currentBooking?.weekendAddOnCost !== 0 && ( - - - Weekend Discounts/Add-ons - - - S$ {currentBooking?.weekendAddOnCost?.toFixed(2)} - - - )} - {currentBooking?.onlineAddOnCost !== 0 && ( - - - Online Discounts/Add-ons - - - S$ {currentBooking?.onlineAddOnCost?.toFixed(2)} - - - )} - {currentBooking?.offlineAddOnCost !== 0 && ( - - - Offline Discounts/Add-ons - - - S$ {currentBooking?.offlineAddOnCost?.toFixed(2)} - - - )} - - - Total - - - S$ {currentBooking?.totalCost?.toFixed(2)} - - - - - - - - + + + + + + + - - ); + ); }; export default BookingsDetails; diff --git a/client-frontend/src/zustand/BookingStore.js b/client-frontend/src/zustand/BookingStore.js index 0f922a9..1e766c3 100644 --- a/client-frontend/src/zustand/BookingStore.js +++ b/client-frontend/src/zustand/BookingStore.js @@ -2,127 +2,143 @@ import { create } from "zustand"; import AxiosConnect from "../utils/AxiosConnect"; const useBookingStore = create((set) => ({ - bookings: [], - pendingBookings: [], - isLoading: false, - currentBooking: null, - setCurrentBooking: (newCurrentBooking) => - set({ currentBooking: newCurrentBooking }), - currentBookingLoading: true, - setCurrentBookingLoading: (newCurrentBookingLoading) => - set({ currentBookingLoading: newCurrentBookingLoading }), - getBookingsForVendor: async () => { - try { - set({ isLoading: true }); - const response = await AxiosConnect.get( - "/gleekVendor/booking/getAllBookings", - ); - set({ bookings: response.data.bookings }); - set({ isLoading: false }); - } catch (error) { - console.error(error.message); - } - }, - getBookingsWithPendingSurvey: async () => { - try { - set({ isLoading: true }); - const response = await AxiosConnect.get("/gleek/booking/pendingSurvey"); - - set({ bookings: response.data.bookings }); - set({ isLoading: false }); - } catch (error) { - console.error(error.message); - throw error; - } - }, - approveBooking: async (bookingId) => { - try { - set({ isLoading: true }); - const response = await AxiosConnect.patch( - `/gleekVendor/booking/confirmBooking/${bookingId}`, - ); - set({ bookings: response.data.bookings }); - set({ isLoading: false }); - return response.data.message; - } catch (error) { - console.error(error.message); - throw new Error("Unexpected Server Error!"); - } - }, - rejectBooking: async (bookingId, rejectionReason) => { - try { - set({ isLoading: true }); - const response = await AxiosConnect.patch( - `/gleekVendor/booking/rejectBooking/${bookingId}`, - { rejectionReason: rejectionReason }, - ); - set({ bookings: response.data.bookings }); - set({ isLoading: false }); - return response.data.message; - } catch (error) { - console.error(error.message); - throw new Error("Unexpected Server Error!"); - } - }, - cancelBooking: async (bookingId, cancelReason) => { - try { - set({ isLoading: true }); - const response = await AxiosConnect.patch( - `/gleekVendor/booking/cancelBooking/${bookingId}`, - { cancelReason: cancelReason }, - ); - set({ bookings: response.data.bookings }); - set({ isLoading: false }); - return response.data.message; - } catch (error) { - console.error(error.message); - throw new Error(error.message); - } - }, - getAllBookingsForClient: async () => { - try { - set({ isLoading: true }); - const response = await AxiosConnect.get( - "/gleek/booking/getAllBookingsForClient", - ); - set({ bookings: response.data.bookings }); - console.log(response.data.bookings); - set({ isLoading: false }); - } catch (error) { - console.error(error.message); - } - }, - getBookingForClient: async (bookingId) => { - try { - set({ currentBookingLoading: true }); - const response = await AxiosConnect.get( - `/gleek/booking/viewBooking/${bookingId}`, - ); - set({ currentBooking: response.data.booking }); - console.log(response.data.booking); - set({ currentBookingLoading: false }); - } catch (error) { - console.error(error.message); - } - }, - cancelBookingForClient: async (bookingId, cancellationReason) => { - try { - set({ isLoading: true }); - const cancelResponse = await AxiosConnect.patch( - `/gleek/booking/updateBookingStatus/${bookingId}`, - { - newStatus: "CANCELLED", - actionByUserType: "CLIENT", - actionRemarks: cancellationReason, - }, - ); - set({ isLoading: false }); - return cancelResponse.data.message; - } catch (error) { - console.error(error.message); - throw new Error(error.message); - } - }, + bookings: [], + pendingBookings: [], + isLoading: false, + currentBooking: null, + setCurrentBooking: (newCurrentBooking) => + set({ currentBooking: newCurrentBooking }), + currentBookingLoading: true, + setCurrentBookingLoading: (newCurrentBookingLoading) => + set({ currentBookingLoading: newCurrentBookingLoading }), + getBookingsForVendor: async () => { + try { + set({ isLoading: true }); + const response = await AxiosConnect.get( + "/gleekVendor/booking/getAllBookings" + ); + set({ bookings: response.data.bookings }); + set({ isLoading: false }); + } catch (error) { + console.error(error.message); + } + }, + getBookingsWithPendingSurvey: async () => { + try { + set({ isLoading: true }); + const response = await AxiosConnect.get( + "/gleek/booking/pendingSurvey" + ); + + set({ bookings: response.data.bookings }); + set({ isLoading: false }); + } catch (error) { + console.error(error.message); + throw error; + } + }, + approveBooking: async (bookingId) => { + try { + set({ isLoading: true }); + const response = await AxiosConnect.patch( + `/gleekVendor/booking/confirmBooking/${bookingId}` + ); + set({ bookings: response.data.bookings }); + set({ isLoading: false }); + return response.data.message; + } catch (error) { + console.error(error.message); + throw new Error("Unexpected Server Error!"); + } + }, + rejectBooking: async (bookingId, rejectionReason) => { + try { + set({ isLoading: true }); + const response = await AxiosConnect.patch( + `/gleekVendor/booking/rejectBooking/${bookingId}`, + { rejectionReason: rejectionReason } + ); + set({ bookings: response.data.bookings }); + set({ isLoading: false }); + return response.data.message; + } catch (error) { + console.error(error.message); + throw new Error("Unexpected Server Error!"); + } + }, + cancelBooking: async (bookingId, cancelReason) => { + try { + set({ isLoading: true }); + const response = await AxiosConnect.patch( + `/gleekVendor/booking/cancelBooking/${bookingId}`, + { cancelReason: cancelReason } + ); + set({ bookings: response.data.bookings }); + set({ isLoading: false }); + return response.data.message; + } catch (error) { + console.error(error.message); + throw new Error(error.message); + } + }, + getAllBookingsForClient: async () => { + try { + set({ isLoading: true }); + const response = await AxiosConnect.get( + "/gleek/booking/getAllBookingsForClient" + ); + set({ bookings: response.data.bookings }); + console.log(response.data.bookings); + set({ isLoading: false }); + } catch (error) { + console.error(error.message); + } + }, + getBookingForClient: async (bookingId) => { + try { + set({ currentBookingLoading: true }); + const response = await AxiosConnect.get( + `/gleek/booking/viewBooking/${bookingId}` + ); + set({ currentBooking: response.data.booking }); + console.log(response.data.booking); + set({ currentBookingLoading: false }); + } catch (error) { + console.error(error.message); + } + }, + cancelBookingForClient: async (bookingId, cancellationReason) => { + try { + set({ isLoading: true }); + const cancelResponse = await AxiosConnect.patch( + `/gleek/booking/updateBookingStatus/${bookingId}`, + { + newStatus: "CANCELLED", + actionByUserType: "CLIENT", + actionRemarks: cancellationReason, + } + ); + set({ isLoading: false }); + return cancelResponse.data.message; + } catch (error) { + console.error(error.message); + throw new Error(error.message); + } + }, + getBookingSummaryPdf: async (id) => { + try { + const response = await AxiosConnect.post( + `/gleek/booking//downloadBookingSummaryUrl/${id}` + ); + window.open( + `http://localhost:5000/gleek/booking/downloadBookingSummaryPdf/${response.data}` + ); + return; + } catch (err) { + console.log(err); + throw new Error(err.message); + } + }, })); export default useBookingStore; diff --git a/server/controller/bookingController.js b/server/controller/bookingController.js index 7208d96..3550c4d 100644 --- a/server/controller/bookingController.js +++ b/server/controller/bookingController.js @@ -843,8 +843,6 @@ export const getBookingSummaryPdfUrl = async (req, res) => { ".pdf"; const pdfFilePath = path.join(process.cwd(), "temp", filename); - console.log("Reached PDF"); - pdf.create(pdfContent, {}).toFile(pdfFilePath, (err) => { if (err) { // Handle errors appropriately From f589cb1e8a6b7810b6d7343477b277d58cc9091f Mon Sep 17 00:00:00 2001 From: Yunus Ali <80503701+yunusali15@users.noreply.github.com> Date: Wed, 1 Nov 2023 23:17:13 +0800 Subject: [PATCH 11/11] Bug Fix for error display --- .../src/containers/Client/Booking/BookingsDetails.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-frontend/src/containers/Client/Booking/BookingsDetails.jsx b/client-frontend/src/containers/Client/Booking/BookingsDetails.jsx index 5fb264e..1915c54 100644 --- a/client-frontend/src/containers/Client/Booking/BookingsDetails.jsx +++ b/client-frontend/src/containers/Client/Booking/BookingsDetails.jsx @@ -58,7 +58,7 @@ const BookingsDetails = () => { try { await getBookingSummaryPdf(bookingId); } catch (err) { - openSnackbar(err.response.data.msg, "error"); + openSnackbar(err, "error"); } };