diff --git a/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx b/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx index 81cc83645..4b3d1253a 100644 --- a/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx +++ b/client/src/components/composite/Admin/AdminBookingView/AdminBookingView.tsx @@ -42,7 +42,7 @@ interface IAdminBookingView { * ] * ``` */ - rowOperations?: TableRowOperation[] + rowOperation?: [TableRowOperation] /** * used to fetch the data once the last page of the table has been reached @@ -86,7 +86,7 @@ const AsyncWrappedAdminBookingCreationPopup = lazy( */ export const AdminBookingView = ({ data, - rowOperations, + rowOperation, dateRange, handleDateRangeChange, isUpdating @@ -153,10 +153,10 @@ export const AdminBookingView = ({ - + data={data || [defaultData]} - operationType="multiple-operations" - rowOperations={rowOperations} + 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 diff --git a/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingView.tsx b/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingView.tsx index 4bda0405e..c6defdec1 100644 --- a/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingView.tsx +++ b/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingView.tsx @@ -4,6 +4,8 @@ import { DateUtils } from "components/utils/DateUtils" import { AdminBookingViewContext } from "./AdminBookingViewContext" import { useContext, useMemo } from "react" import { Timestamp } from "firebase/firestore" +import { TableRowOperation } from "components/generic/ReusableTable/TableUtils" +import { useDeleteBookingMutation } from "services/Admin/AdminMutations" /** * Should be wrapped the `AdminBookingViewProvider` @@ -14,7 +16,7 @@ const WrappedAdminBookingView = () => { handleSelectedDateChange } = useContext(AdminBookingViewContext) - const { data, isLoading } = useAdminBookingsQuery( + const { data, isLoading: isFetchingUsers } = useAdminBookingsQuery( Timestamp.fromDate(DateUtils.convertLocalDateToUTCDate(startDate)), Timestamp.fromDate(DateUtils.convertLocalDateToUTCDate(endDate)) ) @@ -24,7 +26,7 @@ const WrappedAdminBookingView = () => { const newData: BookingMemberColumnFormat = { uid: "" } - newData.uid = user.uid + newData.uid = user.bookingId newData.Date = DateUtils.formattedNzDate( new Date(DateUtils.timestampMilliseconds(date.date)) ) @@ -44,10 +46,34 @@ const WrappedAdminBookingView = () => { ), [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/components/generic/ReusableTable/Table.tsx b/client/src/components/generic/ReusableTable/Table.tsx index 37f3b6d8c..66f33540b 100644 --- a/client/src/components/generic/ReusableTable/Table.tsx +++ b/client/src/components/generic/ReusableTable/Table.tsx @@ -113,6 +113,7 @@ export const OperationButton = <
rowOperations && rowOperations[0]?.handler(uid)} > X diff --git a/client/src/models/__generated__/schema.d.ts b/client/src/models/__generated__/schema.d.ts index 7d3723f2f..6d3deaf57 100644 --- a/client/src/models/__generated__/schema.d.ts +++ b/client/src/models/__generated__/schema.d.ts @@ -250,7 +250,7 @@ export interface components { }; /** @enum {string} */ UserAccountTypes: "admin" | "member" | "guest"; - CombinedUserData: { + BookingIdandUserData: { date_of_birth: components["schemas"]["FirebaseFirestore.Timestamp"]; does_snowboarding?: boolean; does_racing?: boolean; @@ -278,11 +278,12 @@ export interface components { email: string; /** @description What type of account the user has */ membership: components["schemas"]["UserAccountTypes"]; + bookingId: string; }; /** @description Represents the response structure for fetching users by date range. */ UsersByDateRangeResponse: { data?: { - users: components["schemas"]["CombinedUserData"][]; + users: components["schemas"]["BookingIdandUserData"][]; date: components["schemas"]["FirebaseFirestore.Timestamp"]; }[]; error?: string; @@ -327,6 +328,35 @@ export interface components { DeleteBookingRequest: { bookingID: string; }; + CombinedUserData: { + date_of_birth: components["schemas"]["FirebaseFirestore.Timestamp"]; + does_snowboarding?: boolean; + does_racing?: boolean; + does_ski?: boolean; + /** Format: double */ + phone_number: number; + gender?: string; + emergency_contact?: string; + first_name: string; + last_name: string; + dietary_requirements: string; + /** @description **OPTIONAL** field that the user should have the choice to provide */ + ethnicity?: string; + faculty?: string; + university?: string; + student_id?: string; + university_year?: string; + /** @description For identification DO NOT RETURN to users in exposed endpoints */ + stripe_id?: string; + /** @description Firebase identifier of the user *data* based on the firestore document */ + uid: string; + /** @description Formatted UTC date string of when the account was created */ + dateJoined?: string; + /** @description The email the user uses to log in */ + email: string; + /** @description What type of account the user has */ + membership: components["schemas"]["UserAccountTypes"]; + }; AllUsersResponse: { error?: string; message?: string; diff --git a/client/src/services/Admin/AdminMutations.ts b/client/src/services/Admin/AdminMutations.ts index 856981a09..66fa22a11 100644 --- a/client/src/services/Admin/AdminMutations.ts +++ b/client/src/services/Admin/AdminMutations.ts @@ -132,3 +132,16 @@ export function useAddUserToBookingMutation() { } }) } + +export function useDeleteBookingMutation() { + return useMutation({ + mutationKey: ["delete-booking"], + retry: false, + mutationFn: AdminService.deleteBooking, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ALL_BOOKINGS_BETWEEN_RANGE_QUERY] + }) + } + }) +} diff --git a/client/src/services/Admin/AdminService.ts b/client/src/services/Admin/AdminService.ts index bae3d746b..b40362a65 100644 --- a/client/src/services/Admin/AdminService.ts +++ b/client/src/services/Admin/AdminService.ts @@ -50,7 +50,16 @@ const AdminService = { }) if (!response.ok) throw new Error(`Failed to promote ${uid}`) }, - + deleteBooking: async function (id: string) { + const { response } = await fetchClient.POST("/admin/bookings/delete", { + body: { + bookingID: id + } + }) + if (!response.ok) { + throw new Error(`Failed to delete booking with id ${id}`) + } + }, getBookingsBetweenDateRange: async function ({ startDate = Timestamp.fromDate(new Date(Date.now())), endDate = Timestamp.fromDate(new Date(Date.now())) diff --git a/server/src/middleware/__generated__/routes.ts b/server/src/middleware/__generated__/routes.ts index 1e45fcef1..345d3449a 100644 --- a/server/src/middleware/__generated__/routes.ts +++ b/server/src/middleware/__generated__/routes.ts @@ -221,7 +221,7 @@ const models: TsoaRoute.Models = { "enums": ["admin","member","guest"], }, // 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 - "CombinedUserData": { + "BookingIdandUserData": { "dataType": "refObject", "properties": { "date_of_birth": {"ref":"FirebaseFirestore.Timestamp","required":true}, @@ -244,6 +244,7 @@ const models: TsoaRoute.Models = { "dateJoined": {"dataType":"string"}, "email": {"dataType":"string","required":true}, "membership": {"ref":"UserAccountTypes","required":true}, + "bookingId": {"dataType":"string","required":true}, }, "additionalProperties": false, }, @@ -251,7 +252,7 @@ const models: TsoaRoute.Models = { "UsersByDateRangeResponse": { "dataType": "refObject", "properties": { - "data": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"users":{"dataType":"array","array":{"dataType":"refObject","ref":"CombinedUserData"},"required":true},"date":{"ref":"FirebaseFirestore.Timestamp","required":true}}}}, + "data": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"users":{"dataType":"array","array":{"dataType":"refObject","ref":"BookingIdandUserData"},"required":true},"date":{"ref":"FirebaseFirestore.Timestamp","required":true}}}}, "error": {"dataType":"string"}, }, "additionalProperties": false, @@ -314,6 +315,33 @@ 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 + "CombinedUserData": { + "dataType": "refObject", + "properties": { + "date_of_birth": {"ref":"FirebaseFirestore.Timestamp","required":true}, + "does_snowboarding": {"dataType":"boolean"}, + "does_racing": {"dataType":"boolean"}, + "does_ski": {"dataType":"boolean"}, + "phone_number": {"dataType":"double","required":true}, + "gender": {"dataType":"string"}, + "emergency_contact": {"dataType":"string","validators":{"isString":{"errorMsg":"Please enter a name"}}}, + "first_name": {"dataType":"string","required":true,"validators":{"isString":{"errorMsg":"Please enter your First Name"}}}, + "last_name": {"dataType":"string","required":true,"validators":{"isString":{"errorMsg":"Please enter your Second Name"}}}, + "dietary_requirements": {"dataType":"string","required":true,"validators":{"isString":{"errorMsg":"Please write your dietary requirements"}}}, + "ethnicity": {"dataType":"string"}, + "faculty": {"dataType":"string","validators":{"isString":{"errorMsg":"Please enter your faculty"}}}, + "university": {"dataType":"string","validators":{"isString":{"errorMsg":"Please enter your university"}}}, + "student_id": {"dataType":"string","validators":{"isString":{"errorMsg":"Please enter your student ID"}}}, + "university_year": {"dataType":"string","validators":{"isString":{"errorMsg":"Please enter your year of study"}}}, + "stripe_id": {"dataType":"string"}, + "uid": {"dataType":"string","required":true}, + "dateJoined": {"dataType":"string"}, + "email": {"dataType":"string","required":true}, + "membership": {"ref":"UserAccountTypes","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 "AllUsersResponse": { "dataType": "refObject", "properties": { diff --git a/server/src/middleware/__generated__/swagger.json b/server/src/middleware/__generated__/swagger.json index 527c1b84c..d814e5802 100644 --- a/server/src/middleware/__generated__/swagger.json +++ b/server/src/middleware/__generated__/swagger.json @@ -487,7 +487,7 @@ ], "type": "string" }, - "CombinedUserData": { + "BookingIdandUserData": { "properties": { "date_of_birth": { "$ref": "#/components/schemas/FirebaseFirestore.Timestamp" @@ -555,6 +555,9 @@ "membership": { "$ref": "#/components/schemas/UserAccountTypes", "description": "What type of account the user has" + }, + "bookingId": { + "type": "string" } }, "required": [ @@ -565,7 +568,8 @@ "dietary_requirements", "uid", "email", - "membership" + "membership", + "bookingId" ], "type": "object", "additionalProperties": false @@ -578,7 +582,7 @@ "properties": { "users": { "items": { - "$ref": "#/components/schemas/CombinedUserData" + "$ref": "#/components/schemas/BookingIdandUserData" }, "type": "array" }, @@ -723,6 +727,89 @@ "type": "object", "additionalProperties": false }, + "CombinedUserData": { + "properties": { + "date_of_birth": { + "$ref": "#/components/schemas/FirebaseFirestore.Timestamp" + }, + "does_snowboarding": { + "type": "boolean" + }, + "does_racing": { + "type": "boolean" + }, + "does_ski": { + "type": "boolean" + }, + "phone_number": { + "type": "number", + "format": "double" + }, + "gender": { + "type": "string" + }, + "emergency_contact": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "dietary_requirements": { + "type": "string" + }, + "ethnicity": { + "type": "string", + "description": "**OPTIONAL** field that the user should have the choice to provide" + }, + "faculty": { + "type": "string" + }, + "university": { + "type": "string" + }, + "student_id": { + "type": "string" + }, + "university_year": { + "type": "string" + }, + "stripe_id": { + "type": "string", + "description": "For identification DO NOT RETURN to users in exposed endpoints" + }, + "uid": { + "type": "string", + "description": "Firebase identifier of the user *data* based on the firestore document" + }, + "dateJoined": { + "type": "string", + "description": "Formatted UTC date string of when the account was created" + }, + "email": { + "type": "string", + "description": "The email the user uses to log in" + }, + "membership": { + "$ref": "#/components/schemas/UserAccountTypes", + "description": "What type of account the user has" + } + }, + "required": [ + "date_of_birth", + "phone_number", + "first_name", + "last_name", + "dietary_requirements", + "uid", + "email", + "membership" + ], + "type": "object", + "additionalProperties": false + }, "AllUsersResponse": { "properties": { "error": { diff --git a/server/src/service-layer/controllers/AdminController.ts b/server/src/service-layer/controllers/AdminController.ts index b7a3fbb7d..3be210be4 100644 --- a/server/src/service-layer/controllers/AdminController.ts +++ b/server/src/service-layer/controllers/AdminController.ts @@ -149,6 +149,7 @@ export class AdminController extends Controller { } @SuccessResponse("200", "Booking deleted successfuly") + // TODO: Refactor this to be a DELETE request @Post("/bookings/delete") public async removeBooking( @Body() requestBody: DeleteBookingRequest diff --git a/server/src/service-layer/response-models/BookingResponse.ts b/server/src/service-layer/response-models/BookingResponse.ts index 5c28b1c85..7cd34df45 100644 --- a/server/src/service-layer/response-models/BookingResponse.ts +++ b/server/src/service-layer/response-models/BookingResponse.ts @@ -1,6 +1,6 @@ import { Timestamp } from "firebase-admin/firestore" import { CommonResponse } from "./CommonResponse" -import { CombinedUserData } from "./UserResponse" +import { BookingIdandUserData } from "./UserResponse" export interface BookingSlotUpdateResponse extends CommonResponse { updatedBookingSlots?: { date: Timestamp; bookingSlotId: string }[] @@ -14,7 +14,7 @@ export interface AllUserBookingSlotsResponse extends CommonResponse { * Represents the response structure for fetching users by date range. */ export interface UsersByDateRangeResponse { - data?: Array<{ date: Timestamp; users: CombinedUserData[] }> + data?: Array<{ date: Timestamp; users: BookingIdandUserData[] }> error?: string }