From 47090d6aa058c90eac4e0d91c08d37fd537151fa Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Fri, 4 Oct 2024 14:36:29 -0400 Subject: [PATCH 01/15] Import non-nyu member's events --- booking-app/app/api/syncCalendars/route.ts | 229 ++++++++++++--------- 1 file changed, 131 insertions(+), 98 deletions(-) diff --git a/booking-app/app/api/syncCalendars/route.ts b/booking-app/app/api/syncCalendars/route.ts index 224d66e8..2deb47b3 100644 --- a/booking-app/app/api/syncCalendars/route.ts +++ b/booking-app/app/api/syncCalendars/route.ts @@ -1,13 +1,21 @@ -import { Booking, MediaServices } from "@/components/src/types"; - -import { NextResponse } from "next/server"; +import { toFirebaseTimestampFromString } from "@/components/src/client/utils/serverDate"; import { TableNames } from "@/components/src/policy"; -import { Timestamp } from "@firebase/firestore"; +import { Booking, MediaServices } from "@/components/src/types"; import admin from "@/lib/firebase/server/firebaseAdmin"; import { getCalendarClient } from "@/lib/googleClient"; -import { toFirebaseTimestampFromString } from "@/components/src/client/utils/serverDate"; +import { Timestamp } from "@firebase/firestore"; +import { NextResponse } from "next/server"; const db = admin.firestore(); +const areRoomIdsSame = (roomIds1: string, roomIds2: string): boolean => { + // Trim and split the room IDs, then sort both arrays + const sortedRoomIds1 = roomIds1.split(',').map(id => id.trim()).sort(); + const sortedRoomIds2 = roomIds2.split(',').map(id => id.trim()).sort(); + + // Compare the two arrays + return sortedRoomIds1.length === sortedRoomIds2.length && + sortedRoomIds1.every((id, index) => id === sortedRoomIds2[index]); +}; const createBookingWithDefaults = ( partialBooking: Partial, ): Booking => { @@ -49,17 +57,41 @@ const createBookingWithDefaults = ( ...partialBooking, }; }; -const findNyuEmail = (event: any): string => { + +const findGuestEmail = (event: any): string => { const attendees = event.attendees || []; - const nyuEmail = attendees.find( - (attendee: any) => attendee.email && attendee.email.endsWith("@nyu.edu"), + const guestEmail = attendees.find((attendee: any) => + attendee.email && !attendee.email.endsWith('@group.calendar.google.com') ); - return nyuEmail ? nyuEmail.email : ""; + return guestEmail ? guestEmail.email : ""; }; +const findRoomIds = (event: any, resources: any[]): string => { + const attendees = event.attendees || []; + const roomIds = new Set(); + + // Add the roomId of the current resource + const currentResource = resources.find(r => r.calendarId === event.organizer.email); + if (currentResource) { + roomIds.add(currentResource.roomId); + } + + // Add other room IDs + attendees.forEach((attendee: any) => { + const resource = resources.find(r => r.calendarId === attendee.email); + if (resource) { + roomIds.add(resource.roomId); + } + }); + + // Convert to array, sort numerically, and join + return Array.from(roomIds) + .sort((a, b) => parseInt(a) - parseInt(b)) + .join(','); +}; + export async function POST(request: Request) { try { const calendar = await getCalendarClient(); - // Fetch all calendar IDs from the Resource table const resourcesSnapshot = await db.collection("resources").get(); const resources = resourcesSnapshot.docs.map(doc => ({ id: doc.id, @@ -68,110 +100,111 @@ export async function POST(request: Request) { })); let totalNewBookings = 0; + let totalUpdatedBookings = 0; let targetBookings = 0; + + const now = new Date(); + const threeMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 1, 1); + const threeMonthsLater = new Date(now.getFullYear(), now.getMonth() + 4, 0); + const timeMin = threeMonthsAgo.toISOString(); + const timeMax = threeMonthsLater.toISOString(); + for (const resource of resources) { try { - // Fetch events for each calendar let pageToken: string | undefined; - const now = new Date(); - const threeMonthsAgo = new Date(now.getFullYear(), now.getMonth(), 1); - const threeMonthsLater = new Date( - now.getFullYear(), - now.getMonth() + 4, - 0, - ); - const timeMin = threeMonthsAgo.toISOString(); - const timeMax = threeMonthsLater.toISOString(); - const events = await calendar.events.list({ - calendarId: resource.calendarId, - timeMin: timeMin, - timeMax: timeMax, - maxResults: 500, // Maximum allowed by Google Calendar API - singleEvents: true, - orderBy: "startTime", - pageToken: pageToken, - }); - - for (const event of events.data.items || []) { - const bookingRef = db - .collection("bookings") - .where("calendarEventId", "==", event.id); - const bookingSnapshot = await bookingRef.get(); - const nyuEmail = findNyuEmail(event); - if (bookingSnapshot.empty && nyuEmail) { - targetBookings++; - console.log("calendarEventId", event.id); - console.log("title", event.summary); - } + do { + const events = await calendar.events.list({ + calendarId: resource.calendarId, + timeMin: timeMin, + timeMax: timeMax, + maxResults: 500, + singleEvents: true, + orderBy: "startTime", + pageToken: pageToken, + }); - if (bookingSnapshot.empty && nyuEmail) { - // Create a new booking - const calendarEventId = event.id; - const newBooking = createBookingWithDefaults({ - title: event.summary || "", - description: event.description || "", - email: nyuEmail || "", - startDate: toFirebaseTimestampFromString( - event.start?.dateTime, - ) as Timestamp, - endDate: toFirebaseTimestampFromString( - event.end?.dateTime, - ) as Timestamp, - calendarEventId: calendarEventId || "", - equipmentCheckedOut: true, - roomId: resource.roomId, - mediaServices: MediaServices.CHECKOUT_EQUIPMENT, - }); - console.log("newBooking", newBooking); - const bookingDocRef = await db - .collection(TableNames.BOOKING) - .add(newBooking); - - console.log(`New Booking created with ID: ${bookingDocRef.id}`); - - const newBookingStatus = { - calendarEventId: calendarEventId, - email: nyuEmail, - requestedAt: admin.firestore.FieldValue.serverTimestamp(), - firstApprovedAt: admin.firestore.FieldValue.serverTimestamp(), - finalApprovedAt: admin.firestore.FieldValue.serverTimestamp(), - }; - console.log("newBookingStatus", newBookingStatus); - const statusDocRef = await db - .collection(TableNames.BOOKING_STATUS) - .add(newBookingStatus); - console.log( - `New BookingStatus created with ID: ${statusDocRef.id}`, - ); - - totalNewBookings++; + for (const event of events.data.items || []) { + const bookingRef = db + .collection("bookings") + .where("calendarEventId", "==", event.id); + const bookingSnapshot = await bookingRef.get(); + const guestEmail = findGuestEmail(event); + const roomIds = findRoomIds(event, resources); + + if (bookingSnapshot.empty && guestEmail) { + targetBookings++; + console.log("calendarEventId", event.id); + console.log("title", event.summary); + + const calendarEventId = event.id; + const newBooking = createBookingWithDefaults({ + title: event.summary || "", + description: event.description || "", + email: guestEmail, + startDate: toFirebaseTimestampFromString(event.start?.dateTime) as Timestamp, + endDate: toFirebaseTimestampFromString(event.end?.dateTime) as Timestamp, + calendarEventId: calendarEventId || "", + roomId: roomIds, + mediaServices: MediaServices.CHECKOUT_EQUIPMENT, + }); + + console.log("newBooking", newBooking); + const bookingDocRef = await db + .collection(TableNames.BOOKING) + .add(newBooking); + + console.log(`New Booking created with ID: ${bookingDocRef.id}`); + + const newBookingStatus = { + calendarEventId: calendarEventId, + email: guestEmail, + requestedAt: admin.firestore.FieldValue.serverTimestamp(), + firstApprovedAt: admin.firestore.FieldValue.serverTimestamp(), + finalApprovedAt: admin.firestore.FieldValue.serverTimestamp(), + }; + console.log("newBookingStatus", newBookingStatus); + const statusDocRef = await db + .collection(TableNames.BOOKING_STATUS) + .add(newBookingStatus); + console.log(`New BookingStatus created with ID: ${statusDocRef.id}`); + + totalNewBookings++; + } else if (!bookingSnapshot.empty) { + // Update existing booking if roomIds contains multiple rooms and is different from the existing roomId + const existingBooking = bookingSnapshot.docs[0]; + const existingData = existingBooking.data() as Booking; + + if (roomIds.includes(',') && !areRoomIdsSame(roomIds, existingData.roomId)) { + console.log("roomIds",roomIds) + console.log("existingData.roomId",existingData.roomId) + await existingBooking.ref.update({ roomId: roomIds }); + console.log(`Updated roomId for Booking ID: ${existingBooking.id}`); + totalUpdatedBookings++; + } + } } pageToken = events.data.nextPageToken; - } - while (pageToken); - console.log("targetBookings", targetBookings); + } while (pageToken); } catch (error) { - console.error( - `Error processing calendar ${resource.calendarId}:`, - error, - ); - // Continue with the next calendar + console.error(`Error processing calendar ${resource.calendarId}:`, error); } } + console.log("targetBookings", targetBookings); + console.log("totalNewBookings", totalNewBookings); + console.log("totalUpdatedBookings", totalUpdatedBookings); + return NextResponse.json( - { - message: `${totalNewBookings} new bookings have been synchronized.`, + { + message: `${totalNewBookings} new bookings have been synchronized. ${totalUpdatedBookings} existing bookings have been updated with multiple rooms.` }, - { status: 200 }, + { status: 200 } ); } catch (error) { console.error("Error syncing calendars:", error); return NextResponse.json( - { - error: "An error occurred while syncing calendars.", - }, - { status: 500 }, + { error: "An error occurred while syncing calendars." }, + { status: 500 } ); } -} +} \ No newline at end of file From 13699b2c3e05ee3f3066638b5b0490cbd10ad5c1 Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Fri, 4 Oct 2024 14:41:11 -0400 Subject: [PATCH 02/15] Delete logs --- booking-app/app/api/safety_training_users/route.ts | 1 - booking-app/components/src/client/utils/serverDate.ts | 6 ------ booking-app/lib/googleClient.ts | 7 ------- 3 files changed, 14 deletions(-) diff --git a/booking-app/app/api/safety_training_users/route.ts b/booking-app/app/api/safety_training_users/route.ts index 834e1b08..9eef0051 100644 --- a/booking-app/app/api/safety_training_users/route.ts +++ b/booking-app/app/api/safety_training_users/route.ts @@ -33,7 +33,6 @@ export async function GET(request: NextRequest) { range: range, fields: "values", }); - console.log("emails", response.data.values); const logEntry = { logName: process.env.NEXT_PUBLIC_GCP_LOG_NAME + "/safety-training", diff --git a/booking-app/components/src/client/utils/serverDate.ts b/booking-app/components/src/client/utils/serverDate.ts index 9e7e1072..a6509489 100644 --- a/booking-app/components/src/client/utils/serverDate.ts +++ b/booking-app/components/src/client/utils/serverDate.ts @@ -30,12 +30,6 @@ export const serverFormatDate = ( timeZone, }); - console.log("Input:", input); - console.log("Parsed Date:", date.toISOString()); - console.log("Zoned Date:", zonedDate.toString()); - console.log("Formatted Result:", formattedResult); - console.log("Timezone:", timeZone); - return formattedResult; } catch (error) { console.error("Error formatting date:", error, "Input:", input); diff --git a/booking-app/lib/googleClient.ts b/booking-app/lib/googleClient.ts index 2da94dcd..bababfa5 100644 --- a/booking-app/lib/googleClient.ts +++ b/booking-app/lib/googleClient.ts @@ -19,23 +19,16 @@ const refreshAccessTokenIfNeeded = async (oauth2Client) => { try { const { credentials } = await oauth2Client.refreshAccessToken(); oauth2Client.setCredentials(credentials); - console.log("Access token refreshed successfully"); - console.log( - "New token expiry:", - new Date(oauth2Client.credentials.expiry_date) - ); } catch (error) { console.error("Error refreshing access token:", error); throw error; } } else { - console.log("Using existing access token"); } }; const getAuthenticatedClient = async () => { if (!cachedOAuth2Client) { - console.log("Creating new OAuth2 client"); cachedOAuth2Client = createOAuth2Client(); cachedOAuth2Client.setCredentials({ refresh_token: process.env.GOOGLE_REFRESH_TOKEN, From 717ce88222c417caaf5ceb0717e623f164a81443 Mon Sep 17 00:00:00 2001 From: Kaibanda Date: Fri, 4 Oct 2024 17:48:42 -0400 Subject: [PATCH 03/15] Update Logic For Cancel Log Out --- .../components/src/client/routes/components/ConfirmDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/booking-app/components/src/client/routes/components/ConfirmDialog.tsx b/booking-app/components/src/client/routes/components/ConfirmDialog.tsx index 2440dd0c..d506fb42 100644 --- a/booking-app/components/src/client/routes/components/ConfirmDialog.tsx +++ b/booking-app/components/src/client/routes/components/ConfirmDialog.tsx @@ -29,7 +29,7 @@ export default function ConfirmDialog(props: Props) { const handleClose = (result: boolean) => { setOpen(false); - callback(result); + if (result) callback(result); }; return ( From e27d963f215389fe8dbe8186bfe37f01234f1f23 Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Mon, 7 Oct 2024 15:19:01 -0400 Subject: [PATCH 04/15] Add logs for debugging --- booking-app/app/api/syncCalendars/route.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/booking-app/app/api/syncCalendars/route.ts b/booking-app/app/api/syncCalendars/route.ts index 2deb47b3..b04ff557 100644 --- a/booking-app/app/api/syncCalendars/route.ts +++ b/booking-app/app/api/syncCalendars/route.ts @@ -173,10 +173,9 @@ export async function POST(request: Request) { // Update existing booking if roomIds contains multiple rooms and is different from the existing roomId const existingBooking = bookingSnapshot.docs[0]; const existingData = existingBooking.data() as Booking; - + console.log("roomIds",roomIds) + console.log("existingData.roomId",existingData.roomId) if (roomIds.includes(',') && !areRoomIdsSame(roomIds, existingData.roomId)) { - console.log("roomIds",roomIds) - console.log("existingData.roomId",existingData.roomId) await existingBooking.ref.update({ roomId: roomIds }); console.log(`Updated roomId for Booking ID: ${existingBooking.id}`); totalUpdatedBookings++; From 105e540d62f8522e0f4d6c1d82661778921f89c8 Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Mon, 7 Oct 2024 15:38:13 -0400 Subject: [PATCH 05/15] Fix finding rooom logic --- booking-app/app/api/syncCalendars/route.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/booking-app/app/api/syncCalendars/route.ts b/booking-app/app/api/syncCalendars/route.ts index b04ff557..1f6b71ce 100644 --- a/booking-app/app/api/syncCalendars/route.ts +++ b/booking-app/app/api/syncCalendars/route.ts @@ -8,13 +8,21 @@ import { NextResponse } from "next/server"; const db = admin.firestore(); const areRoomIdsSame = (roomIds1: string, roomIds2: string): boolean => { - // Trim and split the room IDs, then sort both arrays - const sortedRoomIds1 = roomIds1.split(',').map(id => id.trim()).sort(); - const sortedRoomIds2 = roomIds2.split(',').map(id => id.trim()).sort(); + const toArray = (ids: string): string[] => { + return ids.includes(',') ? ids.split(',').map(id => id.trim()) : [ids.trim()]; + }; + + const sortedRoomIds1 = toArray(roomIds1).sort(); + const sortedRoomIds2 = toArray(roomIds2).sort(); - // Compare the two arrays - return sortedRoomIds1.length === sortedRoomIds2.length && - sortedRoomIds1.every((id, index) => id === sortedRoomIds2[index]); + console.log('Comparing room IDs:', { ids1: sortedRoomIds1, ids2: sortedRoomIds2 }); + + const areEqual = sortedRoomIds1.length === sortedRoomIds2.length && + sortedRoomIds1.every((id, index) => id === sortedRoomIds2[index]); + + console.log('Comparison result:', areEqual); + + return areEqual; }; const createBookingWithDefaults = ( partialBooking: Partial, From 65724af2bbbc4d70536f628308b8bfba142f0e6e Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Mon, 7 Oct 2024 15:54:00 -0400 Subject: [PATCH 06/15] Converting an Number to String --- booking-app/app/api/syncCalendars/route.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/booking-app/app/api/syncCalendars/route.ts b/booking-app/app/api/syncCalendars/route.ts index 1f6b71ce..a9517ab0 100644 --- a/booking-app/app/api/syncCalendars/route.ts +++ b/booking-app/app/api/syncCalendars/route.ts @@ -8,12 +8,13 @@ import { NextResponse } from "next/server"; const db = admin.firestore(); const areRoomIdsSame = (roomIds1: string, roomIds2: string): boolean => { + const toArray = (ids: string): string[] => { return ids.includes(',') ? ids.split(',').map(id => id.trim()) : [ids.trim()]; }; - const sortedRoomIds1 = toArray(roomIds1).sort(); - const sortedRoomIds2 = toArray(roomIds2).sort(); + const sortedRoomIds1 = toArray(String(roomIds1)).sort(); + const sortedRoomIds2 = toArray(String(roomIds2)).sort(); console.log('Comparing room IDs:', { ids1: sortedRoomIds1, ids2: sortedRoomIds2 }); From cfcb56a9d4382babafa3c9a988604437712ce617 Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Mon, 7 Oct 2024 17:24:16 -0400 Subject: [PATCH 07/15] Record who declined a request and why --- booking-app/app/api/approve/route.ts | 5 +-- booking-app/app/decline/page.tsx | 42 +++++++++++++++------ booking-app/components/src/server/db.ts | 50 +++++++++++++++---------- 3 files changed, 64 insertions(+), 33 deletions(-) diff --git a/booking-app/app/api/approve/route.ts b/booking-app/app/api/approve/route.ts index 297b6d3f..73204525 100644 --- a/booking-app/app/api/approve/route.ts +++ b/booking-app/app/api/approve/route.ts @@ -3,11 +3,10 @@ import { NextRequest, NextResponse } from "next/server"; import { serverApproveBooking } from "@/components/src/server/admin"; export async function POST(req: NextRequest) { - const { id } = await req.json(); + const { id, email } = await req.json(); try { - console.log("id", id); - await serverApproveBooking(id); + await serverApproveBooking(id, email); return NextResponse.json( { message: "Approved successfully" }, { status: 200 }, diff --git a/booking-app/app/decline/page.tsx b/booking-app/app/decline/page.tsx index 957715e0..a2e17f85 100644 --- a/booking-app/app/decline/page.tsx +++ b/booking-app/app/decline/page.tsx @@ -1,10 +1,12 @@ "use client"; - -import React, { Suspense, useState } from "react"; - -import { Button } from "@mui/material"; -import { useSearchParams } from "next/navigation"; +import { + DatabaseContext, + DatabaseProvider, +} from "@/components/src/client/routes/components/Provider"; import { decline } from "@/components/src/server/db"; +import { Button, TextField } from "@mui/material"; +import { useSearchParams } from "next/navigation"; +import React, { Suspense, useContext, useState } from "react"; const DeclinePageContent: React.FC = () => { const searchParams = useSearchParams(); @@ -12,19 +14,24 @@ const DeclinePageContent: React.FC = () => { const [loading, setLoading] = useState(false); const [declined, setDeclined] = useState(false); const [error, setError] = useState(null); + const [reason, setReason] = useState(""); + const { userEmail } = useContext(DatabaseContext); const handleDecline = async () => { - if (paramCalendarEventId) { + if (paramCalendarEventId && reason.trim()) { setLoading(true); setError(null); try { - await decline(paramCalendarEventId); + await decline(paramCalendarEventId, userEmail, reason); setDeclined(true); } catch (err) { setError("Failed to decline booking."); + console.log(err); } finally { setLoading(false); } + } else { + setError("Please provide a reason for declining."); } }; @@ -34,9 +41,20 @@ const DeclinePageContent: React.FC = () => { {paramCalendarEventId ? (

