diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/AdminBookingDate.story.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/AdminBookingDate.story.tsx new file mode 100644 index 000000000..4636e6005 --- /dev/null +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/AdminBookingDate.story.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from "@storybook/react" +import AdminBookingDate, { BookingInfo } from "./AdminBookingDate" +import { Timestamp } from "firebase/firestore" + +const meta: Meta = { + component: AdminBookingDate +} + +export default meta +type Story = StoryObj + +const mockData: BookingInfo = { + bookingId: "djs", + uid: "1", + first_name: "Straight", + last_name: "Zhao", + date_of_birth: Timestamp.fromMillis(0), + phone_number: 69696969, + dietary_requirements: "nothing", + email: "lasdl@gmail.com", + membership: "guest" +} + +const mockDataArray: BookingInfo[] = [] + +for (let i = 0; i < 100; ++i) { + mockDataArray.push(mockData) +} + +export const DefaultAdminBookingDate: Story = { + args: { + dateString: "20/10/2024", + users: mockDataArray + } +} diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/AdminBookingDate.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/AdminBookingDate.tsx new file mode 100644 index 000000000..f5e022c4a --- /dev/null +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/AdminBookingDate.tsx @@ -0,0 +1,61 @@ +import { CombinedUserData } from "@/models/User" +import BookingUserCard from "./BookingUserCard" + +export type BookingInfo = CombinedUserData & { bookingId: string } + +export interface IAdminBookingDate { + /** + * The `unique` string representing the date the users are booked for + */ + dateString: string + + /** + * For more detailed explaination of the date + * + * @example "Friday" + */ + dayName?: string + + /** + * All of the user information associated with the date + */ + users: Readonly[] + + /** + * Callback for when an attempt is made to delete a booking + */ + handleUserDelete: (id: string) => void +} + +/** + * Component to display the available users for each date in a booking + */ +const AdminBookingDate = ({ + dateString, + dayName, + users, + handleUserDelete +}: IAdminBookingDate) => { + return ( +
+ {dayName &&
{dayName}
} +

{dateString}

+
{users.length} Bookings
+
+ Tap on user to toggle information +
+ {users.map((user, index) => { + return ( + + ) + })} +
+ ) +} + +export default AdminBookingDate diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/BookingUserCard.story.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/BookingUserCard.story.tsx new file mode 100644 index 000000000..56020b700 --- /dev/null +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/BookingUserCard.story.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { Timestamp } from "firebase/firestore" +import BookingUserCard from "./BookingUserCard" +import { BookingInfo } from "./AdminBookingDate" + +const meta: Meta = { + component: BookingUserCard +} + +export default meta +type Story = StoryObj + +const mockUser: BookingInfo = { + bookingId: "asd", + uid: "1", + first_name: "Straight", + last_name: "Zhao", + date_of_birth: Timestamp.fromMillis(0), + phone_number: 69696969, + dietary_requirements: + "Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta enim voluptatum id placeat quod exercitationem vero non amet, minima totam voluptas illo ad ipsa autem odio reiciendis optio vel libero quia, consectetur ipsum molestias repellat distinctio a? Non error minima est beatae nostrum, nam, alias officiis, amet dolorem corrupti doloremque!", + email: "lasdl@gmail.com", + membership: "guest" +} + +export const DefaultAdminBookingDateDisplay: Story = { + args: { + user: mockUser + } +} diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/BookingUserCard.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/BookingUserCard.tsx new file mode 100644 index 000000000..ba4b48b86 --- /dev/null +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDate/BookingUserCard.tsx @@ -0,0 +1,61 @@ +import { useState } from "react" +import { BookingInfo } from "./AdminBookingDate" + +interface IBookingUserCard { + index: number + user: Readonly + handleDelete?: (id: string) => void +} + +const BookingUserCard = ({ index, user, handleDelete }: IBookingUserCard) => { + const [isOpen, setIsOpen] = useState(false) + const hasDietaryRequirements = user.dietary_requirements.trim().length > 0 + return ( +
+
setIsOpen(!isOpen)} + > +
+ +

+ {`#${index}`} {user.first_name} {user.last_name} +

+
{user.membership}
+
+ {hasDietaryRequirements && ( +
+
Dietary Reqs
+

{user.dietary_requirements}

+
+ )} +
+
handleDelete?.(user.bookingId)} + > + X +
+
+ {isOpen && ( +
+
+
+ Email: {user.email} +
+
+ Phone Number: {user.phone_number} +
+
+ Emergency Contact: {user.emergency_contact} +
+
+ )} +
+ ) +} + +export default BookingUserCard diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDateDisplay.story.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDateDisplay.story.tsx new file mode 100644 index 000000000..ab513c3cd --- /dev/null +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDateDisplay.story.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from "@storybook/react" +import AdminBookingDateDisplay from "./AdminBookingDateDisplay" +import { Timestamp } from "firebase/firestore" +import { + BookingInfo, + IAdminBookingDate +} from "./AdminBookingDate/AdminBookingDate" + +const meta: Meta = { + component: AdminBookingDateDisplay +} + +export default meta +type Story = StoryObj + +const mockUser: BookingInfo = { + bookingId: "2323", + uid: "1", + first_name: "Straight", + last_name: "Zhao", + date_of_birth: Timestamp.fromMillis(0), + phone_number: 69696969, + dietary_requirements: "nothing", + email: "lasdl@gmail.com", + membership: "guest" +} + +const mockUsersArray: BookingInfo[] = [] + +for (let i = 0; i < 32; ++i) { + mockUsersArray.push(mockUser) +} + +const mockDatesArray: IAdminBookingDate[] = [] + +for (let i = 1; i < 30; ++i) { + mockDatesArray.push({ + dateString: `${i}/10/2002`, + users: mockUsersArray, + handleUserDelete: () => {} + }) +} + +export const DefaultAdminBookingDateDisplay: Story = { + args: { + dates: mockDatesArray + } +} diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDateDisplay.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDateDisplay.tsx new file mode 100644 index 000000000..65a9df155 --- /dev/null +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingDateDisplay/AdminBookingDateDisplay.tsx @@ -0,0 +1,36 @@ +import AdminBookingDate, { + IAdminBookingDate +} from "./AdminBookingDate/AdminBookingDate" + +export interface IAdminBookingDateDisplay { + /** + * The list of dates to be displayed to user + */ + dates: IAdminBookingDate[] + + /** + * Callback to remove the booking with specified `id` from a booking date + */ + handleDelete?: (id: string) => void +} + +const AdminBookingDateDisplay = ({ dates }: IAdminBookingDateDisplay) => { + return ( +
+ {dates.map((date) => { + return ( + + + + ) + })} +
+ ) +} + +export default AdminBookingDateDisplay diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.story.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.story.tsx index 5d7216144..8819b7691 100644 --- a/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.story.tsx +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.story.tsx @@ -1,5 +1,10 @@ import type { Meta, StoryObj } from "@storybook/react" -import AdminBookingView, { BookingMemberColumnFormat } from "./AdminBookingView" +import AdminBookingView from "./AdminBookingView" +import { + BookingInfo, + IAdminBookingDate +} from "./AdminBookingDateDisplay/AdminBookingDate/AdminBookingDate" +import { Timestamp } from "firebase/firestore" const meta: Meta = { component: AdminBookingView @@ -8,24 +13,37 @@ const meta: Meta = { export default meta type Story = StoryObj -const mockData: BookingMemberColumnFormat = { +const mockUser: BookingInfo = { + bookingId: "23132al", uid: "1", - Date: "04/06/2004", - Name: "Ray", - Number: "12345678", - Email: "Ray1111@gmail.com", - "Dietary Requirement": "none" + first_name: "Straight", + last_name: "Zhao", + date_of_birth: Timestamp.fromMillis(0), + phone_number: 69696969, + dietary_requirements: "nothing", + email: "lasdl@gmail.com", + membership: "guest" } -const mockDataArray: BookingMemberColumnFormat[] = [] +const mockUsersArray: BookingInfo[] = [] -for (let i = 0; i < 100; ++i) { - mockDataArray.push(mockData) +for (let i = 0; i < 32; ++i) { + mockUsersArray.push(mockUser) +} + +const mockDatesArray: IAdminBookingDate[] = [] + +for (let i = 1; i < 30; ++i) { + mockDatesArray.push({ + dateString: `${i}/10/2002`, + users: mockUsersArray, + handleUserDelete: () => {} + }) } export const DefaultAdminBookingView: Story = { args: { - data: mockDataArray, + data: mockDatesArray, dateRange: { startDate: new Date("6969-10-10"), endDate: new Date("9696-10-01") diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx index 24f8092fb..9ec85154a 100644 --- a/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx @@ -1,15 +1,13 @@ -import Table from "@/components/generic/ReusableTable/Table" import Button from "@/components/generic/FigmaButtons/FigmaButton" import CalenderIcon from "@/assets/icons/calender.svg" import Calendar from "@/components/generic/Calendar/Calendar" -import { - TABLE_ROW_IDENTIFIER_KEY, - TableRowOperation -} from "@/components/generic/ReusableTable/TableUtils" +import { TableRowOperation } from "@/components/generic/ReusableTable/TableUtils" import { useState, useRef } from "react" import { useClickOutside } from "@/components/utils/Utils" import ModalContainer from "@/components/generic/ModalContainer/ModalContainer" import WrappedAdminBookingCreationPopUp from "./WrappedAdminBookingCreationPopUp" +import AdminBookingDateDisplay from "./AdminBookingDateDisplay/AdminBookingDateDisplay" +import { IAdminBookingDate } from "./AdminBookingDateDisplay/AdminBookingDate/AdminBookingDate" /** * The format of the columns in the admin booking view. @@ -34,7 +32,7 @@ interface IAdminBookingView { * * @example // {Name: "Jon", Phone: "111"} will display `Name` before `Phone` */ - data?: BookingMemberColumnFormat[] + data?: IAdminBookingDate[] /** * @@ -75,22 +73,12 @@ interface IAdminBookingView { * Should be updated with an "empty" default value so the table displays * the headers even if the list of data is empty */ -const defaultData = { - [TABLE_ROW_IDENTIFIER_KEY]: "", - Date: "", - Name: "", - Number: "", - Email: "", - "Dietary Requirement": "", - Membership: "" -} /** * @deprecated not for direct use on any pages, use `WrappedAdminBookingView` instead */ export const AdminBookingView = ({ - data, - rowOperation, + data = [], dateRange, handleDateRangeChange, isUpdating @@ -149,7 +137,7 @@ export const AdminBookingView = ({ ) : null} - - data={data || [defaultData]} - operationType="single-operation" - rowOperations={rowOperation} - // Make sure that this is smaller than the amount we fetch in the `AdminService` for better UX - showPerPage={32} - groupSameRows - /> + + +
{ Timestamp.fromDate(DateUtils.convertLocalDateToUTCDate(startDate)), Timestamp.fromDate(DateUtils.convertLocalDateToUTCDate(endDate)) ) + + const { mutateAsync: deleteBooking, isPending: isDeletingBooking } = + useDeleteBookingMutation() /** * This chooses the fields to display on the booking view table * * Any field additions/deletions require changing `BookingMemberColumnFormat` */ const dataList = useMemo( - () => - data?.flatMap( - (date) => - date.users.map((user) => { - const newData: BookingMemberColumnFormat = { - uid: "" + (): IAdminBookingDate[] => + data?.map((date) => { + const bookingDateObject = new Date( + DateUtils.timestampMilliseconds(date.date) + ) + const bookingDate = DateUtils.formattedNzDate(bookingDateObject) + return { + dateString: bookingDate, + dayName: bookingDateObject.toLocaleDateString("en-NZ", { + weekday: "long" + }), + users: date.users, + handleUserDelete: (bookingId) => { + if ( + confirm( + Messages.deleteUserFromBooking( + date.users.find((user) => user.bookingId === bookingId) + ?.email, + bookingDate + ) + ) + ) { + deleteBooking(bookingId) } - newData.uid = user.bookingId - newData.Date = DateUtils.formattedNzDate( - new Date(DateUtils.timestampMilliseconds(date.date)) - ) - newData.Name = `${user.first_name} ${user.last_name}` - newData.Number = user.phone_number - ? user.phone_number.toString() - : "" - newData.Email = user.email - newData["Dietary Requirement"] = user.dietary_requirements - newData.Emergency = user.emergency_contact - newData.Membership = user.membership - return newData - }) || [] - ), - [data] + } + } + }) || [], + [data, deleteBooking] ) const sortedData = useMemo( () => dataList?.sort( (a, b) => - DateUtils.nzDateStringToMillis(a.Date || "00/00/0000") - - DateUtils.nzDateStringToMillis(b.Date || "00/00/0000") + DateUtils.nzDateStringToMillis(a.dateString || "00/00/0000") - + DateUtils.nzDateStringToMillis(b.dateString || "00/00/0000") ), [dataList] ) - const { mutateAsync: deleteBooking, isPending: isDeletingBooking } = - useDeleteBookingMutation() - const rowOperations: [TableRowOperation] = [ - { - name: "delete booking", - handler: (bookingId: string) => { - const matchingBooking = sortedData?.find( - (data) => data.uid === bookingId - ) - if ( - confirm( - `Are you SURE you want to delete the booking for the user ${matchingBooking?.Email} on the date ${matchingBooking?.Date}? - This can NOT be undone! - ` - ) - ) { - deleteBooking(bookingId) - } - } - } - ] - return ( diff --git a/client/src/services/Utils/Messages.ts b/client/src/services/Utils/Messages.ts index fd1fd098d..49f49e2d9 100644 --- a/client/src/services/Utils/Messages.ts +++ b/client/src/services/Utils/Messages.ts @@ -18,7 +18,19 @@ const Messages = { startDateString?: string, endDateString?: string ) => - `Are you sure you want to add ${first_name} ${last_name} to the dates ${startDateString} - ${endDateString}` as const + `Are you sure you want to add ${first_name} ${last_name} to the dates ${startDateString} - ${endDateString}` as const, + + /** + * Generates a confirmation message for deleting a user's booking. + * + * This function constructs a confirmation message string that includes the user's email and the booking date. + * The message warns the user that the deletion action is irreversible. + * + * @param {string} [email] - The email of the user whose booking is to be deleted. This parameter is optional. + * @param {string} [dateString] - The date of the booking to be deleted. This parameter is optional. + */ + deleteUserFromBooking: (email?: string, dateString?: string) => + `Are you SURE you want to delete the booking for the user with email ${email} on the date ${dateString}? This can NOT be undone!` as const } as const export default Messages