From ce23346b6420a380200cb8ebab17d457630171d9 Mon Sep 17 00:00:00 2001 From: Riho Takagi Date: Tue, 8 Oct 2024 13:00:35 -0400 Subject: [PATCH 1/2] 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 4d7205977974f28d3b04e1224099173ee9a025cb Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Tue, 8 Oct 2024 15:52:43 -0400 Subject: [PATCH 2/2] 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,