Event ID: {paramCalendarEventId}

+ setReason(e.target.value)} + style={{ marginBottom: 16 }} + required + />
}> - - + + Loading...}> + + + ); export default DeclinePage; diff --git a/booking-app/components/src/server/db.ts b/booking-app/components/src/server/db.ts index ac299854..eb095f1b 100644 --- a/booking-app/components/src/server/db.ts +++ b/booking-app/components/src/server/db.ts @@ -1,20 +1,20 @@ import { - BookingFormDetails, - BookingStatusLabel, - PolicySettings, -} from "../types"; + clientFetchAllDataFromCollection, + clientGetDataByCalendarEventId, + clientUpdateDataInFirestore, +} from "@/lib/firebase/firebase"; +import { Timestamp, where } from "@firebase/firestore"; import { TableNames, clientGetFinalApproverEmail, getCancelCcEmail, } from "../policy"; -import { Timestamp, where } from "@firebase/firestore"; -import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; import { - clientFetchAllDataFromCollection, - clientGetDataByCalendarEventId, - clientUpdateDataInFirestore, -} from "@/lib/firebase/firebase"; + BookingFormDetails, + BookingStatusLabel, + PolicySettings, +} from "../types"; +import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; import { clientUpdateDataByCalendarEventId } from "@/lib/firebase/client/clientDb"; import { roundTimeUp } from "../client/utils/date"; @@ -69,9 +69,10 @@ export const getOldSafetyTrainingEmails = () => { //return combinedValues; }; -export const decline = async (id: string) => { +export const decline = async (id: string, email: string, reason?: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { declinedAt: Timestamp.now(), + declinedBy: email, }); const doc = await clientGetDataByCalendarEventId( @@ -80,8 +81,15 @@ export const decline = async (id: string) => { ); //@ts-ignore const guestEmail = doc ? doc.email : null; - const headerMessage = - "Your reservation request for Media Commons has been declined. For detailed reasons regarding this decision, please contact us at mediacommons.reservations@nyu.edu."; + let headerMessage = + "Your reservation request for Media Commons has been declined."; + + if (reason) { + headerMessage += ` Reason: ${reason}`; + } else { + headerMessage += + " For detailed reasons regarding this decision, please contact us at mediacommons.reservations@nyu.edu."; + } clientSendBookingDetailEmail( id, guestEmail, @@ -102,9 +110,10 @@ export const decline = async (id: string) => { } ); }; -export const cancel = async (id: string) => { +export const cancel = async (id: string, email: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { canceledAt: Timestamp.now(), + canceledBy: email, }); const doc = await clientGetDataByCalendarEventId( TableNames.BOOKING_STATUS, @@ -156,9 +165,10 @@ export const updatePolicySettingData = async (updatedData: object) => { console.log("No policy settings docs found"); } }; -export const checkin = async (id: string) => { +export const checkin = async (id: string, email: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { checkedInAt: Timestamp.now(), + checkedInBy: email, }); const doc = await clientGetDataByCalendarEventId( TableNames.BOOKING_STATUS, @@ -190,10 +200,11 @@ export const checkin = async (id: string) => { ); }; -export const checkOut = async (id: string) => { +export const checkOut = async (id: string, email: string) => { const checkoutDate = roundTimeUp(); clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { checkedOutAt: Timestamp.now(), + checkedOutBy: email, }); clientUpdateDataByCalendarEventId(TableNames.BOOKING, id, { endDate: Timestamp.fromDate(checkoutDate), @@ -234,9 +245,10 @@ export const checkOut = async (id: string) => { ); }; -export const noShow = async (id: string) => { +export const noShow = async (id: string, email: email) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { noShowedAt: Timestamp.now(), + noShowedBy: email, }); const doc = await clientGetDataByCalendarEventId( TableNames.BOOKING_STATUS, @@ -324,12 +336,12 @@ export const clientSendConfirmationEmail = async ( clientSendBookingDetailEmail(calendarEventId, email, headerMessage, status); }; -export const clientApproveBooking = async (id: string) => { +export const clientApproveBooking = async (id: string, email: string) => { const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/approve`, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ id: id }), + body: JSON.stringify({ id: id, email: email }), }); }; From 947b28b4825349429353b3920e4d6a0a2ff98b45 Mon Sep 17 00:00:00 2001 From: Riho Takagi Date: Tue, 8 Oct 2024 12:47:56 -0400 Subject: [PATCH 08/15] Revert "Record who declined a request and why" --- booking-app/app/api/approve/route.ts | 5 ++- booking-app/app/decline/page.tsx | 42 ++++++--------------- booking-app/components/src/server/db.ts | 50 ++++++++++--------------- 3 files changed, 33 insertions(+), 64 deletions(-) diff --git a/booking-app/app/api/approve/route.ts b/booking-app/app/api/approve/route.ts index 73204525..297b6d3f 100644 --- a/booking-app/app/api/approve/route.ts +++ b/booking-app/app/api/approve/route.ts @@ -3,10 +3,11 @@ import { NextRequest, NextResponse } from "next/server"; import { serverApproveBooking } from "@/components/src/server/admin"; export async function POST(req: NextRequest) { - const { id, email } = await req.json(); + const { id } = await req.json(); try { - await serverApproveBooking(id, email); + console.log("id", id); + await serverApproveBooking(id); return NextResponse.json( { message: "Approved successfully" }, { status: 200 }, diff --git a/booking-app/app/decline/page.tsx b/booking-app/app/decline/page.tsx index a2e17f85..957715e0 100644 --- a/booking-app/app/decline/page.tsx +++ b/booking-app/app/decline/page.tsx @@ -1,12 +1,10 @@ "use client"; -import { - DatabaseContext, - DatabaseProvider, -} from "@/components/src/client/routes/components/Provider"; -import { decline } from "@/components/src/server/db"; -import { Button, TextField } from "@mui/material"; + +import React, { Suspense, useState } from "react"; + +import { Button } from "@mui/material"; import { useSearchParams } from "next/navigation"; -import React, { Suspense, useContext, useState } from "react"; +import { decline } from "@/components/src/server/db"; const DeclinePageContent: React.FC = () => { const searchParams = useSearchParams(); @@ -14,24 +12,19 @@ const DeclinePageContent: React.FC = () => { const [loading, setLoading] = useState(false); const [declined, setDeclined] = useState(false); const [error, setError] = useState(null); - const [reason, setReason] = useState(""); - const { userEmail } = useContext(DatabaseContext); const handleDecline = async () => { - if (paramCalendarEventId && reason.trim()) { + if (paramCalendarEventId) { setLoading(true); setError(null); try { - await decline(paramCalendarEventId, userEmail, reason); + await decline(paramCalendarEventId); setDeclined(true); } catch (err) { setError("Failed to decline booking."); - console.log(err); } finally { setLoading(false); } - } else { - setError("Please provide a reason for declining."); } }; @@ -41,20 +34,9 @@ const DeclinePageContent: React.FC = () => { {paramCalendarEventId ? (

Event ID: {paramCalendarEventId}

- setReason(e.target.value)} - style={{ marginBottom: 16 }} - required - />
}> - - - + Loading...}> + + ); export default DeclinePage; diff --git a/booking-app/components/src/server/db.ts b/booking-app/components/src/server/db.ts index eb095f1b..ac299854 100644 --- a/booking-app/components/src/server/db.ts +++ b/booking-app/components/src/server/db.ts @@ -1,20 +1,20 @@ import { - clientFetchAllDataFromCollection, - clientGetDataByCalendarEventId, - clientUpdateDataInFirestore, -} from "@/lib/firebase/firebase"; -import { Timestamp, where } from "@firebase/firestore"; + BookingFormDetails, + BookingStatusLabel, + PolicySettings, +} from "../types"; import { TableNames, clientGetFinalApproverEmail, getCancelCcEmail, } from "../policy"; -import { - BookingFormDetails, - BookingStatusLabel, - PolicySettings, -} from "../types"; +import { Timestamp, where } from "@firebase/firestore"; import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; +import { + clientFetchAllDataFromCollection, + clientGetDataByCalendarEventId, + clientUpdateDataInFirestore, +} from "@/lib/firebase/firebase"; import { clientUpdateDataByCalendarEventId } from "@/lib/firebase/client/clientDb"; import { roundTimeUp } from "../client/utils/date"; @@ -69,10 +69,9 @@ export const getOldSafetyTrainingEmails = () => { //return combinedValues; }; -export const decline = async (id: string, email: string, reason?: string) => { +export const decline = async (id: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { declinedAt: Timestamp.now(), - declinedBy: email, }); const doc = await clientGetDataByCalendarEventId( @@ -81,15 +80,8 @@ export const decline = async (id: string, email: string, reason?: string) => { ); //@ts-ignore const guestEmail = doc ? doc.email : null; - let headerMessage = - "Your reservation request for Media Commons has been declined."; - - if (reason) { - headerMessage += ` Reason: ${reason}`; - } else { - headerMessage += - " For detailed reasons regarding this decision, please contact us at mediacommons.reservations@nyu.edu."; - } + const headerMessage = + "Your reservation request for Media Commons has been declined. For detailed reasons regarding this decision, please contact us at mediacommons.reservations@nyu.edu."; clientSendBookingDetailEmail( id, guestEmail, @@ -110,10 +102,9 @@ export const decline = async (id: string, email: string, reason?: string) => { } ); }; -export const cancel = async (id: string, email: string) => { +export const cancel = async (id: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { canceledAt: Timestamp.now(), - canceledBy: email, }); const doc = await clientGetDataByCalendarEventId( TableNames.BOOKING_STATUS, @@ -165,10 +156,9 @@ export const updatePolicySettingData = async (updatedData: object) => { console.log("No policy settings docs found"); } }; -export const checkin = async (id: string, email: string) => { +export const checkin = async (id: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { checkedInAt: Timestamp.now(), - checkedInBy: email, }); const doc = await clientGetDataByCalendarEventId( TableNames.BOOKING_STATUS, @@ -200,11 +190,10 @@ export const checkin = async (id: string, email: string) => { ); }; -export const checkOut = async (id: string, email: string) => { +export const checkOut = async (id: string) => { const checkoutDate = roundTimeUp(); clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { checkedOutAt: Timestamp.now(), - checkedOutBy: email, }); clientUpdateDataByCalendarEventId(TableNames.BOOKING, id, { endDate: Timestamp.fromDate(checkoutDate), @@ -245,10 +234,9 @@ export const checkOut = async (id: string, email: string) => { ); }; -export const noShow = async (id: string, email: email) => { +export const noShow = async (id: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { noShowedAt: Timestamp.now(), - noShowedBy: email, }); const doc = await clientGetDataByCalendarEventId( TableNames.BOOKING_STATUS, @@ -336,12 +324,12 @@ export const clientSendConfirmationEmail = async ( clientSendBookingDetailEmail(calendarEventId, email, headerMessage, status); }; -export const clientApproveBooking = async (id: string, email: string) => { +export const clientApproveBooking = async (id: string) => { const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/approve`, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ id: id, email: email }), + body: JSON.stringify({ id: id }), }); }; From 31f35a4656bc0524382c6b91de45e46e82ff9396 Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Tue, 8 Oct 2024 12:59:15 -0400 Subject: [PATCH 09/15] Record email when someone change status --- booking-app/app/approve/page.tsx | 19 +++++++---- .../admin/components/BookingActions.tsx | 32 +++++++++---------- booking-app/components/src/server/admin.ts | 26 ++++++++------- 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/booking-app/app/approve/page.tsx b/booking-app/app/approve/page.tsx index beff1943..11d7f5ec 100644 --- a/booking-app/app/approve/page.tsx +++ b/booking-app/app/approve/page.tsx @@ -1,10 +1,14 @@ "use client"; -import React, { Suspense, useState } from "react"; +import React, { Suspense, useContext, useState } from "react"; +import { + DatabaseContext, + DatabaseProvider, +} from "@/components/src/client/routes/components/Provider"; +import { clientApproveBooking } from "@/components/src/server/db"; import { Button } from "@mui/material"; import { useSearchParams } from "next/navigation"; -import { clientApproveBooking } from "@/components/src/server/db"; const ApprovePageContent: React.FC = () => { const searchParams = useSearchParams(); @@ -12,13 +16,14 @@ const ApprovePageContent: React.FC = () => { const [loading, setLoading] = useState(false); const [approved, setApproved] = useState(false); const [error, setError] = useState(null); + const { userEmail } = useContext(DatabaseContext); const handleApprove = async () => { if (paramCalendarEventId) { setLoading(true); setError(null); try { - await clientApproveBooking(paramCalendarEventId); + await clientApproveBooking(paramCalendarEventId, userEmail); setApproved(true); } catch (err) { setError("Failed to approve booking."); @@ -55,9 +60,11 @@ const ApprovePageContent: React.FC = () => { }; const ApprovePage: React.FC = () => ( - Loading...}> - - + + Loading...}> + + + ); export default ApprovePage; diff --git a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx index 10437974..3358f92a 100644 --- a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx +++ b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx @@ -1,6 +1,3 @@ -import { BookingStatusLabel, PageContextLevel } from "../../../../types"; -import { IconButton, MenuItem, Select } from "@mui/material"; -import React, { useContext, useMemo, useState } from "react"; import { cancel, checkOut, @@ -9,16 +6,19 @@ import { decline, noShow, } from "@/components/src/server/db"; +import { IconButton, MenuItem, Select } from "@mui/material"; +import { useContext, useMemo, useState } from "react"; +import { BookingStatusLabel, PageContextLevel } from "../../../../types"; -import AlertToast from "../../components/AlertToast"; -import { BookingContext } from "../../booking/bookingProvider"; +import { Timestamp } from "@firebase/firestore"; import Check from "@mui/icons-material/Check"; +import { useRouter } from "next/navigation"; +import { BookingContext } from "../../booking/bookingProvider"; +import AlertToast from "../../components/AlertToast"; import ConfirmDialog from "../../components/ConfirmDialog"; -import { DatabaseContext } from "../../components/Provider"; import Loading from "../../components/Loading"; -import { Timestamp } from "@firebase/firestore"; +import { DatabaseContext } from "../../components/Provider"; import useExistingBooking from "../hooks/useExistingBooking"; -import { useRouter } from "next/navigation"; interface Props { calendarEventId: string; @@ -65,7 +65,7 @@ export default function BookingActions({ const [date, setDate] = useState(new Date()); const router = useRouter(); const loadExistingBookingData = useExistingBooking(); - + const { userEmail } = useContext(DatabaseContext); const reload = async () => { await Promise.all([reloadBookings(), reloadBookingStatuses()]); }; @@ -109,44 +109,44 @@ export default function BookingActions({ const actions: { [key in Actions]: ActionDefinition } = { [Actions.CANCEL]: { action: async () => { - await cancel(calendarEventId); + await cancel(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.CANCELED, confirmation: true, }, [Actions.NO_SHOW]: { action: async () => { - await noShow(calendarEventId); + await noShow(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.NO_SHOW, }, [Actions.CHECK_IN]: { action: async () => { - await checkin(calendarEventId); + await checkin(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.CHECKED_IN, }, [Actions.CHECK_OUT]: { action: async () => { - await checkOut(calendarEventId); + await checkOut(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.CHECKED_OUT, }, [Actions.FIRST_APPROVE]: { action: async () => { - await clientApproveBooking(calendarEventId); + await clientApproveBooking(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.PENDING, }, [Actions.FINAL_APPROVE]: { action: async () => { - await clientApproveBooking(calendarEventId); + await clientApproveBooking(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.APPROVED, }, [Actions.DECLINE]: { action: async () => { - await decline(calendarEventId); + await decline(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.DECLINED, confirmation: true, diff --git a/booking-app/components/src/server/admin.ts b/booking-app/components/src/server/admin.ts index ef068696..4d92d1a1 100644 --- a/booking-app/components/src/server/admin.ts +++ b/booking-app/components/src/server/admin.ts @@ -1,10 +1,3 @@ -import { - BookingFormDetails, - BookingStatus, - BookingStatusLabel, - PolicySettings, - RoomSetting, -} from "../types"; import { Constraint, serverDeleteData, @@ -14,6 +7,12 @@ import { serverUpdateInFirestore, } from "@/lib/firebase/server/adminDb"; import { TableNames, getApprovalCcEmail } from "../policy"; +import { + BookingFormDetails, + BookingStatus, + BookingStatusLabel, + RoomSetting, +} from "../types"; import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; import { Timestamp } from "firebase-admin/firestore"; @@ -72,15 +71,17 @@ export const serverDeleteDataByCalendarEventId = async ( }; // from server -const serverFirstApprove = (id: string) => { +const serverFirstApprove = (id: string, email?: string) => { serverUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { firstApprovedAt: Timestamp.now(), + firstApprovedBy: email, }); }; -const serverFinalApprove = (id: string) => { +const serverFinalApprove = (id: string, email?: string) => { serverUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { finalApprovedAt: Timestamp.now(), + finalApprovedBy: email, }); }; @@ -92,7 +93,7 @@ export const serverApproveInstantBooking = (id: string) => { }; // both first approve and second approve flows hit here -export const serverApproveBooking = async (id: string) => { +export const serverApproveBooking = async (id: string, email: string) => { const bookingStatus = await serverGetDataByCalendarEventId( TableNames.BOOKING_STATUS, id @@ -106,10 +107,11 @@ export const serverApproveBooking = async (id: string) => { // if already first approved, then this is a second approve if (firstApproveDateRange !== null) { - serverFinalApprove(id); + serverFinalApprove(id, email); await serverApproveEvent(id); } else { - serverFirstApprove(id); + console.log("email", email); + serverFirstApprove(id, email); const response = await fetch( `${process.env.NEXT_PUBLIC_BASE_URL}/api/calendarEvents`, From ce23346b6420a380200cb8ebab17d457630171d9 Mon Sep 17 00:00:00 2001 From: Riho Takagi Date: Tue, 8 Oct 2024 13:00:35 -0400 Subject: [PATCH 10/15] Revert "Revert "Record who declined a request and why"" --- booking-app/app/api/approve/route.ts | 5 +-- booking-app/app/decline/page.tsx | 42 +++++++++++++++------ booking-app/components/src/server/db.ts | 50 +++++++++++++++---------- 3 files changed, 64 insertions(+), 33 deletions(-) diff --git a/booking-app/app/api/approve/route.ts b/booking-app/app/api/approve/route.ts index 297b6d3f..73204525 100644 --- a/booking-app/app/api/approve/route.ts +++ b/booking-app/app/api/approve/route.ts @@ -3,11 +3,10 @@ import { NextRequest, NextResponse } from "next/server"; import { serverApproveBooking } from "@/components/src/server/admin"; export async function POST(req: NextRequest) { - const { id } = await req.json(); + const { id, email } = await req.json(); try { - console.log("id", id); - await serverApproveBooking(id); + await serverApproveBooking(id, email); return NextResponse.json( { message: "Approved successfully" }, { status: 200 }, diff --git a/booking-app/app/decline/page.tsx b/booking-app/app/decline/page.tsx index 957715e0..a2e17f85 100644 --- a/booking-app/app/decline/page.tsx +++ b/booking-app/app/decline/page.tsx @@ -1,10 +1,12 @@ "use client"; - -import React, { Suspense, useState } from "react"; - -import { Button } from "@mui/material"; -import { useSearchParams } from "next/navigation"; +import { + DatabaseContext, + DatabaseProvider, +} from "@/components/src/client/routes/components/Provider"; import { decline } from "@/components/src/server/db"; +import { Button, TextField } from "@mui/material"; +import { useSearchParams } from "next/navigation"; +import React, { Suspense, useContext, useState } from "react"; const DeclinePageContent: React.FC = () => { const searchParams = useSearchParams(); @@ -12,19 +14,24 @@ const DeclinePageContent: React.FC = () => { const [loading, setLoading] = useState(false); const [declined, setDeclined] = useState(false); const [error, setError] = useState(null); + const [reason, setReason] = useState(""); + const { userEmail } = useContext(DatabaseContext); const handleDecline = async () => { - if (paramCalendarEventId) { + if (paramCalendarEventId && reason.trim()) { setLoading(true); setError(null); try { - await decline(paramCalendarEventId); + await decline(paramCalendarEventId, userEmail, reason); setDeclined(true); } catch (err) { setError("Failed to decline booking."); + console.log(err); } finally { setLoading(false); } + } else { + setError("Please provide a reason for declining."); } }; @@ -34,9 +41,20 @@ const DeclinePageContent: React.FC = () => { {paramCalendarEventId ? (

Event ID: {paramCalendarEventId}

+ setReason(e.target.value)} + style={{ marginBottom: 16 }} + required + />
}> - - + + Loading...}> + + + ); export default DeclinePage; diff --git a/booking-app/components/src/server/db.ts b/booking-app/components/src/server/db.ts index ac299854..eb095f1b 100644 --- a/booking-app/components/src/server/db.ts +++ b/booking-app/components/src/server/db.ts @@ -1,20 +1,20 @@ import { - BookingFormDetails, - BookingStatusLabel, - PolicySettings, -} from "../types"; + clientFetchAllDataFromCollection, + clientGetDataByCalendarEventId, + clientUpdateDataInFirestore, +} from "@/lib/firebase/firebase"; +import { Timestamp, where } from "@firebase/firestore"; import { TableNames, clientGetFinalApproverEmail, getCancelCcEmail, } from "../policy"; -import { Timestamp, where } from "@firebase/firestore"; -import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; import { - clientFetchAllDataFromCollection, - clientGetDataByCalendarEventId, - clientUpdateDataInFirestore, -} from "@/lib/firebase/firebase"; + BookingFormDetails, + BookingStatusLabel, + PolicySettings, +} from "../types"; +import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; import { clientUpdateDataByCalendarEventId } from "@/lib/firebase/client/clientDb"; import { roundTimeUp } from "../client/utils/date"; @@ -69,9 +69,10 @@ export const getOldSafetyTrainingEmails = () => { //return combinedValues; }; -export const decline = async (id: string) => { +export const decline = async (id: string, email: string, reason?: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { declinedAt: Timestamp.now(), + declinedBy: email, }); const doc = await clientGetDataByCalendarEventId( @@ -80,8 +81,15 @@ export const decline = async (id: string) => { ); //@ts-ignore const guestEmail = doc ? doc.email : null; - const headerMessage = - "Your reservation request for Media Commons has been declined. For detailed reasons regarding this decision, please contact us at mediacommons.reservations@nyu.edu."; + let headerMessage = + "Your reservation request for Media Commons has been declined."; + + if (reason) { + headerMessage += ` Reason: ${reason}`; + } else { + headerMessage += + " For detailed reasons regarding this decision, please contact us at mediacommons.reservations@nyu.edu."; + } clientSendBookingDetailEmail( id, guestEmail, @@ -102,9 +110,10 @@ export const decline = async (id: string) => { } ); }; -export const cancel = async (id: string) => { +export const cancel = async (id: string, email: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { canceledAt: Timestamp.now(), + canceledBy: email, }); const doc = await clientGetDataByCalendarEventId( TableNames.BOOKING_STATUS, @@ -156,9 +165,10 @@ export const updatePolicySettingData = async (updatedData: object) => { console.log("No policy settings docs found"); } }; -export const checkin = async (id: string) => { +export const checkin = async (id: string, email: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { checkedInAt: Timestamp.now(), + checkedInBy: email, }); const doc = await clientGetDataByCalendarEventId( TableNames.BOOKING_STATUS, @@ -190,10 +200,11 @@ export const checkin = async (id: string) => { ); }; -export const checkOut = async (id: string) => { +export const checkOut = async (id: string, email: string) => { const checkoutDate = roundTimeUp(); clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { checkedOutAt: Timestamp.now(), + checkedOutBy: email, }); clientUpdateDataByCalendarEventId(TableNames.BOOKING, id, { endDate: Timestamp.fromDate(checkoutDate), @@ -234,9 +245,10 @@ export const checkOut = async (id: string) => { ); }; -export const noShow = async (id: string) => { +export const noShow = async (id: string, email: email) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { noShowedAt: Timestamp.now(), + noShowedBy: email, }); const doc = await clientGetDataByCalendarEventId( TableNames.BOOKING_STATUS, @@ -324,12 +336,12 @@ export const clientSendConfirmationEmail = async ( clientSendBookingDetailEmail(calendarEventId, email, headerMessage, status); }; -export const clientApproveBooking = async (id: string) => { +export const clientApproveBooking = async (id: string, email: string) => { const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/approve`, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ id: id }), + body: JSON.stringify({ id: id, email: email }), }); }; From ad8b09bdb7206cff8416fef7a1a17557156fb05c Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:13:13 -0400 Subject: [PATCH 11/15] update status tooltip language --- .../components/bookingTable/StatusChip.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/booking-app/components/src/client/routes/components/bookingTable/StatusChip.tsx b/booking-app/components/src/client/routes/components/bookingTable/StatusChip.tsx index fcd66a03..ec185dd4 100644 --- a/booking-app/components/src/client/routes/components/bookingTable/StatusChip.tsx +++ b/booking-app/components/src/client/routes/components/bookingTable/StatusChip.tsx @@ -108,25 +108,25 @@ export default function StatusChip({ const tooltipText = useMemo(() => { switch (status) { case BookingStatusLabel.APPROVED: - return "Your request has been fully approved!"; + return "Your request has been approved!"; case BookingStatusLabel.CANCELED: - return "You have canceled your request"; + return "Your request has been canceled."; case BookingStatusLabel.CHECKED_IN: - return "Your reservation has begun, thank you for checking in at the front desk"; + return "Your reservation has begun, thank you for checking in at the front desk."; case BookingStatusLabel.CHECKED_OUT: - return "Your reservation has ended"; + return "Your reservation has ended."; case BookingStatusLabel.NO_SHOW: - return "You did not show up in time for your reservation. Your booking has been forfeited"; + return "Your reservation has been cancelled as you did not check in on time."; case BookingStatusLabel.PENDING: - return "Your request has been partially approved, still pending final approval"; + return "Your request has been partially approved, still pending final approval."; case BookingStatusLabel.DECLINED: - return "Your request has been declined"; + return "Your request has been declined."; case BookingStatusLabel.REQUESTED: - return "Your request has been received and is pending approval"; + return "Your request has been received and is pending approval."; case BookingStatusLabel.UNKNOWN: - return "Unable to determine the status of this request"; + return "Unable to determine the status of this request."; case BookingStatusLabel.WALK_IN: - return "This request was booked as a walk-in"; + return "This request has been booked as a walk-in session."; } }, [status]); From 4d7205977974f28d3b04e1224099173ee9a025cb Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Tue, 8 Oct 2024 15:52:43 -0400 Subject: [PATCH 12/15] Record emaile who changes status --- booking-app/app/approve/page.tsx | 19 +++++++---- .../admin/components/BookingActions.tsx | 32 +++++++++---------- booking-app/components/src/server/admin.ts | 26 ++++++++------- booking-app/components/src/server/db.ts | 2 +- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/booking-app/app/approve/page.tsx b/booking-app/app/approve/page.tsx index beff1943..11d7f5ec 100644 --- a/booking-app/app/approve/page.tsx +++ b/booking-app/app/approve/page.tsx @@ -1,10 +1,14 @@ "use client"; -import React, { Suspense, useState } from "react"; +import React, { Suspense, useContext, useState } from "react"; +import { + DatabaseContext, + DatabaseProvider, +} from "@/components/src/client/routes/components/Provider"; +import { clientApproveBooking } from "@/components/src/server/db"; import { Button } from "@mui/material"; import { useSearchParams } from "next/navigation"; -import { clientApproveBooking } from "@/components/src/server/db"; const ApprovePageContent: React.FC = () => { const searchParams = useSearchParams(); @@ -12,13 +16,14 @@ const ApprovePageContent: React.FC = () => { const [loading, setLoading] = useState(false); const [approved, setApproved] = useState(false); const [error, setError] = useState(null); + const { userEmail } = useContext(DatabaseContext); const handleApprove = async () => { if (paramCalendarEventId) { setLoading(true); setError(null); try { - await clientApproveBooking(paramCalendarEventId); + await clientApproveBooking(paramCalendarEventId, userEmail); setApproved(true); } catch (err) { setError("Failed to approve booking."); @@ -55,9 +60,11 @@ const ApprovePageContent: React.FC = () => { }; const ApprovePage: React.FC = () => ( - Loading...}> - - + + Loading...}> + + + ); export default ApprovePage; diff --git a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx index 10437974..3358f92a 100644 --- a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx +++ b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx @@ -1,6 +1,3 @@ -import { BookingStatusLabel, PageContextLevel } from "../../../../types"; -import { IconButton, MenuItem, Select } from "@mui/material"; -import React, { useContext, useMemo, useState } from "react"; import { cancel, checkOut, @@ -9,16 +6,19 @@ import { decline, noShow, } from "@/components/src/server/db"; +import { IconButton, MenuItem, Select } from "@mui/material"; +import { useContext, useMemo, useState } from "react"; +import { BookingStatusLabel, PageContextLevel } from "../../../../types"; -import AlertToast from "../../components/AlertToast"; -import { BookingContext } from "../../booking/bookingProvider"; +import { Timestamp } from "@firebase/firestore"; import Check from "@mui/icons-material/Check"; +import { useRouter } from "next/navigation"; +import { BookingContext } from "../../booking/bookingProvider"; +import AlertToast from "../../components/AlertToast"; import ConfirmDialog from "../../components/ConfirmDialog"; -import { DatabaseContext } from "../../components/Provider"; import Loading from "../../components/Loading"; -import { Timestamp } from "@firebase/firestore"; +import { DatabaseContext } from "../../components/Provider"; import useExistingBooking from "../hooks/useExistingBooking"; -import { useRouter } from "next/navigation"; interface Props { calendarEventId: string; @@ -65,7 +65,7 @@ export default function BookingActions({ const [date, setDate] = useState(new Date()); const router = useRouter(); const loadExistingBookingData = useExistingBooking(); - + const { userEmail } = useContext(DatabaseContext); const reload = async () => { await Promise.all([reloadBookings(), reloadBookingStatuses()]); }; @@ -109,44 +109,44 @@ export default function BookingActions({ const actions: { [key in Actions]: ActionDefinition } = { [Actions.CANCEL]: { action: async () => { - await cancel(calendarEventId); + await cancel(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.CANCELED, confirmation: true, }, [Actions.NO_SHOW]: { action: async () => { - await noShow(calendarEventId); + await noShow(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.NO_SHOW, }, [Actions.CHECK_IN]: { action: async () => { - await checkin(calendarEventId); + await checkin(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.CHECKED_IN, }, [Actions.CHECK_OUT]: { action: async () => { - await checkOut(calendarEventId); + await checkOut(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.CHECKED_OUT, }, [Actions.FIRST_APPROVE]: { action: async () => { - await clientApproveBooking(calendarEventId); + await clientApproveBooking(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.PENDING, }, [Actions.FINAL_APPROVE]: { action: async () => { - await clientApproveBooking(calendarEventId); + await clientApproveBooking(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.APPROVED, }, [Actions.DECLINE]: { action: async () => { - await decline(calendarEventId); + await decline(calendarEventId, userEmail); }, optimisticNextStatus: BookingStatusLabel.DECLINED, confirmation: true, diff --git a/booking-app/components/src/server/admin.ts b/booking-app/components/src/server/admin.ts index ef068696..4d92d1a1 100644 --- a/booking-app/components/src/server/admin.ts +++ b/booking-app/components/src/server/admin.ts @@ -1,10 +1,3 @@ -import { - BookingFormDetails, - BookingStatus, - BookingStatusLabel, - PolicySettings, - RoomSetting, -} from "../types"; import { Constraint, serverDeleteData, @@ -14,6 +7,12 @@ import { serverUpdateInFirestore, } from "@/lib/firebase/server/adminDb"; import { TableNames, getApprovalCcEmail } from "../policy"; +import { + BookingFormDetails, + BookingStatus, + BookingStatusLabel, + RoomSetting, +} from "../types"; import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; import { Timestamp } from "firebase-admin/firestore"; @@ -72,15 +71,17 @@ export const serverDeleteDataByCalendarEventId = async ( }; // from server -const serverFirstApprove = (id: string) => { +const serverFirstApprove = (id: string, email?: string) => { serverUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { firstApprovedAt: Timestamp.now(), + firstApprovedBy: email, }); }; -const serverFinalApprove = (id: string) => { +const serverFinalApprove = (id: string, email?: string) => { serverUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { finalApprovedAt: Timestamp.now(), + finalApprovedBy: email, }); }; @@ -92,7 +93,7 @@ export const serverApproveInstantBooking = (id: string) => { }; // both first approve and second approve flows hit here -export const serverApproveBooking = async (id: string) => { +export const serverApproveBooking = async (id: string, email: string) => { const bookingStatus = await serverGetDataByCalendarEventId( TableNames.BOOKING_STATUS, id @@ -106,10 +107,11 @@ export const serverApproveBooking = async (id: string) => { // if already first approved, then this is a second approve if (firstApproveDateRange !== null) { - serverFinalApprove(id); + serverFinalApprove(id, email); await serverApproveEvent(id); } else { - serverFirstApprove(id); + console.log("email", email); + serverFirstApprove(id, email); const response = await fetch( `${process.env.NEXT_PUBLIC_BASE_URL}/api/calendarEvents`, diff --git a/booking-app/components/src/server/db.ts b/booking-app/components/src/server/db.ts index eb095f1b..966c3374 100644 --- a/booking-app/components/src/server/db.ts +++ b/booking-app/components/src/server/db.ts @@ -245,7 +245,7 @@ export const checkOut = async (id: string, email: string) => { ); }; -export const noShow = async (id: string, email: email) => { +export const noShow = async (id: string, email: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { noShowedAt: Timestamp.now(), noShowedBy: email, From 9afe8ef59961a4c61d6eedd3b4ed9098e9468e6c Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:14:09 -0400 Subject: [PATCH 13/15] refactor booking actions component --- .../admin/components/BookingActions.tsx | 208 ++---------------- .../routes/admin/hooks/useBookingActions.tsx | 202 +++++++++++++++++ 2 files changed, 221 insertions(+), 189 deletions(-) create mode 100644 booking-app/components/src/client/routes/admin/hooks/useBookingActions.tsx diff --git a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx index 10437974..012baab9 100644 --- a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx +++ b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx @@ -1,24 +1,17 @@ import { BookingStatusLabel, PageContextLevel } from "../../../../types"; import { IconButton, MenuItem, Select } from "@mui/material"; -import React, { useContext, useMemo, useState } from "react"; -import { - cancel, - checkOut, - checkin, - clientApproveBooking, - decline, - noShow, -} from "@/components/src/server/db"; +import React, { useContext, useState } from "react"; +import useBookingActions, { + ActionDefinition, + Actions, +} from "../hooks/useBookingActions"; import AlertToast from "../../components/AlertToast"; -import { BookingContext } from "../../booking/bookingProvider"; import Check from "@mui/icons-material/Check"; import ConfirmDialog from "../../components/ConfirmDialog"; import { DatabaseContext } from "../../components/Provider"; import Loading from "../../components/Loading"; import { Timestamp } from "@firebase/firestore"; -import useExistingBooking from "../hooks/useExistingBooking"; -import { useRouter } from "next/navigation"; interface Props { calendarEventId: string; @@ -28,43 +21,26 @@ interface Props { startDate: Timestamp; } -enum Actions { - CANCEL = "Cancel", - NO_SHOW = "No Show", - CHECK_IN = "Check In", - CHECK_OUT = "Check Out", - FIRST_APPROVE = "1st Approve", - FINAL_APPROVE = "Final Approve", - DECLINE = "Decline", - EDIT = "Edit", - MODIFICATION = "Modification", - PLACEHOLDER = "", -} - -type ActionDefinition = { - // TODO: Fix this type - action: () => any; - optimisticNextStatus: BookingStatusLabel; - confirmation?: boolean; -}; - -export default function BookingActions({ - status, - calendarEventId, - pageContext, - setOptimisticStatus, - startDate, -}: Props) { +export default function BookingActions(props: Props) { + const { + status, + calendarEventId, + pageContext, + setOptimisticStatus, + startDate, + } = props; const [uiLoading, setUiLoading] = useState(false); const [selectedAction, setSelectedAction] = useState( Actions.PLACEHOLDER ); const { reloadBookings, reloadBookingStatuses } = useContext(DatabaseContext); - const { reloadExistingCalendarEvents } = useContext(BookingContext); const [showError, setShowError] = useState(false); - const [date, setDate] = useState(new Date()); - const router = useRouter(); - const loadExistingBookingData = useExistingBooking(); + const { actions, updateActions, options } = useBookingActions({ + status, + calendarEventId, + pageContext, + startDate, + }); const reload = async () => { await Promise.all([reloadBookings(), reloadBookingStatuses()]); @@ -74,10 +50,6 @@ export default function BookingActions({ setShowError(true); }; - const updateActions = () => { - setDate(new Date()); - }; - const handleDialogChoice = (result: boolean) => { if (result) { const actionDetails = actions[selectedAction]; @@ -106,148 +78,6 @@ export default function BookingActions({ } }; - const actions: { [key in Actions]: ActionDefinition } = { - [Actions.CANCEL]: { - action: async () => { - await cancel(calendarEventId); - }, - optimisticNextStatus: BookingStatusLabel.CANCELED, - confirmation: true, - }, - [Actions.NO_SHOW]: { - action: async () => { - await noShow(calendarEventId); - }, - optimisticNextStatus: BookingStatusLabel.NO_SHOW, - }, - [Actions.CHECK_IN]: { - action: async () => { - await checkin(calendarEventId); - }, - optimisticNextStatus: BookingStatusLabel.CHECKED_IN, - }, - [Actions.CHECK_OUT]: { - action: async () => { - await checkOut(calendarEventId); - }, - optimisticNextStatus: BookingStatusLabel.CHECKED_OUT, - }, - [Actions.FIRST_APPROVE]: { - action: async () => { - await clientApproveBooking(calendarEventId); - }, - optimisticNextStatus: BookingStatusLabel.PENDING, - }, - [Actions.FINAL_APPROVE]: { - action: async () => { - await clientApproveBooking(calendarEventId); - }, - optimisticNextStatus: BookingStatusLabel.APPROVED, - }, - [Actions.DECLINE]: { - action: async () => { - await decline(calendarEventId); - }, - optimisticNextStatus: BookingStatusLabel.DECLINED, - confirmation: true, - }, - [Actions.EDIT]: { - action: async () => { - loadExistingBookingData(calendarEventId); - reloadExistingCalendarEvents(); - router.push("/edit/" + calendarEventId); - }, - optimisticNextStatus: status, - confirmation: false, - }, - [Actions.MODIFICATION]: { - action: async () => { - loadExistingBookingData(calendarEventId); - reloadExistingCalendarEvents(); - router.push("/modification/" + calendarEventId); - }, - optimisticNextStatus: status, - confirmation: false, - }, - // never used, just make typescript happy - [Actions.PLACEHOLDER]: { - action: async () => {}, - optimisticNextStatus: BookingStatusLabel.UNKNOWN, - }, - }; - - const userOptions = useMemo(() => { - let options = []; - if (status !== BookingStatusLabel.CANCELED) { - options.push(Actions.CANCEL); - } - if ( - status !== BookingStatusLabel.CHECKED_IN && - status !== BookingStatusLabel.NO_SHOW && - startDate.toDate() > date - ) { - options.push(Actions.EDIT); - } - return options; - }, [status]); - - const paOptions = useMemo(() => { - let options = []; - - if (status === BookingStatusLabel.APPROVED) { - options.push(Actions.CHECK_IN); - options.push(Actions.NO_SHOW); - options.push(Actions.MODIFICATION); - } else if (status === BookingStatusLabel.CHECKED_IN) { - options.push(Actions.NO_SHOW); - options.push(Actions.CHECK_OUT); - options.push(Actions.MODIFICATION); - } else if (status === BookingStatusLabel.NO_SHOW) { - options.push(Actions.CHECK_IN); - } else if (status === BookingStatusLabel.WALK_IN) { - options.push(Actions.CHECK_OUT); - options.push(Actions.MODIFICATION); - } - return options; - }, [status]); - - const liaisonOptions = [Actions.FIRST_APPROVE, Actions.DECLINE]; - - const adminOptions = useMemo(() => { - if ( - status === BookingStatusLabel.CANCELED || - status === BookingStatusLabel.DECLINED || - status === BookingStatusLabel.CHECKED_OUT - ) { - return []; - } - - let options: Actions[] = []; - if (status === BookingStatusLabel.REQUESTED) { - options.push(Actions.FIRST_APPROVE); - } else if (status === BookingStatusLabel.PENDING) { - options.push(Actions.FINAL_APPROVE); - } - - options = options.concat(paOptions); - options.push(Actions.CANCEL); - options.push(Actions.DECLINE); - return options; - }, [status, paOptions, date]); - - const options = () => { - switch (pageContext) { - case PageContextLevel.USER: - return userOptions; - case PageContextLevel.PA: - return paOptions; - case PageContextLevel.LIAISON: - return liaisonOptions; - default: - return adminOptions; - } - }; - if (options().length === 0) { return <>; } diff --git a/booking-app/components/src/client/routes/admin/hooks/useBookingActions.tsx b/booking-app/components/src/client/routes/admin/hooks/useBookingActions.tsx new file mode 100644 index 00000000..0bcda633 --- /dev/null +++ b/booking-app/components/src/client/routes/admin/hooks/useBookingActions.tsx @@ -0,0 +1,202 @@ +import { BookingStatusLabel, PageContextLevel } from "@/components/src/types"; +import { + cancel, + checkOut, + checkin, + clientApproveBooking, + decline, + noShow, +} from "@/components/src/server/db"; +import { useContext, useMemo, useState } from "react"; + +import { BookingContext } from "../../booking/bookingProvider"; +import { Timestamp } from "@firebase/firestore"; +import useExistingBooking from "./useExistingBooking"; +import { useRouter } from "next/navigation"; + +export enum Actions { + CANCEL = "Cancel", + NO_SHOW = "No Show", + CHECK_IN = "Check In", + CHECK_OUT = "Check Out", + FIRST_APPROVE = "1st Approve", + FINAL_APPROVE = "Final Approve", + DECLINE = "Decline", + EDIT = "Edit", + MODIFICATION = "Modification", + PLACEHOLDER = "", +} + +export type ActionDefinition = { + // TODO: Fix this type + action: () => any; + optimisticNextStatus: BookingStatusLabel; + confirmation?: boolean; +}; + +interface Props { + calendarEventId: string; + pageContext: PageContextLevel; + status: BookingStatusLabel; + startDate: Timestamp; +} + +export default function useBookingActions({ + calendarEventId, + pageContext, + status, + startDate, +}: Props) { + const [date, setDate] = useState(new Date()); + const { reloadExistingCalendarEvents } = useContext(BookingContext); + const loadExistingBookingData = useExistingBooking(); + const router = useRouter(); + + const updateActions = () => { + setDate(new Date()); + }; + + const actions: { [key in Actions]: ActionDefinition } = { + [Actions.CANCEL]: { + action: async () => { + await cancel(calendarEventId); + }, + optimisticNextStatus: BookingStatusLabel.CANCELED, + confirmation: true, + }, + [Actions.NO_SHOW]: { + action: async () => { + await noShow(calendarEventId); + }, + optimisticNextStatus: BookingStatusLabel.NO_SHOW, + }, + [Actions.CHECK_IN]: { + action: async () => { + await checkin(calendarEventId); + }, + optimisticNextStatus: BookingStatusLabel.CHECKED_IN, + }, + [Actions.CHECK_OUT]: { + action: async () => { + await checkOut(calendarEventId); + }, + optimisticNextStatus: BookingStatusLabel.CHECKED_OUT, + }, + [Actions.FIRST_APPROVE]: { + action: async () => { + await clientApproveBooking(calendarEventId); + }, + optimisticNextStatus: BookingStatusLabel.PENDING, + }, + [Actions.FINAL_APPROVE]: { + action: async () => { + await clientApproveBooking(calendarEventId); + }, + optimisticNextStatus: BookingStatusLabel.APPROVED, + }, + [Actions.DECLINE]: { + action: async () => { + await decline(calendarEventId); + }, + optimisticNextStatus: BookingStatusLabel.DECLINED, + confirmation: true, + }, + [Actions.EDIT]: { + action: async () => { + loadExistingBookingData(calendarEventId); + reloadExistingCalendarEvents(); + router.push("/edit/" + calendarEventId); + }, + optimisticNextStatus: status, + confirmation: false, + }, + [Actions.MODIFICATION]: { + action: async () => { + loadExistingBookingData(calendarEventId); + reloadExistingCalendarEvents(); + router.push("/modification/" + calendarEventId); + }, + optimisticNextStatus: status, + confirmation: false, + }, + // never used, just make typescript happy + [Actions.PLACEHOLDER]: { + action: async () => {}, + optimisticNextStatus: BookingStatusLabel.UNKNOWN, + }, + }; + + const userOptions = useMemo(() => { + let options = []; + if (status !== BookingStatusLabel.CANCELED) { + options.push(Actions.CANCEL); + } + if ( + status !== BookingStatusLabel.CHECKED_IN && + status !== BookingStatusLabel.NO_SHOW && + startDate.toDate() > date + ) { + options.push(Actions.EDIT); + } + return options; + }, [status]); + + const paOptions = useMemo(() => { + let options = []; + + if (status === BookingStatusLabel.APPROVED) { + options.push(Actions.CHECK_IN); + options.push(Actions.NO_SHOW); + options.push(Actions.MODIFICATION); + } else if (status === BookingStatusLabel.CHECKED_IN) { + options.push(Actions.NO_SHOW); + options.push(Actions.CHECK_OUT); + options.push(Actions.MODIFICATION); + } else if (status === BookingStatusLabel.NO_SHOW) { + options.push(Actions.CHECK_IN); + } else if (status === BookingStatusLabel.WALK_IN) { + options.push(Actions.CHECK_OUT); + options.push(Actions.MODIFICATION); + } + return options; + }, [status]); + + const liaisonOptions = [Actions.FIRST_APPROVE, Actions.DECLINE]; + + const adminOptions = useMemo(() => { + if ( + status === BookingStatusLabel.CANCELED || + status === BookingStatusLabel.DECLINED || + status === BookingStatusLabel.CHECKED_OUT + ) { + return []; + } + + let options: Actions[] = []; + if (status === BookingStatusLabel.REQUESTED) { + options.push(Actions.FIRST_APPROVE); + } else if (status === BookingStatusLabel.PENDING) { + options.push(Actions.FINAL_APPROVE); + } + + options = options.concat(paOptions); + options.push(Actions.CANCEL); + options.push(Actions.DECLINE); + return options; + }, [status, paOptions, date]); + + const options = () => { + switch (pageContext) { + case PageContextLevel.USER: + return userOptions; + case PageContextLevel.PA: + return paOptions; + case PageContextLevel.LIAISON: + return liaisonOptions; + default: + return adminOptions; + } + }; + + return { actions, updateActions, options }; +} From ab017c72ecd2de775a7242a9a2d6e7e0481f865e Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Tue, 8 Oct 2024 16:16:09 -0400 Subject: [PATCH 14/15] Record decline reason --- booking-app/components/src/server/db.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/booking-app/components/src/server/db.ts b/booking-app/components/src/server/db.ts index 966c3374..1b09d610 100644 --- a/booking-app/components/src/server/db.ts +++ b/booking-app/components/src/server/db.ts @@ -73,6 +73,7 @@ export const decline = async (id: string, email: string, reason?: string) => { clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { declinedAt: Timestamp.now(), declinedBy: email, + declineReason: reason || null, }); const doc = await clientGetDataByCalendarEventId( From 302ffec629870d720f4ad49378faf723c255fd8d Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:49:23 -0400 Subject: [PATCH 15/15] decline modal in booking table --- .../admin/components/BookingActions.tsx | 71 ++++++++++++------ .../routes/admin/hooks/useBookingActions.tsx | 5 +- .../routes/components/DeclineReasonDialog.tsx | 72 +++++++++++++++++++ 3 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 booking-app/components/src/client/routes/components/DeclineReasonDialog.tsx diff --git a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx index 0bd78003..919cd817 100644 --- a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx +++ b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx @@ -1,6 +1,6 @@ import { BookingStatusLabel, PageContextLevel } from "../../../../types"; import { IconButton, MenuItem, Select } from "@mui/material"; -import React, { useContext, useState } from "react"; +import React, { useContext, useMemo, useState } from "react"; import useBookingActions, { ActionDefinition, Actions, @@ -10,6 +10,7 @@ import AlertToast from "../../components/AlertToast"; import Check from "@mui/icons-material/Check"; import ConfirmDialog from "../../components/ConfirmDialog"; import { DatabaseContext } from "../../components/Provider"; +import DeclineReasonDialog from "../../components/DeclineReasonDialog"; import Loading from "../../components/Loading"; import { Timestamp } from "@firebase/firestore"; @@ -35,12 +36,14 @@ export default function BookingActions(props: Props) { ); const { reloadBookings, reloadBookingStatuses } = useContext(DatabaseContext); const [showError, setShowError] = useState(false); + const [reason, setReason] = useState(); const { actions, updateActions, options } = useBookingActions({ status, calendarEventId, pageContext, startDate, + reason, }); const reload = async () => { @@ -79,6 +82,50 @@ export default function BookingActions(props: Props) { } }; + const onAction = useMemo(() => { + if (selectedAction === Actions.DECLINE) { + return ( + + + + + + ); + } + + if (actions[selectedAction].confirmation === true) { + return ( + + + + + + ); + } + + return ( + { + handleDialogChoice(true); + }} + > + + + ); + }, [selectedAction, reason]); + if (options().length === 0) { return <>; } @@ -119,28 +166,8 @@ export default function BookingActions(props: Props) { {uiLoading ? ( - ) : actions[selectedAction].confirmation === true ? ( - - - - - ) : ( - { - handleDialogChoice(true); - }} - > - - + onAction )} { - await decline(calendarEventId, userEmail); + await decline(calendarEventId, userEmail, reason); }, optimisticNextStatus: BookingStatusLabel.DECLINED, confirmation: true, diff --git a/booking-app/components/src/client/routes/components/DeclineReasonDialog.tsx b/booking-app/components/src/client/routes/components/DeclineReasonDialog.tsx new file mode 100644 index 00000000..f145b214 --- /dev/null +++ b/booking-app/components/src/client/routes/components/DeclineReasonDialog.tsx @@ -0,0 +1,72 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + TextField, +} from "@mui/material"; +import React, { cloneElement, useState } from "react"; + +interface Props { + value: string; + setValue: (x: string) => void; + callback: (result: boolean) => void; + children: React.ReactElement; +} + +export default function DeclineReasonDialog({ + callback, + value, + setValue, + children, +}: Props) { + const [open, setOpen] = useState(false); + + const trigger = cloneElement(children, { + onClick: () => setOpen(true), + }); + + const handleClose = (result: boolean) => { + setOpen(false); + if (result) callback(result); + }; + + return ( + <> + {trigger} + handleClose(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + + + Are you sure? This action can't be undone. + + + Please give a reason for declining this request. + + setValue(e.target.value)} + sx={{ marginTop: 2 }} + fullWidth + /> + + + + + + + + ); +}