From 557613301db29bb74ef12e22a7216062e261e509 Mon Sep 17 00:00:00 2001 From: Benson Cho <100653148+bcho892@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:54:16 +1200 Subject: [PATCH] add functionality to booking creation popup and integrate into page (#501) * integrate modal and lazy load protected pages * include booking slots in component * change conditional * update placeholder * revert lazy changes * set up code for handling functionality * fix endpoint * working implementation * memoize and add proper handler --- .../AdminBookingCreationPopUp.tsx | 272 ++++++++++++++---- .../AdminBookingView/AdminBookingView.tsx | 26 +- .../WrappedAdminBookingCreationPopUp.tsx | 52 ++++ .../Admin/AdminMemberView/AdminSearchBar.tsx | 8 +- .../BookingCreation/BookingCreation.tsx | 33 +-- client/src/components/utils/DateUtils.tsx | 27 ++ client/src/models/__generated__/schema.d.ts | 14 +- client/src/services/Admin/AdminMutations.ts | 18 +- client/src/services/Admin/AdminQueries.ts | 3 +- client/src/services/Admin/AdminService.ts | 28 ++ server/src/middleware/__generated__/routes.ts | 14 +- .../src/middleware/__generated__/swagger.json | 34 ++- server/src/middleware/routes.test.ts | 81 +----- .../controllers/BookingController.ts | 33 +-- .../request-models/UserRequests.ts | 8 + 15 files changed, 467 insertions(+), 184 deletions(-) create mode 100644 client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingCreationPopUp.tsx diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingCreationPopUp.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingCreationPopUp.tsx index b700e58c6..6126121d9 100644 --- a/client/src/components/composite/Admin/AdminBookingView/AdminBookingCreationPopUp.tsx +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingCreationPopUp.tsx @@ -3,17 +3,44 @@ import { CombinedUserData } from "models/User" import Calendar from "components/generic/Calendar/Calendar" import Button from "components/generic/FigmaButtons/FigmaButton" import DateRangePicker from "components/generic/DateRangePicker/DateRangePicker" -import { useState, useMemo } from "react" +import { useState, useMemo, useRef } from "react" import CloseButton from "assets/icons/x.svg?react" import LeftArrowButton from "assets/icons/leftarrow.svg?react" import Tick from "assets/icons/tick.svg?react" +import { NEXT_YEAR_FROM_TODAY, TODAY } from "utils/Constants" +import { DateRange, DateUtils } from "components/utils/DateUtils" +import { BookingAvailability } from "models/Booking" +import { Timestamp } from "firebase/firestore" +import { useClickOutside } from "components/utils/Utils" interface IAdminBookingCreationPopUp { - bookingCreationHandler?: () => void + /** + * Performs the required mutation to add the user(s) to the bookings within date range. + */ + bookingCreationHandler?: ( + startDate: Timestamp, + endDate: Timestamp, + selectedUserUid: string + ) => void + /** * Callback for when a 'close' event is triggered with the modal open */ handleClose?: () => void + + /** + * When the user is in process of being added to avoid extra calls + */ + isPending?: boolean + + /** + * The "unfiltered" booking slots for processing + */ + bookingSlots?: BookingAvailability[] + + /** + * Users to display during a search + */ users?: CombinedUserData[] } @@ -24,14 +51,57 @@ enum FlowStages { const Divider = () => { return ( -
+
) } const AdminBookingCreationPopUp = ({ handleClose, - users = [] + bookingCreationHandler, + bookingSlots = [], + users = [], + isPending }: IAdminBookingCreationPopUp) => { + const containerRef = useRef(null) + useClickOutside(containerRef, () => handleClose?.()) + + const [selectedDateRange, setSelectedDateRange] = useState({ + startDate: new Date(), + endDate: new Date() + }) + + const { startDate: currentStartDate, endDate: currentEndDate } = + selectedDateRange + + const disabledDates = DateUtils.unavailableDates(bookingSlots) + + /** + * Function to be called to confirm the date range selected by the user. + * + * Will notify user if an unavailable date was included in the new date range + * + * @param startDate the first date of the range + * @param endDate the last date of the range + */ + const checkValidRange = (startDate: Date, endDate: Date) => { + const dateArray = DateUtils.datesToDateRange(startDate, endDate) + if ( + dateArray.some( + (date) => + disabledDates.some((disabledDate) => + DateUtils.dateEqualToTimestamp(date, disabledDate.date) + ) || + !bookingSlots.some((slot) => + DateUtils.dateEqualToTimestamp(date, slot.date) + ) + ) + ) { + alert("Invalid date range, some dates are unavailable") + return false + } + return true + } + const [currentSearchQuery, setCurrentSearchQuery] = useState< string | undefined >(undefined) @@ -61,8 +131,9 @@ const AdminBookingCreationPopUp = ({ if (currentSearchQuery) { return users.filter( (user) => - user.email.toLowerCase().includes(currentSearchQuery) || - user.first_name.toLowerCase().includes(currentSearchQuery) || + (user.email.toLowerCase().includes(currentSearchQuery) || + user.first_name.toLowerCase().includes(currentSearchQuery) || + user.last_name.toLowerCase().includes(currentSearchQuery)) && user.membership !== "admin" ) } else { @@ -104,52 +175,63 @@ const AdminBookingCreationPopUp = ({ [usersToDisplay] ) - const DetailedUserInfoPanel = () => ( -
- -
- {currentlySelectedUser?.membership} -
-
setCurrentSelectedUserUid(undefined)} - className="ml-auto h-[15px] w-[15px] cursor-pointer" - > - + const DetailedUserInfoPanel = useMemo( + () => ( +
+ +
+ {currentlySelectedUser?.membership} +
+
setCurrentSelectedUserUid(undefined)} + className="ml-auto h-[15px] w-[15px] cursor-pointer" + > + +
+
+

+ {currentlySelectedUser?.first_name} {currentlySelectedUser?.last_name} +

+
+

Allergies/Dietary Requirements

+

{currentlySelectedUser?.dietary_requirements}

- -

- {currentlySelectedUser?.first_name} {currentlySelectedUser?.last_name} -

-
-

Allergies/Dietary Requirements

-

{currentlySelectedUser?.dietary_requirements}

-
-
-

Email

-

{currentlySelectedUser?.email}

-
-
-

Number

-

{currentlySelectedUser?.phone_number}

+
+

Email

+

{currentlySelectedUser?.email}

+
+
+

Number

+

{currentlySelectedUser?.phone_number}

+
-
+ ), + [currentlySelectedUser] ) return ( -
+

Add a booking

-
-
-

Select user

- onQueryChanged(newQuery)} - /> +
+
+ +

Select user

+ onQueryChanged(newQuery)} + /> +
{currentStage === FlowStages.SELECT_DATES && (

Creating booking for:

@@ -174,12 +256,11 @@ const AdminBookingCreationPopUp = ({
)} - {currentStage !== - FlowStages.SEARCH_FOR_USER ? null : currentSelectedUserUid ? ( - - ) : ( - UserList - )} + {currentStage !== FlowStages.SEARCH_FOR_USER + ? null + : currentSelectedUserUid + ? DetailedUserInfoPanel + : UserList} {currentStage === FlowStages.SELECT_DATES ? (
-
+
- + + view !== "year" && + (!bookingSlots.some((slot) => + DateUtils.UTCDatesEqual(slot.date, date) + ) || + disabledDates.some((slot) => + DateUtils.UTCDatesEqual(slot.date, date) + )) + } + tileContent={({ date }) => { + const slot = bookingSlots.find( + (slot) => + DateUtils.UTCDatesEqual(slot.date, date) && + slot.availableSpaces > 0 + ) + return slot ? ( +

+ {slot?.availableSpaces}/{slot.maxBookings} +

+ ) : null + }} + onChange={(e) => { + const [start, end] = e as [Date, Date] + if ( + checkValidRange( + DateUtils.convertLocalDateToUTCDate(start), + DateUtils.convertLocalDateToUTCDate(end) + ) + ) { + setSelectedDateRange({ + startDate: start, + endDate: end + }) + } + }} + returnValue="range" + /> {}} + valueStart={selectedDateRange.startDate} + valueEnd={selectedDateRange.endDate} + handleDateRangeInputChange={(start, end) => { + const newStartDate = start || currentStartDate + const newEndDate = end || currentEndDate + if ( + checkValidRange( + DateUtils.convertLocalDateToUTCDate(newStartDate), + DateUtils.convertLocalDateToUTCDate(newEndDate) + ) + ) { + setSelectedDateRange({ + startDate: newStartDate, + endDate: newEndDate + }) + } + }} /> -
diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx index 2fef6896c..9d2560d86 100644 --- a/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx @@ -6,8 +6,9 @@ import { TABLE_ROW_IDENTIFIER_KEY, TableRowOperation } from "components/generic/ReusableTable/TableUtils" -import { useState, useRef } from "react" +import { useState, useRef, lazy, Suspense } from "react" import { useClickOutside } from "components/utils/Utils" +import ModalContainer from "components/generic/ModalContainer/ModalContainer" export type BookingMemberColumnFormat = { /** @@ -73,6 +74,13 @@ const defaultData = { "Dietary Requirement": "" } +const AsyncWrappedAdminBookingCreationPopup = lazy( + () => + import( + "components/composite/Admin/AdminBookingView/WrappedAdminBookingCreationPopUp" + ) +) + /** * @deprecated not for direct use on any pages, use `WrappedAdminBookingView` instead */ @@ -86,6 +94,8 @@ export const AdminBookingView = ({ // Have state for if the calendar is displayed or not const [displayedCalendar, setDisplayedCalendar] = useState(false) + const [openAddBookingPopup, setOpenAddBookingPopup] = useState(false) + // Add handler for when the Pick Date button is clicked const onClickHandler = () => { setDisplayedCalendar(!displayedCalendar) @@ -102,7 +112,12 @@ export const AdminBookingView = ({

Bookings

- +
@@ -146,6 +161,13 @@ export const AdminBookingView = ({ showPerPage={15} />
+ + Loading}> + setOpenAddBookingPopup(false)} + /> + + ) } diff --git a/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingCreationPopUp.tsx b/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingCreationPopUp.tsx new file mode 100644 index 000000000..5410f81b8 --- /dev/null +++ b/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingCreationPopUp.tsx @@ -0,0 +1,52 @@ +import { useUsersQuery } from "services/Admin/AdminQueries" +import AdminBookingCreationPopUp from "./AdminBookingCreationPopUp" +import { useEffect, useMemo } from "react" +import { useAvailableBookingsQuery } from "services/Booking/BookingQueries" +import { useAddUserToBookingMutation } from "services/Admin/AdminMutations" + +interface IWrappedAdminBookingCreationPopUp { + handleClose: () => void +} + +const WrappedAdminBookingCreationPopUp = ({ + handleClose +}: IWrappedAdminBookingCreationPopUp) => { + const { + data: userPages, + fetchNextPage, + isFetchingNextPage, + hasNextPage + } = useUsersQuery() + + const { data: bookingSlots } = useAvailableBookingsQuery() + + const { mutateAsync: handleAddUserToBooking, isPending } = + useAddUserToBookingMutation() + + useEffect(() => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }, [fetchNextPage, isFetchingNextPage, hasNextPage]) + + const users = useMemo( + () => userPages?.pages.flatMap((page) => page.data || []), + [userPages] + ) + + return ( + + { + await handleAddUserToBooking({ startDate, endDate, userIds: [uid] }) + }} + /> + + ) +} + +export default WrappedAdminBookingCreationPopUp diff --git a/client/src/components/composite/Admin/AdminMemberView/AdminSearchBar.tsx b/client/src/components/composite/Admin/AdminMemberView/AdminSearchBar.tsx index d7481f4a6..071015d2d 100644 --- a/client/src/components/composite/Admin/AdminMemberView/AdminSearchBar.tsx +++ b/client/src/components/composite/Admin/AdminMemberView/AdminSearchBar.tsx @@ -2,6 +2,7 @@ import TextInput from "components/generic/TextInputComponent/TextInput" import { debounce } from "components/utils/Utils" interface IAdminSearchBar { + placeholder?: string /** * @param newQuery a **lower case** string representing the new query value */ @@ -10,7 +11,10 @@ interface IAdminSearchBar { const ADMIN_SEARCH_BAR_DEFAULT_DEBOUNCE = 300 as const -const AdminSearchBar = ({ onQueryChanged }: IAdminSearchBar) => { +const AdminSearchBar = ({ + onQueryChanged, + placeholder = "search" +}: IAdminSearchBar) => { const changeHandler = (e: React.ChangeEvent) => { const debouncedCallback = debounce( () => onQueryChanged?.(e.target.value.toLowerCase()), @@ -24,7 +28,7 @@ const AdminSearchBar = ({ onQueryChanged }: IAdminSearchBar) => { data-testid="search-input" type="text" onChange={(e) => changeHandler(e)} - placeholder="search" + placeholder={placeholder} /> ) } diff --git a/client/src/components/composite/Booking/BookingCreation/BookingCreation.tsx b/client/src/components/composite/Booking/BookingCreation/BookingCreation.tsx index 15340d79a..3bab86343 100644 --- a/client/src/components/composite/Booking/BookingCreation/BookingCreation.tsx +++ b/client/src/components/composite/Booking/BookingCreation/BookingCreation.tsx @@ -9,19 +9,7 @@ import { BookingAvailability } from "models/Booking" import { NEXT_YEAR_FROM_TODAY, TODAY } from "utils/Constants" import { Timestamp } from "firebase/firestore" import Checkbox from "components/generic/Checkbox/Checkbox" -import { DateUtils, UnknownTimestamp } from "components/utils/DateUtils" - -type DateRange = { - /** - * Javascript date object representing the date of the first night for the booking - */ - startDate: Date - - /** - * Javascript date object representing the date of the last night for the booking - */ - endDate: Date -} +import { DateRange, DateUtils } from "components/utils/DateUtils" /* * Swaps around dates if invalid @@ -74,12 +62,6 @@ interface ICreateBookingSection { */ isPending?: boolean } -const UTCDatesEqual = (slot: UnknownTimestamp, date: Date) => { - return DateUtils.dateEqualToTimestamp( - DateUtils.convertLocalDateToUTCDate(date), - slot - ) -} const NORMAL_PRICE = 40 as const const SPECIAL_PRICE = 60 as const @@ -101,7 +83,7 @@ export const CreateBookingSection = ({ const { startDate: currentStartDate, endDate: currentEndDate } = selectedDateRange - const disabledDates = bookingSlots.filter((slot) => slot.availableSpaces <= 0) + const disabledDates = DateUtils.unavailableDates(bookingSlots) /** * Function to be called to confirm the date range selected by the user. @@ -216,13 +198,18 @@ export const CreateBookingSection = ({ } tileDisabled={({ date, view }) => view !== "year" && - (!bookingSlots.some((slot) => UTCDatesEqual(slot.date, date)) || - disabledDates.some((slot) => UTCDatesEqual(slot.date, date))) + (!bookingSlots.some((slot) => + DateUtils.UTCDatesEqual(slot.date, date) + ) || + disabledDates.some((slot) => + DateUtils.UTCDatesEqual(slot.date, date) + )) } tileContent={({ date }) => { const slot = bookingSlots.find( (slot) => - UTCDatesEqual(slot.date, date) && slot.availableSpaces > 0 + DateUtils.UTCDatesEqual(slot.date, date) && + slot.availableSpaces > 0 ) return slot ? (

diff --git a/client/src/components/utils/DateUtils.tsx b/client/src/components/utils/DateUtils.tsx index d68036a20..07b144555 100644 --- a/client/src/components/utils/DateUtils.tsx +++ b/client/src/components/utils/DateUtils.tsx @@ -1,5 +1,17 @@ +import { BookingAvailability } from "models/Booking" import { MS_IN_SECOND } from "utils/Constants" +export type DateRange = { + /** + * Javascript date object representing the date of the first night for the booking + */ + startDate: Date + + /** + * Javascript date object representing the date of the last night for the booking + */ + endDate: Date +} /** * Utility type to allow us to handle cases where the timestamp may actually have * `_seconds` or `_nanoseconds` @@ -7,6 +19,21 @@ import { MS_IN_SECOND } from "utils/Constants" export interface UnknownTimestamp extends Record {} export const DateUtils = { + /** + * gets a list of dates that are unavailable from availability objects + * + * @param bookingSlots the list of booking slots for which to search for unavailbale dates + * @returns the fully unavailable dates + */ + unavailableDates: (bookingSlots: BookingAvailability[]) => + bookingSlots.filter((slot) => slot.availableSpaces <= 0), + + UTCDatesEqual: (slot: UnknownTimestamp, date: Date) => { + return DateUtils.dateEqualToTimestamp( + DateUtils.convertLocalDateToUTCDate(date), + slot + ) + }, datesToDateRange: (startDate: Date, endDate: Date) => { const dateArray = [] const currentDate = new Date(startDate) diff --git a/client/src/models/__generated__/schema.d.ts b/client/src/models/__generated__/schema.d.ts index b93e61a08..b41782795 100644 --- a/client/src/models/__generated__/schema.d.ts +++ b/client/src/models/__generated__/schema.d.ts @@ -207,12 +207,13 @@ export interface components { }[]; error?: string; }; - /** @description Represents the structure of a request model for fetching bookings within a specific date range. */ - BookingsByDateRangeRequestModel: { + CreateBookingsRequestModel: { /** @description Firestore timestamp, should represent a UTC date that is set to exactly midnight */ startDate: components["schemas"]["FirebaseFirestore.Timestamp"]; /** @description Firestore timestamp, should represent a UTC date that is set to exactly midnight */ endDate: components["schemas"]["FirebaseFirestore.Timestamp"]; + /** @description List of users to add to the bookings between date range */ + userIds: string[]; }; AllUserBookingSlotsResponse: { error?: string; @@ -278,6 +279,13 @@ export interface components { }[]; error?: string; }; + /** @description Represents the structure of a request model for fetching bookings within a specific date range. */ + BookingsByDateRangeRequestModel: { + /** @description Firestore timestamp, should represent a UTC date that is set to exactly midnight */ + startDate: components["schemas"]["FirebaseFirestore.Timestamp"]; + /** @description Firestore timestamp, should represent a UTC date that is set to exactly midnight */ + endDate: components["schemas"]["FirebaseFirestore.Timestamp"]; + }; BookingSlotUpdateResponse: { error?: string; message?: string; @@ -536,7 +544,7 @@ export interface operations { CreateBookings: { requestBody: { content: { - "application/json": components["schemas"]["BookingsByDateRangeRequestModel"]; + "application/json": components["schemas"]["CreateBookingsRequestModel"]; }; }; responses: { diff --git a/client/src/services/Admin/AdminMutations.ts b/client/src/services/Admin/AdminMutations.ts index 67db70248..856981a09 100644 --- a/client/src/services/Admin/AdminMutations.ts +++ b/client/src/services/Admin/AdminMutations.ts @@ -3,7 +3,10 @@ import AdminService from "./AdminService" import { Timestamp } from "firebase/firestore" import queryClient from "services/QueryClient" import { BOOKING_AVAILABLITY_KEY } from "services/Booking/BookingQueries" -import { ALL_USERS_QUERY } from "./AdminQueries" +import { + ALL_BOOKINGS_BETWEEN_RANGE_QUERY, + ALL_USERS_QUERY +} from "./AdminQueries" import { CombinedUserData } from "models/User" import { replaceUserInPage } from "./AdminUtils" @@ -116,3 +119,16 @@ export function useMakeDatesUnavailableMutation( } }) } + +export function useAddUserToBookingMutation() { + return useMutation({ + mutationKey: ["add-users-to-booking"], + retry: false, + mutationFn: AdminService.addUsersToBookingForDateRange, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ALL_BOOKINGS_BETWEEN_RANGE_QUERY] + }) + } + }) +} diff --git a/client/src/services/Admin/AdminQueries.ts b/client/src/services/Admin/AdminQueries.ts index 38d21adce..4882f6499 100644 --- a/client/src/services/Admin/AdminQueries.ts +++ b/client/src/services/Admin/AdminQueries.ts @@ -3,6 +3,7 @@ import { Timestamp } from "firebase/firestore" import AdminService from "./AdminService" export const ALL_USERS_QUERY = "allUsers" +export const ALL_BOOKINGS_BETWEEN_RANGE_QUERY = "bookings-between-range" export function useUsersQuery() { return useInfiniteQuery({ @@ -19,7 +20,7 @@ export function useAdminBookingsQuery( endDate: Timestamp ) { return useQuery({ - queryKey: ["bookingsBetweenRange", startDate, endDate], + queryKey: [ALL_BOOKINGS_BETWEEN_RANGE_QUERY, startDate, endDate], queryFn: () => AdminService.getBookingsBetweenDateRange({ startDate, diff --git a/client/src/services/Admin/AdminService.ts b/client/src/services/Admin/AdminService.ts index 0d68a7c52..bae3d746b 100644 --- a/client/src/services/Admin/AdminService.ts +++ b/client/src/services/Admin/AdminService.ts @@ -130,6 +130,34 @@ const AdminService = { `Failed to make dates ${startDate.toString()} to ${endDate.toString()} available` ) return data + }, + addUsersToBookingForDateRange: async function ({ + startDate, + endDate, + userIds + }: { + startDate: Timestamp + endDate: Timestamp + userIds: string[] + }) { + const { response, data } = await fetchClient.POST( + "/bookings/create-bookings", + { + body: { + startDate, + endDate, + userIds + } + } + ) + + if (!response.ok) { + throw new Error( + `Failed to add the users ${userIds.join(",")} to the date range ${startDate.toString()} to ${endDate.toString()} ` + ) + } + + return data?.data } } as const diff --git a/server/src/middleware/__generated__/routes.ts b/server/src/middleware/__generated__/routes.ts index 10508f6c6..6f3a659ef 100644 --- a/server/src/middleware/__generated__/routes.ts +++ b/server/src/middleware/__generated__/routes.ts @@ -172,11 +172,12 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "BookingsByDateRangeRequestModel": { + "CreateBookingsRequestModel": { "dataType": "refObject", "properties": { "startDate": {"ref":"FirebaseFirestore.Timestamp","required":true}, "endDate": {"ref":"FirebaseFirestore.Timestamp","required":true}, + "userIds": {"dataType":"array","array":{"dataType":"string"},"required":true}, }, "additionalProperties": false, }, @@ -256,6 +257,15 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "BookingsByDateRangeRequestModel": { + "dataType": "refObject", + "properties": { + "startDate": {"ref":"FirebaseFirestore.Timestamp","required":true}, + "endDate": {"ref":"FirebaseFirestore.Timestamp","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "BookingSlotUpdateResponse": { "dataType": "refObject", "properties": { @@ -653,7 +663,7 @@ export function RegisterRoutes(app: Router) { function BookingController_createBookings(request: ExRequest, response: ExResponse, next: any) { const args: Record = { - requestBody: {"in":"body","name":"requestBody","required":true,"ref":"BookingsByDateRangeRequestModel"}, + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"CreateBookingsRequestModel"}, }; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git a/server/src/middleware/__generated__/swagger.json b/server/src/middleware/__generated__/swagger.json index ee5f2261b..3b9bddc26 100644 --- a/server/src/middleware/__generated__/swagger.json +++ b/server/src/middleware/__generated__/swagger.json @@ -375,8 +375,7 @@ "type": "object", "additionalProperties": false }, - "BookingsByDateRangeRequestModel": { - "description": "Represents the structure of a request model for fetching bookings within a specific date range.", + "CreateBookingsRequestModel": { "properties": { "startDate": { "$ref": "#/components/schemas/FirebaseFirestore.Timestamp", @@ -385,11 +384,19 @@ "endDate": { "$ref": "#/components/schemas/FirebaseFirestore.Timestamp", "description": "Firestore timestamp, should represent a UTC date that is set to exactly midnight" + }, + "userIds": { + "items": { + "type": "string" + }, + "type": "array", + "description": "List of users to add to the bookings between date range" } }, "required": [ "startDate", - "endDate" + "endDate", + "userIds" ], "type": "object", "additionalProperties": false @@ -594,6 +601,25 @@ "type": "object", "additionalProperties": false }, + "BookingsByDateRangeRequestModel": { + "description": "Represents the structure of a request model for fetching bookings within a specific date range.", + "properties": { + "startDate": { + "$ref": "#/components/schemas/FirebaseFirestore.Timestamp", + "description": "Firestore timestamp, should represent a UTC date that is set to exactly midnight" + }, + "endDate": { + "$ref": "#/components/schemas/FirebaseFirestore.Timestamp", + "description": "Firestore timestamp, should represent a UTC date that is set to exactly midnight" + } + }, + "required": [ + "startDate", + "endDate" + ], + "type": "object", + "additionalProperties": false + }, "BookingSlotUpdateResponse": { "properties": { "error": { @@ -1255,7 +1281,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BookingsByDateRangeRequestModel" + "$ref": "#/components/schemas/CreateBookingsRequestModel" } } } diff --git a/server/src/middleware/routes.test.ts b/server/src/middleware/routes.test.ts index aac1b465a..7b9270210 100644 --- a/server/src/middleware/routes.test.ts +++ b/server/src/middleware/routes.test.ts @@ -1284,19 +1284,18 @@ describe("Endpoints", () => { await cleanFirestore() }) - it("should return userIds for successful bookings within the date range", async () => { + it("should create bookings for userIds within the date range", async () => { const bookingSlotService = new BookingSlotService() - const bookingDataService = new BookingDataService() const startDate = dateToFirestoreTimeStamp(new Date("01/01/2022")) const endDate = dateToFirestoreTimeStamp(new Date("12/31/2023")) - const slot1 = await bookingSlotService.createBookingSlot({ + await bookingSlotService.createBookingSlot({ date: dateToFirestoreTimeStamp(new Date("02/01/2023")), max_bookings: 10 }) - const slot2 = await bookingSlotService.createBookingSlot({ + await bookingSlotService.createBookingSlot({ date: dateToFirestoreTimeStamp(new Date("03/01/2023")), max_bookings: 10 }) @@ -1307,28 +1306,17 @@ describe("Endpoints", () => { max_bookings: 10 }) - await bookingDataService.createBooking({ - user_id: MEMBER_USER_UID, - booking_slot_id: slot1.id, - stripe_payment_id: "" - }) - - await bookingDataService.createBooking({ - user_id: GUEST_USER_UID, - booking_slot_id: slot2.id, - stripe_payment_id: "" - }) - const res = await request .post("/bookings/create-bookings") .set("Authorization", `Bearer ${adminToken}`) .send({ startDate, - endDate + endDate, + userIds: [GUEST_USER_UID, MEMBER_USER_UID] }) expect(res.status).toEqual(200) - expect(res.body.data).toHaveLength(2) + expect(res.body.data).toHaveLength(3) expect.arrayContaining([ expect.objectContaining({ users: expect.arrayContaining([ @@ -1341,57 +1329,6 @@ describe("Endpoints", () => { ]) }) ]) - - // Check to see if the bookings are created correctly - const result1 = - await bookingDataService.getBookingsByUserId(MEMBER_USER_UID) - const result2 = - await bookingDataService.getBookingsByUserId(GUEST_USER_UID) - const result3 = await bookingDataService.getBookingsBySlotId(slot1.id) - - expect(result1).toHaveLength(1) - expect.arrayContaining([ - expect.objectContaining({ booking_slot_id: slot1.id }), - expect.objectContaining({ user_id: MEMBER_USER_UID }), - expect.objectContaining({ stripe_payment_id: "a" }) - ]) - expect(result2).toHaveLength(1) - expect.arrayContaining([ - expect.objectContaining({ booking_slot_id: slot2.id }), - expect.objectContaining({ user_id: GUEST_USER_UID }), - expect.objectContaining({ stripe_payment_id: "" }) - ]) - expect(result1).toEqual(result3) - }) - - it("should return an empty array if no users have bookings within the date range", async () => { - const startDate = dateToFirestoreTimeStamp(new Date("01/01/2024")) - const endDate = dateToFirestoreTimeStamp(new Date("12/31/2024")) - - const bookingSlotService = new BookingSlotService() - const bookingDataService = new BookingDataService() - - const slot1 = await bookingSlotService.createBookingSlot({ - date: dateToFirestoreTimeStamp(new Date("02/01/2025")), // Out of range date - max_bookings: 10 - }) - - await bookingDataService.createBooking({ - user_id: MEMBER_USER_UID, - booking_slot_id: slot1.id, - stripe_payment_id: "" - }) - - const res = await request - .post("/bookings/create-bookings") - .set("Authorization", `Bearer ${adminToken}`) - .send({ - startDate, - endDate - }) - - expect(res.status).toEqual(200) - expect(res.body.data).toHaveLength(0) }) it("should return unauthorized error for non-admin users", async () => { @@ -1403,7 +1340,8 @@ describe("Endpoints", () => { .set("Authorization", `Bearer ${memberToken}`) .send({ startDate, - endDate + endDate, + userIds: [] }) expect(res.status).toEqual(401) @@ -1438,7 +1376,8 @@ describe("Endpoints", () => { .set("Authorization", `Bearer ${adminToken}`) .send({ startDate, - endDate + endDate, + userIds: [MEMBER_USER_UID] }) expect(res.status).toEqual(200) diff --git a/server/src/service-layer/controllers/BookingController.ts b/server/src/service-layer/controllers/BookingController.ts index 51daab6fe..b940798d5 100644 --- a/server/src/service-layer/controllers/BookingController.ts +++ b/server/src/service-layer/controllers/BookingController.ts @@ -1,6 +1,7 @@ import { AvailableDatesRequestModel, - BookingsByDateRangeRequestModel + BookingsByDateRangeRequestModel, + CreateBookingsRequestModel } from "service-layer/request-models/UserRequests" import { AvailableDatesResponse } from "service-layer/response-models/PaymentResponse" import { Timestamp } from "firebase-admin/firestore" @@ -39,7 +40,7 @@ export class BookingController extends Controller { @Security("jwt", ["admin"]) @Post("create-bookings") public async createBookings( - @Body() requestBody: BookingsByDateRangeRequestModel + @Body() requestBody: CreateBookingsRequestModel ): Promise { try { const { startDate, endDate } = requestBody @@ -63,32 +64,18 @@ export class BookingController extends Controller { /** Iterating through each booking slot */ const bookingPromises = bookingSlots.map(async (slot) => { - /** Getting the bookings for the current slot */ - const bookings = await bookingDataService.getBookingsBySlotId(slot.id) - - /** Extracting the all 3 Ids from the bookings */ - const userIds = bookings.map((booking) => booking.user_id) - const slotIds = bookings.map((booking) => booking.booking_slot_id) - const stripePaymentIds = bookings.map( - (booking) => booking.stripe_payment_id - ) - - if (userIds.length === 0) { - return - } - + let userIds = [...requestBody.userIds] /** For every slotid add a booking for that id only if user doesn't already have a booking */ - const userIdsPromises = userIds.map(async (userId, i) => { + const userIdsPromises = userIds.map(async (userId) => { if ( - (await bookingDataService.getBookingsByUserId(userIds[i])) - .length !== 0 + (await bookingDataService.getBookingsByUserId(userId)).length !== 0 ) { - delete userIds[i] // Remove user from list if they already have a booking + userIds = userIds.filter((id) => id !== userId) // Remove user from list if they already have a booking } else { await bookingDataService.createBooking({ - user_id: userIds[i], - booking_slot_id: slotIds[i], - stripe_payment_id: stripePaymentIds[i] + user_id: userId, + booking_slot_id: slot.id, + stripe_payment_id: "manual_entry" }) } }) diff --git a/server/src/service-layer/request-models/UserRequests.ts b/server/src/service-layer/request-models/UserRequests.ts index 52fc059d0..8fe2e1e13 100644 --- a/server/src/service-layer/request-models/UserRequests.ts +++ b/server/src/service-layer/request-models/UserRequests.ts @@ -76,3 +76,11 @@ export interface BookingsByDateRangeRequestModel { */ endDate: Timestamp } + +export interface CreateBookingsRequestModel + extends BookingsByDateRangeRequestModel { + /** + * List of users to add to the bookings between date range + */ + userIds: string[] +}