From cfcb56a9d4382babafa3c9a988604437712ce617 Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Mon, 7 Oct 2024 17:24:16 -0400 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 + /> + + + + + + + + ); +} From 0961a5e42cb0db5057bb1ba3ed0b1d83e7495634 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:32:49 -0400 Subject: [PATCH 10/13] more info modal style refactor --- .../components/bookingTable/MoreInfoModal.tsx | 139 +++++++++--------- 1 file changed, 71 insertions(+), 68 deletions(-) diff --git a/booking-app/components/src/client/routes/components/bookingTable/MoreInfoModal.tsx b/booking-app/components/src/client/routes/components/bookingTable/MoreInfoModal.tsx index 301a4c33..ef548f27 100644 --- a/booking-app/components/src/client/routes/components/bookingTable/MoreInfoModal.tsx +++ b/booking-app/components/src/client/routes/components/bookingTable/MoreInfoModal.tsx @@ -16,7 +16,6 @@ import Grid from "@mui/material/Unstable_Grid2/Grid2"; import React from "react"; import { RoomDetails } from "../../booking/components/BookingSelection"; import StackedTableCell from "./StackedTableCell"; -import StatusChip from "./StatusChip"; import { formatTimeAmPm } from "../../../utils/date"; import { styled } from "@mui/system"; @@ -30,18 +29,24 @@ const modalStyle = { top: "50%", left: "50%", transform: "translate(-50%, -50%)", - width: "75vw", + height: "90vh", + width: "600px", bgcolor: "background.paper", boxShadow: 24, p: 4, padding: 4, + overflowY: "scroll", +}; + +const SectionTitle = styled(Typography)({}); +SectionTitle.defaultProps = { + variant: "subtitle1", }; const LabelCell = styled(TableCell)(({ theme }) => ({ borderRight: `1px solid ${theme.palette.custom.border}`, width: 175, verticalAlign: "top", - fontWeight: 500, })); const AlertHeader = styled(Alert)(({ theme }) => ({ @@ -83,72 +88,68 @@ export default function MoreInfoModal({ booking, closeModal }: Props) { - - Requestor - - - - NetID / Name - - - - Contact Info - - - - N-Number - {booking.nNumber} - - - Secondary Contact - {booking.secondaryName || BLANK} - - - Sponsor - - - -
-
+ Requestor + + + + NetID / Name + + + + Contact Info + + + + N-Number + {booking.nNumber} + + + Secondary Contact + {booking.secondaryName || BLANK} + + + Sponsor + + + +
- - Details - - - - Title - {booking.title} - - - Description - {booking.description} - - - Booking Type - {booking.bookingType} - - - Expected Attendance - {booking.expectedAttendance} - - - Attendee Affiliation - {booking.attendeeAffiliation} - - -
-
+ Details + + + + Title + {booking.title} + + + Description + {booking.description} + + + Booking Type + {booking.bookingType} + + + Expected Attendance + {booking.expectedAttendance} + + + Attendee Affiliation + {booking.attendeeAffiliation} + + +
- Services + Services @@ -165,7 +166,9 @@ export default function MoreInfoModal({ booking, closeModal }: Props) { ? BLANK : booking.mediaServices .split(", ") - .map((service) =>

{service.trim()}

)} + .map((service) => ( +

{service.trim()}

+ ))}

{booking.mediaServicesDetails}

From 4add66c4737bae4a60d6ed6632939073d41c0185 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:32:07 -0400 Subject: [PATCH 11/13] status history UI --- .../src/client/routes/components/Provider.tsx | 8 ++ .../src/client/routes/components/Table.tsx | 18 +-- .../components/bookingTable/MoreInfoModal.tsx | 23 +++- .../routes/hooks/useSortBookingHistory.tsx | 106 ++++++++++++++++++ booking-app/components/src/types.ts | 8 ++ 5 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 booking-app/components/src/client/routes/hooks/useSortBookingHistory.tsx diff --git a/booking-app/components/src/client/routes/components/Provider.tsx b/booking-app/components/src/client/routes/components/Provider.tsx index 5a5131c4..91960515 100644 --- a/booking-app/components/src/client/routes/components/Provider.tsx +++ b/booking-app/components/src/client/routes/components/Provider.tsx @@ -202,12 +202,20 @@ export const DatabaseProvider = ({ email: item.email, requestedAt: item.requestedAt, firstApprovedAt: item.firstApprovedAt, + firstApprovedBy: item.firstApprovedBy, finalApprovedAt: item.finalApprovedAt, + finalApprovedBy: item.finalApprovedBy, declinedAt: item.declinedAt, + declinedBy: item.declinedBy, + declineReason: item.declineReason, canceledAt: item.canceledAt, + canceledBy: item.canceledBy, checkedInAt: item.checkedInAt, + checkedInBy: item.checkedInBy, checkedOutAt: item.checkedOutAt, + checkedOutBy: item.checkedOutBy, noShowedAt: item.noShowedAt, + noShowedBy: item.noShowedBy, walkedInAt: item.walkedInAt, })); setBookingStatuses(filtered); diff --git a/booking-app/components/src/client/routes/components/Table.tsx b/booking-app/components/src/client/routes/components/Table.tsx index a4ac8153..eea569af 100644 --- a/booking-app/components/src/client/routes/components/Table.tsx +++ b/booking-app/components/src/client/routes/components/Table.tsx @@ -62,7 +62,7 @@ interface Props { className?: string; columns: React.ReactNode[]; children: React.ReactNode[]; - topRow: React.ReactNode; + topRow?: React.ReactNode; sx?: SxProps; } @@ -75,13 +75,15 @@ export default function Table({ }: Props) { return ( - - - - {topRow} - - - + {topRow && ( + + + + {topRow} + + + + )} diff --git a/booking-app/components/src/client/routes/components/bookingTable/MoreInfoModal.tsx b/booking-app/components/src/client/routes/components/bookingTable/MoreInfoModal.tsx index ef548f27..a533a8a0 100644 --- a/booking-app/components/src/client/routes/components/bookingTable/MoreInfoModal.tsx +++ b/booking-app/components/src/client/routes/components/bookingTable/MoreInfoModal.tsx @@ -6,18 +6,22 @@ import { Table, TableBody, TableCell, + TableHead, TableRow, Typography, } from "@mui/material"; +import { BookingRow, BookingStatusLabel } from "../../../../types"; -import { BookingRow } from "../../../../types"; +import { default as CustomTable } from "../Table"; import { Event } from "@mui/icons-material"; import Grid from "@mui/material/Unstable_Grid2/Grid2"; import React from "react"; import { RoomDetails } from "../../booking/components/BookingSelection"; import StackedTableCell from "./StackedTableCell"; +import StatusChip from "./StatusChip"; import { formatTimeAmPm } from "../../../utils/date"; import { styled } from "@mui/system"; +import useSortBookingHistory from "../../hooks/useSortBookingHistory"; interface Props { booking: BookingRow; @@ -38,6 +42,10 @@ const modalStyle = { overflowY: "scroll", }; +const StatusTable = styled(CustomTable)({ + width: "100%", +}); + const SectionTitle = styled(Typography)({}); SectionTitle.defaultProps = { variant: "subtitle1", @@ -60,6 +68,15 @@ const AlertHeader = styled(Alert)(({ theme }) => ({ const BLANK = "None"; export default function MoreInfoModal({ booking, closeModal }: Props) { + const historyRows = useSortBookingHistory(booking); + + const historyCols = [ + Status, + User, + Date, + Note, + ]; + return ( @@ -88,6 +105,10 @@ export default function MoreInfoModal({ booking, closeModal }: Props) { + + {historyRows} + + Requestor
diff --git a/booking-app/components/src/client/routes/hooks/useSortBookingHistory.tsx b/booking-app/components/src/client/routes/hooks/useSortBookingHistory.tsx new file mode 100644 index 00000000..66433ef1 --- /dev/null +++ b/booking-app/components/src/client/routes/hooks/useSortBookingHistory.tsx @@ -0,0 +1,106 @@ +import { BookingRow, BookingStatusLabel } from "@/components/src/types"; +import { TableCell, TableRow } from "@mui/material"; +import { formatDateTable, formatTimeAmPm } from "../../utils/date"; +import { useContext, useMemo } from "react"; + +import { DatabaseContext } from "../components/Provider"; +import StatusChip from "../components/bookingTable/StatusChip"; +import { Timestamp } from "@firebase/firestore"; + +type HistoryRow = { + status: BookingStatusLabel; + user: string; + time: Timestamp; + note?: string; +}; + +export default function useSortBookingHistory(booking: BookingRow) { + const { bookingStatuses } = useContext(DatabaseContext); + const status = bookingStatuses.filter( + (row) => row.calendarEventId === booking.calendarEventId + )[0]; + + const rows = useMemo(() => { + let data: HistoryRow[] = []; + data.push({ + status: BookingStatusLabel.REQUESTED, + user: status.email, + time: status.requestedAt, + }); + + if (status.finalApprovedAt) { + data.push({ + status: BookingStatusLabel.APPROVED, + user: status.finalApprovedBy, + time: status.finalApprovedAt, + }); + } + if (status.canceledAt) { + data.push({ + status: BookingStatusLabel.CANCELED, + user: status.canceledBy, + time: status.canceledAt, + }); + } + if (status.checkedInAt) { + data.push({ + status: BookingStatusLabel.CHECKED_IN, + user: status.checkedInBy, + time: status.checkedInAt, + }); + } + if (status.checkedOutAt) { + data.push({ + status: BookingStatusLabel.CHECKED_OUT, + user: status.checkedOutBy, + time: status.checkedOutAt, + }); + } + if (status.noShowedAt) { + data.push({ + status: BookingStatusLabel.NO_SHOW, + user: status.noShowedBy, + time: status.noShowedAt, + }); + } + if (status.firstApprovedAt) { + data.push({ + status: BookingStatusLabel.PENDING, + user: status.firstApprovedBy, + time: status.firstApprovedAt, + }); + } + if (status.declinedAt) { + data.push({ + status: BookingStatusLabel.DECLINED, + user: status.declinedBy, + time: status.declinedAt, + note: status.declineReason, + }); + } + if (status.walkedInAt) { + data.push({ + status: BookingStatusLabel.WALK_IN, + user: "", + time: status.walkedInAt, + }); + } + return data; + }, [booking, status]); + + return rows + .sort((a, b) => b.time.toMillis() - a.time.toMillis()) + .map((row, i) => ( + + + + + {row.user} + + {formatDateTable(row.time.toDate())}{" "} + {formatTimeAmPm(row.time.toDate())} + + {row.note} + + )); +} diff --git a/booking-app/components/src/types.ts b/booking-app/components/src/types.ts index 247f2cd0..592c87b6 100644 --- a/booking-app/components/src/types.ts +++ b/booking-app/components/src/types.ts @@ -43,12 +43,20 @@ export type BookingStatus = { email: string; requestedAt: Timestamp; firstApprovedAt: Timestamp; + firstApprovedBy: string; finalApprovedAt: Timestamp; + finalApprovedBy: string; declinedAt: Timestamp; + declinedBy: string; + declineReason: string; canceledAt: Timestamp; + canceledBy: string; checkedInAt: Timestamp; + checkedInBy: string; checkedOutAt: Timestamp; + checkedOutBy: string; noShowedAt: Timestamp; + noShowedBy: string; walkedInAt: Timestamp; }; From 07d32dfc83e286d4b41ef53ebdac96a58cb87f3f Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:14:46 -0400 Subject: [PATCH 12/13] status history chronological order --- .../src/client/routes/hooks/useSortBookingHistory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/booking-app/components/src/client/routes/hooks/useSortBookingHistory.tsx b/booking-app/components/src/client/routes/hooks/useSortBookingHistory.tsx index 66433ef1..dd591ce6 100644 --- a/booking-app/components/src/client/routes/hooks/useSortBookingHistory.tsx +++ b/booking-app/components/src/client/routes/hooks/useSortBookingHistory.tsx @@ -89,7 +89,7 @@ export default function useSortBookingHistory(booking: BookingRow) { }, [booking, status]); return rows - .sort((a, b) => b.time.toMillis() - a.time.toMillis()) + .sort((a, b) => a.time.toMillis() - b.time.toMillis()) .map((row, i) => ( From 769d8dac4ef485e1c2c3a605793aeefe87c15a30 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Fri, 11 Oct 2024 11:52:31 -0400 Subject: [PATCH 13/13] fix modification for pre-game events --- .../app/modification/confirmation/page.tsx | 10 +++++++ .../routes/booking/components/FormInput.tsx | 26 ++++++++----------- .../booking/hooks/useCheckFormMissingData.tsx | 3 ++- .../routes/booking/hooks/useSubmitBooking.tsx | 5 ++-- .../src/client/routes/components/Provider.tsx | 3 +-- booking-app/components/src/policy.ts | 2 +- booking-app/components/src/server/admin.ts | 16 ++++++------ 7 files changed, 35 insertions(+), 30 deletions(-) create mode 100644 booking-app/app/modification/confirmation/page.tsx diff --git a/booking-app/app/modification/confirmation/page.tsx b/booking-app/app/modification/confirmation/page.tsx new file mode 100644 index 00000000..d13ea5f0 --- /dev/null +++ b/booking-app/app/modification/confirmation/page.tsx @@ -0,0 +1,10 @@ +// app/modification/confirmation/page.tsx +import BookingFormConfirmationPage from "@/components/src/client/routes/booking/formPages/BookingFormConfirmationPage"; +import { FormContextLevel } from "@/components/src/types"; +import React from "react"; + +const Role: React.FC = () => ( + +); + +export default Role; diff --git a/booking-app/components/src/client/routes/booking/components/FormInput.tsx b/booking-app/components/src/client/routes/booking/components/FormInput.tsx index b6ac590c..fc1660f5 100644 --- a/booking-app/components/src/client/routes/booking/components/FormInput.tsx +++ b/booking-app/components/src/client/routes/booking/components/FormInput.tsx @@ -111,6 +111,7 @@ export default function FormInput({ calendarEventId, formContext }: Props) { }); const isWalkIn = formContext === FormContextLevel.WALK_IN; + const isMod = formContext === FormContextLevel.MODIFICATION; // different from other switches b/c mediaServices doesn't have yes/no column in DB const [showMediaServices, setShowMediaServices] = useState(false); @@ -187,7 +188,11 @@ export default function FormInput({ calendarEventId, formContext }: Props) { // setFormData(data); registerEvent(data, isAutoApproval, calendarEventId); - router.push(isWalkIn ? "/walk-in/confirmation" : "/book/confirmation"); + if (isMod) { + router.push("/modification/confirmation"); + } else { + router.push(isWalkIn ? "/walk-in/confirmation" : "/book/confirmation"); + } }; const fullFormFields = ( @@ -252,7 +257,6 @@ export default function FormInput({ calendarEventId, formContext }: Props) { @@ -336,12 +340,6 @@ export default function FormInput({ calendarEventId, formContext }: Props) { required={false} description={

- If your event needs a specific room setup with tables and chairs, - please provide a description below. In the description please include - # of chairs, # of tables, and formation. Depending on the scope, it may - be required to hire CBS services for the room set up which comes at a cost. - The Media Commons Team will procure the services needed for the Room Setup. - Please provide the chartfield below.

} {...{ control, errors, trigger }} @@ -417,9 +415,6 @@ export default function FormInput({ calendarEventId, formContext }: Props) { label="Catering?" description={

- If the event includes catering, it is required for the reservation - holder to provide a chartfield so that the Media Commons Team can - obtain CBS Cleaning Services.

} required={false} @@ -450,10 +445,11 @@ export default function FormInput({ calendarEventId, formContext }: Props) { required={false} description={

- Only for large events with 75+ attendees, and bookings in - The Garage where the Willoughby entrance will be in use. - It is required for the reservation holder to provide a chartfield - so that the Media Commons Team can obtain Campus Safety Security Services. + Only for large events with 75+ attendees, and bookings in The + Garage where the Willoughby entrance will be in use. It is + required for the reservation holder to provide a chartfield so + that the Media Commons Team can obtain Campus Safety Security + Services.

} {...{ control, errors, trigger }} diff --git a/booking-app/components/src/client/routes/booking/hooks/useCheckFormMissingData.tsx b/booking-app/components/src/client/routes/booking/hooks/useCheckFormMissingData.tsx index 9fc25f7f..26ea6761 100644 --- a/booking-app/components/src/client/routes/booking/hooks/useCheckFormMissingData.tsx +++ b/booking-app/components/src/client/routes/booking/hooks/useCheckFormMissingData.tsx @@ -10,7 +10,8 @@ export default function useCheckFormMissingData() { const { role, department, selectedRooms, bookingCalendarInfo, formData } = useContext(BookingContext); - const hasAffiliationFields = role && department; + const hasAffiliationFields = + (role && department) || pathname.includes("/modification"); const hasRoomSelectionFields = selectedRooms && bookingCalendarInfo && diff --git a/booking-app/components/src/client/routes/booking/hooks/useSubmitBooking.tsx b/booking-app/components/src/client/routes/booking/hooks/useSubmitBooking.tsx index 6526c39d..565d9ef4 100644 --- a/booking-app/components/src/client/routes/booking/hooks/useSubmitBooking.tsx +++ b/booking-app/components/src/client/routes/booking/hooks/useSubmitBooking.tsx @@ -33,9 +33,9 @@ export default function useSubmitBooking(formContext: FormContextLevel) { const registerEvent = useCallback( async (data: Inputs, isAutoApproval: boolean, calendarEventId?: string) => { + const hasAffiliation = (role && department) || isModification; if ( - !department || - !role || + !hasAffiliation || selectedRooms.length === 0 || !bookingCalendarInfo ) { @@ -46,7 +46,6 @@ export default function useSubmitBooking(formContext: FormContextLevel) { if (isEdit && data.netId) { // block another person editing someone's booking - // TODO unless is PA or admin editing if (data.netId + "@nyu.edu" !== userEmail) { setSubmitting("error"); return; diff --git a/booking-app/components/src/client/routes/components/Provider.tsx b/booking-app/components/src/client/routes/components/Provider.tsx index 91960515..f74d128a 100644 --- a/booking-app/components/src/client/routes/components/Provider.tsx +++ b/booking-app/components/src/client/routes/components/Provider.tsx @@ -19,7 +19,6 @@ import { fetchAllFutureBookingStatus, } from "@/components/src/server/db"; -import { Liaisons } from "../admin/components/Liaisons"; import { TableNames } from "@/components/src/policy"; import { clientFetchAllDataFromCollection } from "@/lib/firebase/firebase"; import { useAuth } from "@/components/src/client/routes/components/AuthProvider"; @@ -151,7 +150,7 @@ export const DatabaseProvider = ({ email: item.email, startDate: item.startDate, endDate: item.endDate, - roomId: item.roomId, + roomId: String(item.roomId), user: item.user, room: item.room, startTime: item.startTime, diff --git a/booking-app/components/src/policy.ts b/booking-app/components/src/policy.ts index 013629c1..726222ad 100644 --- a/booking-app/components/src/policy.ts +++ b/booking-app/components/src/policy.ts @@ -1,7 +1,7 @@ /********** GOOGLE SHEETS ************/ -import { clientGetFinalApproverEmailFromDatabase } from "@/lib/firebase/firebase"; import { BookingStatusLabel } from "./types"; +import { clientGetFinalApproverEmailFromDatabase } from "@/lib/firebase/firebase"; /** ACTIVE master Google Sheet */ export const ACTIVE_SHEET_ID = "1MnWbn6bvNyMiawddtYYx0tRW4NMgvugl0I8zBO3sy68"; diff --git a/booking-app/components/src/server/admin.ts b/booking-app/components/src/server/admin.ts index 4d92d1a1..1dcbec54 100644 --- a/booking-app/components/src/server/admin.ts +++ b/booking-app/components/src/server/admin.ts @@ -1,3 +1,9 @@ +import { + BookingFormDetails, + BookingStatus, + BookingStatusLabel, + RoomSetting, +} from "../types"; import { Constraint, serverDeleteData, @@ -7,12 +13,6 @@ 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"; @@ -87,8 +87,8 @@ const serverFinalApprove = (id: string, email?: string) => { //server export const serverApproveInstantBooking = (id: string) => { - serverFirstApprove(id); - serverFinalApprove(id); + serverFirstApprove(id, ""); + serverFinalApprove(id, ""); serverApproveEvent(id); };