From 7e848fc9fbe2afbe03768ee5e64b86fa777d5b2c Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:54:20 -0400 Subject: [PATCH 01/16] WEBPACK ERROR --- .../admin/components/BookingActions.tsx | 31 +++++++++++++++-- .../bookingTable/BookingTableRow.tsx | 1 + .../components/src/client/utils/date.tsx | 20 +++++++++++ .../components/src/server/calendars.ts | 34 +++++++++++++++++-- booking-app/components/src/server/db.ts | 24 ++++++++----- 5 files changed, 97 insertions(+), 13 deletions(-) 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 f10d1a61..d96078a7 100644 --- a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx +++ b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx @@ -15,6 +15,8 @@ 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 { updateByCalendarEventId } from "@/components/src/server/calendars"; import useExistingBooking from "../hooks/useExistingBooking"; import { useRouter } from "next/navigation"; @@ -23,6 +25,7 @@ interface Props { pageContext: PageContextLevel; setOptimisticStatus: (x: BookingStatusLabel) => void; status: BookingStatusLabel; + startDate: Timestamp; } enum Actions { @@ -34,6 +37,7 @@ enum Actions { FINAL_APPROVE = "Final Approve", DECLINE = "Decline", EDIT = "Edit", + MODIFICATION = "Modification", PLACEHOLDER = "", } @@ -49,6 +53,7 @@ export default function BookingActions({ calendarEventId, pageContext, setOptimisticStatus, + startDate, }: Props) { const [uiLoading, setUiLoading] = useState(false); const [selectedAction, setSelectedAction] = useState( @@ -56,6 +61,7 @@ export default function BookingActions({ ); const { reloadBookings, reloadBookingStatuses } = useContext(DatabaseContext); const [showError, setShowError] = useState(false); + const [date, setDate] = useState(new Date()); const router = useRouter(); const loadExistingBookingData = useExistingBooking(); @@ -67,6 +73,10 @@ export default function BookingActions({ setShowError(true); }; + const updateActions = () => { + setDate(new Date()); + }; + const handleDialogChoice = (result: boolean) => { if (result) { const actionDetails = actions[selectedAction]; @@ -118,6 +128,11 @@ export default function BookingActions({ [Actions.CHECK_OUT]: { action: async () => { await checkOut(calendarEventId); + await updateByCalendarEventId(calendarEventId, { + end: { + dateTime: new Date().toISOString(), + }, + }); }, optimisticNextStatus: BookingStatusLabel.CHECKED_OUT, }, @@ -148,6 +163,14 @@ export default function BookingActions({ optimisticNextStatus: status, confirmation: false, }, + [Actions.MODIFICATION]: { + action: async () => { + loadExistingBookingData(calendarEventId); + router.push("/modification/" + calendarEventId); + }, + optimisticNextStatus: status, + confirmation: false, + }, // never used, just make typescript happy [Actions.PLACEHOLDER]: { action: async () => {}, @@ -162,7 +185,8 @@ export default function BookingActions({ } if ( status !== BookingStatusLabel.CHECKED_IN && - status !== BookingStatusLabel.NO_SHOW + status !== BookingStatusLabel.NO_SHOW && + startDate.toDate() > date ) { options.push(Actions.EDIT); } @@ -175,9 +199,11 @@ export default function BookingActions({ 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); } @@ -204,7 +230,7 @@ export default function BookingActions({ options.push(Actions.CANCEL); options.push(Actions.DECLINE); return options; - }, [status, paOptions]); + }, [status, paOptions, date]); const options = () => { switch (pageContext) { @@ -234,6 +260,7 @@ export default function BookingActions({ value={selectedAction} size="small" displayEmpty + onFocus={updateActions} onChange={(e) => setSelectedAction(e.target.value as Actions)} renderValue={(selected) => { if (selected === Actions.PLACEHOLDER) { diff --git a/booking-app/components/src/client/routes/components/bookingTable/BookingTableRow.tsx b/booking-app/components/src/client/routes/components/bookingTable/BookingTableRow.tsx index 36a3a40e..b0183fd0 100644 --- a/booking-app/components/src/client/routes/components/bookingTable/BookingTableRow.tsx +++ b/booking-app/components/src/client/routes/components/bookingTable/BookingTableRow.tsx @@ -123,6 +123,7 @@ export default function BookingTableRow({ diff --git a/booking-app/components/src/client/utils/date.tsx b/booking-app/components/src/client/utils/date.tsx index f14f8f12..fa4ccef8 100644 --- a/booking-app/components/src/client/utils/date.tsx +++ b/booking-app/components/src/client/utils/date.tsx @@ -58,3 +58,23 @@ export const formatTimeAmPm = (d: Date) => { hour12: true, }); }; + +export function roundTimeUp() { + const now = new Date(); + const minutes = now.getMinutes(); + + // Round up to next half-hour or hour + const roundedMinutes = minutes > 30 ? 60 : 30; + + if (roundedMinutes === 60) { + now.setHours(now.getHours() + 1); + now.setMinutes(0); + } else { + now.setMinutes(30); + } + + now.setSeconds(0); + now.setMilliseconds(0); + + return now; +} diff --git a/booking-app/components/src/server/calendars.ts b/booking-app/components/src/server/calendars.ts index 8b92b9d2..8ba35a52 100644 --- a/booking-app/components/src/server/calendars.ts +++ b/booking-app/components/src/server/calendars.ts @@ -1,12 +1,11 @@ import { BookingFormDetails, BookingStatusLabel, RoomSetting } from "../types"; -import { Timestamp, endAt, sum, where } from "@firebase/firestore"; import { TableNames } from "../policy"; import { clientFetchAllDataFromCollection } from "@/lib/firebase/firebase"; import { getCalendarClient } from "@/lib/googleClient"; import { serverGetRoomCalendarIds } from "./admin"; -const patchCalendarEvent = async ( +export const patchCalendarEvent = async ( event: any, calendarId: string, eventId: string, @@ -190,3 +189,34 @@ export const deleteEvent = async ( console.log("calendar event doesn't exist for room " + roomId); } }; + +export const updateByCalendarEventId = async ( + calendarEventId: string, + newValues: any +) => { + // const allRooms: RoomSetting[] = await clientFetchAllDataFromCollection( + // TableNames.RESOURCES + // ); + // const roomCalendarIds = allRooms.map((room) => room.calendarId); + // // const calendarIdsToEvent = {}; + // const calendar = await getCalendarClient(); + // for (const roomCalendarId of roomCalendarIds) { + // const event = await calendar.events.get({ + // calendarId: roomCalendarId, + // eventId: calendarEventId, + // }); + // await patchCalendarEvent(event, roomCalendarId, calendarEventId, newValues); + // } +}; + +// update endTime for all calendar events +// searchCalendarsForEventId(id); +// const roomCalendarIdsToEvents = await searchCalendarsForEventId(id); +// for (let [calendarId, event] of Object.entries(roomCalendarIdsToEvents)) { +// await patchCalendarEvent(event, calendarId, id, { +// end: { +// dateTime: checkoutDate.toISOString(), +// }, +// }); +// console.log(`Updated end time on ${calendarId} event: ${id}`); +// } diff --git a/booking-app/components/src/server/db.ts b/booking-app/components/src/server/db.ts index 6e238244..05796ffd 100644 --- a/booking-app/components/src/server/db.ts +++ b/booking-app/components/src/server/db.ts @@ -1,22 +1,23 @@ -import { Timestamp, where } from "@firebase/firestore"; - import { + BookingFormDetails, + BookingStatusLabel, + PolicySettings, +} from "../types"; +import { + TableNames, clientGetFinalApproverEmail, getCancelCcEmail, - TableNames, } from "../policy"; +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 { - BookingFormDetails, - BookingStatusLabel, - PolicySettings, -} from "../types"; -import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; +import { roundTimeUp } from "../client/utils/date"; export const fetchAllFutureBooking = async ( collectionName: TableNames @@ -190,9 +191,13 @@ export const checkin = async (id: string) => { }; export const checkOut = async (id: string) => { + const checkoutDate = roundTimeUp(); clientUpdateDataByCalendarEventId(TableNames.BOOKING_STATUS, id, { checkedOutAt: Timestamp.now(), }); + clientUpdateDataByCalendarEventId(TableNames.BOOKING, id, { + endDate: Timestamp.fromDate(checkoutDate), + }); const doc = await clientGetDataByCalendarEventId( TableNames.BOOKING_STATUS, id @@ -208,6 +213,7 @@ export const checkOut = async (id: string) => { headerMessage, BookingStatusLabel.CHECKED_OUT ); + const response = await fetch( `${process.env.NEXT_PUBLIC_BASE_URL}/api/calendarEvents`, { From f2f9c7494f25459db71977551ab5390caffb3e63 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:29:22 -0400 Subject: [PATCH 02/16] more calendar drag and drop --- .../booking/components/CalendarEventBlock.tsx | 72 ++++++++++--------- .../components/CalendarVerticalResource.tsx | 32 ++++++++- 2 files changed, 69 insertions(+), 35 deletions(-) diff --git a/booking-app/components/src/client/routes/booking/components/CalendarEventBlock.tsx b/booking-app/components/src/client/routes/booking/components/CalendarEventBlock.tsx index f9786b11..92ca0fbf 100644 --- a/booking-app/components/src/client/routes/booking/components/CalendarEventBlock.tsx +++ b/booking-app/components/src/client/routes/booking/components/CalendarEventBlock.tsx @@ -1,6 +1,5 @@ import { Box, IconButton } from "@mui/material"; -import { BookingStatusLabel } from "../../../../types"; import { Close } from "@mui/icons-material"; import { EventContentArg } from "@fullcalendar/core"; import React from "react"; @@ -11,28 +10,31 @@ export const UNKNOWN_BLOCK_TITLE = "Unavailable"; interface Props { bgcolor: string; - isFirst: boolean; - isLast: boolean; - isNew: boolean; + isnew: boolean; + numrooms: number; } -const Block = styled(Box)( - ({ theme, bgcolor, isFirst, isLast, isNew }) => ({ - backgroundColor: bgcolor || theme.palette.primary.main, - border: `2px solid ${bgcolor || theme.palette.primary.main}`, - borderRadius: "4px", - outline: "none", - height: "100%", - width: isNew && !isLast ? "105%" : "100%", - overflowX: "hidden", - cursor: bgcolor ? "unset" : "pointer", - padding: "2px 4px", - position: "relative", - zIndex: isNew ? 99 : 2, - borderTopLeftRadius: isNew && !isFirst ? 0 : 4, - borderBottomLeftRadius: isNew && !isFirst ? 0 : 4, - }) -); +const Block = styled(Box)(({ theme, bgcolor, isnew, numrooms }) => ({ + backgroundColor: bgcolor || theme.palette.primary.main, + border: `2px solid ${bgcolor || theme.palette.primary.main}`, + borderRadius: "4px", + outline: "none", + height: "100%", + width: isnew + ? `calc((100% + 2px) * ${numrooms} + ${numrooms - 1}px)` + : "100%", + overflowX: "hidden", + cursor: bgcolor ? "unset" : "pointer", + padding: "2px 4px", + position: "relative", + zIndex: isnew ? 99 : 2, +})); + +const Empty = styled(Box)` + width: "100%"; + height: "100%"; + position: relative; +`; const CloseButton = styled(IconButton)(({ theme }) => ({ position: "absolute", @@ -52,8 +54,9 @@ export default function CalendarEventBlock(eventInfo: EventContentArg) { ? eventInfo.event.url.split(":") : ["0", "0"]; const index = Number(params[0]); - const maxIndex = Number(params[1]); - const isLast = index === maxIndex; + const numRooms = Number(params[1]); + + const isLast = index === numRooms - 1; const isOneColumn = index === 0 && isLast; const backgroundColor = () => { @@ -63,17 +66,22 @@ export default function CalendarEventBlock(eventInfo: EventContentArg) { return "rgba(72, 196, 77, 1)"; }; - const hideTitle = isNew && index !== 0 && !isOneColumn; + if (isNew && index !== 0) { + return ( + + {isNew && isLast && ( + + + + )} + + ); + } return ( - - {!hideTitle && {title}} - {isNew && isLast && ( + + {title} + {isNew && isOneColumn && ( diff --git a/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx b/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx index 9853a86a..6a509743 100644 --- a/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx +++ b/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx @@ -1,10 +1,16 @@ import { Box, Typography, useMediaQuery, useTheme } from "@mui/material"; -import { CalendarApi, DateSelectArg, EventClickArg } from "@fullcalendar/core"; +import { + CalendarApi, + DateSelectArg, + EventClickArg, + EventDropArg, +} from "@fullcalendar/core"; import { CalendarEvent, RoomSetting } from "../../../../types"; import CalendarEventBlock, { NEW_TITLE_TAG } from "./CalendarEventBlock"; import React, { useContext, useEffect, useMemo, useRef } from "react"; import { BookingContext } from "../bookingProvider"; +import { EventResizeDoneArg } from "fullcalendar"; import FullCalendar from "@fullcalendar/react"; import googleCalendarPlugin from "@fullcalendar/google-calendar"; import interactionPlugin from "@fullcalendar/interaction"; // for selectable @@ -117,7 +123,10 @@ export default function CalendarVerticalResource({ resourceId: room.roomId + "", title: NEW_TITLE_TAG, overlap: true, - url: `${index}:${rooms.length - 1}`, // some hackiness to let us render multiple events visually as one big block + durationEditable: true, + startEditable: true, + groupId: "new", + url: `${index}:${rooms.length}`, // some hackiness to let us render multiple events visually as one big block })); }, [bookingCalendarInfo, rooms]); @@ -164,12 +173,26 @@ export default function CalendarVerticalResource({ return el.overlap; }; + // clicking on created event should delete it const handleEventClick = (info: EventClickArg) => { if (info.event.title.includes(NEW_TITLE_TAG)) { setBookingCalendarInfo(null); } }; + // if change event duration via dragging edges or drag event block to move + const handleEventEdit = (info: EventResizeDoneArg | EventDropArg) => { + setBookingCalendarInfo({ + startStr: info.event.startStr, + start: info.event.start, + endStr: info.event.endStr, + end: info.event.end, + allDay: false, + jsEvent: info.jsEvent, + view: info.view, + }); + }; + // for editing an existing reservation const existingCalEventsFiltered = useMemo(() => { if (!isEdit || calendarEventId == null || calendarEventId.length === 0) @@ -185,7 +208,8 @@ export default function CalendarVerticalResource({ return ( - Select spaces to view their availability and choose a time slot + Select spaces to view their availability, then click and drag to + choose a time slot ); @@ -214,6 +238,8 @@ export default function CalendarVerticalResource({ info.jsEvent.preventDefault(); handleEventClick(info); }} + eventResize={handleEventEdit} + eventDrop={handleEventEdit} headerToolbar={false} slotMinTime="09:00:00" slotMaxTime="21:00:00" From ead2fa6847447a47d7c1ab09d7541e8152696279 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:44:27 -0400 Subject: [PATCH 03/16] remove webpack error --- .../routes/admin/components/BookingActions.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 d96078a7..0824ab10 100644 --- a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx +++ b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx @@ -16,7 +16,7 @@ import ConfirmDialog from "../../components/ConfirmDialog"; import { DatabaseContext } from "../../components/Provider"; import Loading from "../../components/Loading"; import { Timestamp } from "@firebase/firestore"; -import { updateByCalendarEventId } from "@/components/src/server/calendars"; +// import { updateByCalendarEventId } from "@/components/src/server/calendars"; import useExistingBooking from "../hooks/useExistingBooking"; import { useRouter } from "next/navigation"; @@ -128,11 +128,11 @@ export default function BookingActions({ [Actions.CHECK_OUT]: { action: async () => { await checkOut(calendarEventId); - await updateByCalendarEventId(calendarEventId, { - end: { - dateTime: new Date().toISOString(), - }, - }); + // await updateByCalendarEventId(calendarEventId, { + // end: { + // dateTime: new Date().toISOString(), + // }, + // }); }, optimisticNextStatus: BookingStatusLabel.CHECKED_OUT, }, From 4faee5bbfd4c5407152305faebdb3c95c9d5aeea Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:08:47 -0400 Subject: [PATCH 04/16] update event end time on checkout --- booking-app/app/api/calendarEvents/route.ts | 8 +-- .../admin/components/BookingActions.tsx | 6 -- booking-app/components/src/server/admin.ts | 15 ++--- .../components/src/server/calendars.ts | 56 +++++++++++++------ booking-app/components/src/server/db.ts | 15 +++-- 5 files changed, 61 insertions(+), 39 deletions(-) diff --git a/booking-app/app/api/calendarEvents/route.ts b/booking-app/app/api/calendarEvents/route.ts index 11db3bce..4e8cfabc 100644 --- a/booking-app/app/api/calendarEvents/route.ts +++ b/booking-app/app/api/calendarEvents/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { deleteEvent, insertEvent, - updateEventPrefix, + updateCalendarEvent, } from "@/components/src/server/calendars"; import { getCalendarClient } from "@/lib/googleClient"; @@ -101,9 +101,9 @@ export async function GET(req: NextRequest) { } export async function PUT(req: NextRequest) { - const { calendarEventId, newPrefix } = await req.json(); + const { calendarEventId, newValues } = await req.json(); - if (!calendarEventId || !newPrefix) { + if (!calendarEventId || !newValues) { return NextResponse.json( { error: "Missing required fields" }, { status: 400 }, @@ -111,7 +111,7 @@ export async function PUT(req: NextRequest) { } const contents = await serverBookingContents(calendarEventId); try { - await updateEventPrefix(calendarEventId, newPrefix, contents); + await updateCalendarEvent(calendarEventId, newValues, contents); return NextResponse.json( { message: "Event updated successfully" }, { status: 200 }, 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 0824ab10..dfa9e80d 100644 --- a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx +++ b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx @@ -16,7 +16,6 @@ import ConfirmDialog from "../../components/ConfirmDialog"; import { DatabaseContext } from "../../components/Provider"; import Loading from "../../components/Loading"; import { Timestamp } from "@firebase/firestore"; -// import { updateByCalendarEventId } from "@/components/src/server/calendars"; import useExistingBooking from "../hooks/useExistingBooking"; import { useRouter } from "next/navigation"; @@ -128,11 +127,6 @@ export default function BookingActions({ [Actions.CHECK_OUT]: { action: async () => { await checkOut(calendarEventId); - // await updateByCalendarEventId(calendarEventId, { - // end: { - // dateTime: new Date().toISOString(), - // }, - // }); }, optimisticNextStatus: BookingStatusLabel.CHECKED_OUT, }, diff --git a/booking-app/components/src/server/admin.ts b/booking-app/components/src/server/admin.ts index 2b942375..ef068696 100644 --- a/booking-app/components/src/server/admin.ts +++ b/booking-app/components/src/server/admin.ts @@ -5,11 +5,6 @@ import { PolicySettings, RoomSetting, } from "../types"; - -import { getApprovalCcEmail, TableNames } from "../policy"; -import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; - -import { Timestamp } from "firebase-admin/firestore"; import { Constraint, serverDeleteData, @@ -18,6 +13,10 @@ import { serverGetFinalApproverEmail, serverUpdateInFirestore, } from "@/lib/firebase/server/adminDb"; +import { TableNames, getApprovalCcEmail } from "../policy"; +import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; + +import { Timestamp } from "firebase-admin/firestore"; export const serverBookingContents = (id: string) => { return serverGetDataByCalendarEventId(TableNames.BOOKING, id) @@ -121,7 +120,9 @@ export const serverApproveBooking = async (id: string) => { }, body: JSON.stringify({ calendarEventId: id, - newPrefix: BookingStatusLabel.PENDING, + newValues: { + statusPrefix: BookingStatusLabel.PENDING, + }, }), } ); @@ -241,7 +242,7 @@ export const serverApproveEvent = async (id: string) => { const formDataForCalendarEvents = { calendarEventId: id, - newPrefix: BookingStatusLabel.APPROVED, + newValues: { statusPrefix: BookingStatusLabel.APPROVED }, }; await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/calendarEvents`, { method: "PUT", diff --git a/booking-app/components/src/server/calendars.ts b/booking-app/components/src/server/calendars.ts index 8ba35a52..2ea6c0cd 100644 --- a/booking-app/components/src/server/calendars.ts +++ b/booking-app/components/src/server/calendars.ts @@ -122,9 +122,14 @@ export const insertEvent = async ({ return event.data; }; -export const updateEventPrefix = async ( +export const updateCalendarEvent = async ( calendarEventId: string, - newPrefix: BookingStatusLabel, + newValues: { + end: { + dateTime: string; + }; + statusPrefix?: BookingStatusLabel; + }, bookingContents: BookingFormDetails ) => { const roomCalendarIds = await serverGetRoomCalendarIds( @@ -143,27 +148,44 @@ export const updateEventPrefix = async ( eventId: calendarEventId, }); - if (event) { + if (!event) { + throw new Error("event not found with specified id"); + } + + const updatedValues = {}; + + if (newValues.statusPrefix) { const eventData = event.data; const eventTitle = eventData.summary ?? ""; const prefixRegex = /\[.*?\]/g; - const newTitle = eventTitle.replace(prefixRegex, `[${newPrefix}]`); + const newTitle = eventTitle.replace( + prefixRegex, + `[${newValues.statusPrefix}]` + ); + updatedValues["summary"] = newTitle; + } - let description = bookingContents - ? bookingContentsToDescription(bookingContents) - : ""; - description += - 'To cancel reservations please return to the Booking Tool, visit My Bookings, and click "cancel" on the booking at least 24 hours before the date of the event. Failure to cancel an unused booking is considered a no-show and may result in restricted use of the space.'; + if (newValues.end) { + updatedValues["end"] = newValues.end; + } - await patchCalendarEvent(event, roomCalendarId, calendarEventId, { - summary: newTitle, - description: description, - }); + let description = bookingContents + ? bookingContentsToDescription(bookingContents) + : ""; + description += + 'To cancel reservations please return to the Booking Tool, visit My Bookings, and click "cancel" on the booking at least 24 hours before the date of the event. Failure to cancel an unused booking is considered a no-show and may result in restricted use of the space.'; + updatedValues["description"] = description; + + await patchCalendarEvent( + event, + roomCalendarId, + calendarEventId, + updatedValues + ); - console.log( - `Updated event ${calendarEventId} in calendar ${roomCalendarId} with new prefix ${newPrefix}` - ); - } + console.log( + `Updated event ${calendarEventId} in calendar ${roomCalendarId} with new values: ${JSON.stringify(newValues)}` + ); } catch (error) { console.error( `Error updating event ${calendarEventId} in calendar ${roomCalendarId}:`, diff --git a/booking-app/components/src/server/db.ts b/booking-app/components/src/server/db.ts index 05796ffd..ac299854 100644 --- a/booking-app/components/src/server/db.ts +++ b/booking-app/components/src/server/db.ts @@ -97,7 +97,7 @@ export const decline = async (id: string) => { }, body: JSON.stringify({ calendarEventId: id, - newPrefix: BookingStatusLabel.DECLINED, + newValues: { statusPrefix: BookingStatusLabel.DECLINED }, }), } ); @@ -135,7 +135,7 @@ export const cancel = async (id: string) => { }, body: JSON.stringify({ calendarEventId: id, - newPrefix: BookingStatusLabel.CANCELED, + newValues: { statusPrefix: BookingStatusLabel.CANCELED }, }), } ); @@ -184,7 +184,7 @@ export const checkin = async (id: string) => { }, body: JSON.stringify({ calendarEventId: id, - newPrefix: BookingStatusLabel.CHECKED_IN, + newValues: { statusPrefix: BookingStatusLabel.CHECKED_IN }, }), } ); @@ -223,7 +223,12 @@ export const checkOut = async (id: string) => { }, body: JSON.stringify({ calendarEventId: id, - newPrefix: BookingStatusLabel.CHECKED_OUT, + newValues: { + statusPrefix: BookingStatusLabel.CHECKED_OUT, + end: { + dateTime: roundTimeUp().toISOString(), + }, + }, }), } ); @@ -262,7 +267,7 @@ export const noShow = async (id: string) => { }, body: JSON.stringify({ calendarEventId: id, - newPrefix: BookingStatusLabel.NO_SHOW, + newValues: { statusPrefix: BookingStatusLabel.NO_SHOW }, }), } ); From 614c42239ead44af4c4e50976295f7ff455af345 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:34:54 -0400 Subject: [PATCH 05/16] refactor isEdit and isWalkIn to formContext --- booking-app/app/edit/form/[id]/page.tsx | 6 ++++- booking-app/app/edit/role/[id]/page.tsx | 6 ++++- booking-app/app/edit/selectRoom/[id]/page.tsx | 6 ++++- booking-app/app/walk-in/form/page.tsx | 5 ++++- booking-app/app/walk-in/role/page.tsx | 5 ++++- booking-app/app/walk-in/selectRoom/page.tsx | 5 ++++- .../components/BookingFormMediaServices.tsx | 10 ++++----- .../components/CalendarVerticalResource.tsx | 14 +++++++----- .../routes/booking/components/FormInput.tsx | 22 ++++++++++--------- .../routes/booking/components/SelectRooms.tsx | 9 ++++---- .../formPages/BookingFormDetailsPage.tsx | 9 ++++---- .../booking/formPages/SelectRoomPage.tsx | 13 ++++++----- .../routes/booking/formPages/UserRolePage.tsx | 16 ++++++++------ .../routes/booking/hooks/useSubmitBooking.tsx | 7 ++++-- booking-app/components/src/types.ts | 9 ++++++++ 15 files changed, 92 insertions(+), 50 deletions(-) diff --git a/booking-app/app/edit/form/[id]/page.tsx b/booking-app/app/edit/form/[id]/page.tsx index 18cc0282..8125dc08 100644 --- a/booking-app/app/edit/form/[id]/page.tsx +++ b/booking-app/app/edit/form/[id]/page.tsx @@ -3,10 +3,14 @@ "use client"; import BookingFormDetailsPage from "@/components/src/client/routes/booking/formPages/BookingFormDetailsPage"; +import { FormContextLevel } from "@/components/src/types"; import React from "react"; const Form: React.FC = ({ params }: { params: { id: string } }) => ( - + ); export default Form; diff --git a/booking-app/app/edit/role/[id]/page.tsx b/booking-app/app/edit/role/[id]/page.tsx index c13fe9ab..d81b0722 100644 --- a/booking-app/app/edit/role/[id]/page.tsx +++ b/booking-app/app/edit/role/[id]/page.tsx @@ -1,9 +1,13 @@ +import { FormContextLevel } from "@/components/src/types"; // app/edit/role/[id].tsx import React from "react"; import UserRolePage from "@/components/src/client/routes/booking/formPages/UserRolePage"; const Role: React.FC = ({ params }: { params: { id: string } }) => ( - + ); export default Role; diff --git a/booking-app/app/edit/selectRoom/[id]/page.tsx b/booking-app/app/edit/selectRoom/[id]/page.tsx index 2078cc7f..bd0c9ce2 100644 --- a/booking-app/app/edit/selectRoom/[id]/page.tsx +++ b/booking-app/app/edit/selectRoom/[id]/page.tsx @@ -2,11 +2,15 @@ "use client"; +import { FormContextLevel } from "@/components/src/types"; import React from "react"; import SelectRoomPage from "@/components/src/client/routes/booking/formPages/SelectRoomPage"; const SelectRoom: React.FC = ({ params }: { params: { id: string } }) => ( - + ); export default SelectRoom; diff --git a/booking-app/app/walk-in/form/page.tsx b/booking-app/app/walk-in/form/page.tsx index 8b89434e..4a4f1b32 100644 --- a/booking-app/app/walk-in/form/page.tsx +++ b/booking-app/app/walk-in/form/page.tsx @@ -3,8 +3,11 @@ "use client"; import BookingFormDetailsPage from "@/components/src/client/routes/booking/formPages/BookingFormDetailsPage"; +import { FormContextLevel } from "@/components/src/types"; import React from "react"; -const Form: React.FC = () => ; +const Form: React.FC = () => ( + +); export default Form; diff --git a/booking-app/app/walk-in/role/page.tsx b/booking-app/app/walk-in/role/page.tsx index 42b7c5ee..c17a842f 100644 --- a/booking-app/app/walk-in/role/page.tsx +++ b/booking-app/app/walk-in/role/page.tsx @@ -1,7 +1,10 @@ +import { FormContextLevel } from "@/components/src/types"; // app/walk-in/form/page.tsx import React from "react"; import UserRolePage from "@/components/src/client/routes/booking/formPages/UserRolePage"; -const Role: React.FC = () => ; +const Role: React.FC = () => ( + +); export default Role; diff --git a/booking-app/app/walk-in/selectRoom/page.tsx b/booking-app/app/walk-in/selectRoom/page.tsx index 1f2108bd..c691d852 100644 --- a/booking-app/app/walk-in/selectRoom/page.tsx +++ b/booking-app/app/walk-in/selectRoom/page.tsx @@ -2,9 +2,12 @@ "use client"; +import { FormContextLevel } from "@/components/src/types"; import React from "react"; import SelectRoomPage from "@/components/src/client/routes/booking/formPages/SelectRoomPage"; -const SelectRoom: React.FC = () => ; +const SelectRoom: React.FC = () => ( + +); export default SelectRoom; diff --git a/booking-app/components/src/client/routes/booking/components/BookingFormMediaServices.tsx b/booking-app/components/src/client/routes/booking/components/BookingFormMediaServices.tsx index 264d454c..e759ac8d 100644 --- a/booking-app/components/src/client/routes/booking/components/BookingFormMediaServices.tsx +++ b/booking-app/components/src/client/routes/booking/components/BookingFormMediaServices.tsx @@ -5,7 +5,7 @@ import { } from "../../../../policy"; import { Checkbox, FormControlLabel, Switch } from "@mui/material"; import { Control, Controller, UseFormTrigger } from "react-hook-form"; -import { Inputs, MediaServices } from "../../../../types"; +import { FormContextLevel, Inputs, MediaServices } from "../../../../types"; import React, { useContext, useMemo } from "react"; import { BookingContext } from "../bookingProvider"; @@ -24,7 +24,7 @@ interface Props { trigger: UseFormTrigger; showMediaServices: boolean; setShowMediaServices: any; - isWalkIn: boolean; + formContext: FormContextLevel; } export default function BookingFormMediaServices(props: Props) { @@ -34,7 +34,7 @@ export default function BookingFormMediaServices(props: Props) { trigger, showMediaServices, setShowMediaServices, - isWalkIn, + formContext, } = props; const { selectedRooms } = useContext(BookingContext); const roomIds = selectedRooms.map((room) => room.roomId); @@ -74,7 +74,7 @@ export default function BookingFormMediaServices(props: Props) { if (!e.target.checked) { // de-select boxes if switch says "no media services" field.onChange(""); - } else if (isWalkIn) { + } else if (formContext === FormContextLevel.WALK_IN) { field.onChange(MediaServices.CHECKOUT_EQUIPMENT); } @@ -88,7 +88,7 @@ export default function BookingFormMediaServices(props: Props) { > ); - if (isWalkIn) { + if (formContext === FormContextLevel.WALK_IN) { return (
diff --git a/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx b/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx index 6a509743..0d2e7901 100644 --- a/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx +++ b/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx @@ -5,8 +5,8 @@ import { EventClickArg, EventDropArg, } from "@fullcalendar/core"; -import { CalendarEvent, RoomSetting } from "../../../../types"; import CalendarEventBlock, { NEW_TITLE_TAG } from "./CalendarEventBlock"; +import { FormContextLevel, RoomSetting } from "../../../../types"; import React, { useContext, useEffect, useMemo, useRef } from "react"; import { BookingContext } from "../bookingProvider"; @@ -19,7 +19,7 @@ import { styled } from "@mui/system"; interface Props { calendarEventId?: string; - isEdit: boolean; + formContext: FormContextLevel; rooms: RoomSetting[]; dateView: Date; } @@ -78,7 +78,7 @@ const Empty = styled(Box)(({ theme }) => ({ export default function CalendarVerticalResource({ calendarEventId, - isEdit, + formContext, rooms, dateView, }: Props) { @@ -195,14 +195,18 @@ export default function CalendarVerticalResource({ // for editing an existing reservation const existingCalEventsFiltered = useMemo(() => { - if (!isEdit || calendarEventId == null || calendarEventId.length === 0) + if ( + formContext !== FormContextLevel.EDIT || + calendarEventId == null || + calendarEventId.length === 0 + ) return existingCalendarEvents; // based on how we format the id in fetchCalendarEvents return existingCalendarEvents.filter( (event) => event.id.split(":")[0] !== calendarEventId ); - }, [existingCalendarEvents, isEdit]); + }, [existingCalendarEvents, formContext]); if (rooms.length === 0) { return ( 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 c6a5eb2c..707d438d 100644 --- a/booking-app/components/src/client/routes/booking/components/FormInput.tsx +++ b/booking-app/components/src/client/routes/booking/components/FormInput.tsx @@ -1,4 +1,9 @@ -import { AttendeeAffiliation, Inputs, Role } from "../../../../types"; +import { + AttendeeAffiliation, + FormContextLevel, + Inputs, + Role, +} from "../../../../types"; import { BookingFormAgreementCheckbox, BookingFormDropdown, @@ -54,15 +59,10 @@ const Container = styled(Box)(({ theme }) => ({ interface Props { calendarEventId?: string; - isEdit: boolean; - isWalkIn: boolean; + formContext: FormContextLevel; } -export default function FormInput({ - calendarEventId, - isEdit, - isWalkIn, -}: Props) { +export default function FormInput({ calendarEventId, formContext }: Props) { const { userEmail, settings } = useContext(DatabaseContext); const { role, @@ -75,7 +75,7 @@ export default function FormInput({ setFormData, } = useContext(BookingContext); const router = useRouter(); - const registerEvent = useSubmitBooking(isEdit, isWalkIn); + const registerEvent = useSubmitBooking(formContext); const { isAutoApproval } = useCheckAutoApproval(); const { @@ -109,6 +109,8 @@ export default function FormInput({ mode: "onBlur", }); + const isWalkIn = formContext === FormContextLevel.WALK_IN; + // different from other switches b/c mediaServices doesn't have yes/no column in DB const [showMediaServices, setShowMediaServices] = useState(false); @@ -380,7 +382,7 @@ export default function FormInput({ trigger, showMediaServices, setShowMediaServices, - isWalkIn, + formContext, }} /> {watch("mediaServices") !== undefined && diff --git a/booking-app/components/src/client/routes/booking/components/SelectRooms.tsx b/booking-app/components/src/client/routes/booking/components/SelectRooms.tsx index d8f00ccb..1e21709a 100644 --- a/booking-app/components/src/client/routes/booking/components/SelectRooms.tsx +++ b/booking-app/components/src/client/routes/booking/components/SelectRooms.tsx @@ -1,21 +1,21 @@ import { Checkbox, FormControlLabel, FormGroup } from "@mui/material"; +import { FormContextLevel, RoomSetting } from "../../../../types"; import { MOCAP_ROOMS, WALK_IN_CAN_BOOK_TWO } from "../../../../policy"; import React, { useContext, useMemo } from "react"; import { BookingContext } from "../bookingProvider"; import { ConfirmDialogControlled } from "../../components/ConfirmDialog"; -import { RoomSetting } from "../../../../types"; interface Props { allRooms: RoomSetting[]; - isWalkIn: boolean; + formContext: FormContextLevel; selected: RoomSetting[]; setSelected: any; } export const SelectRooms = ({ allRooms, - isWalkIn, + formContext, selected, setSelected, }: Props) => { @@ -34,7 +34,8 @@ export const SelectRooms = ({ // walk-ins can only book 1 room unless it's 2 ballroom bays (221-224) const isDisabled = (roomId: number) => { - if (!isWalkIn || selectedIds.length === 0) return false; + if (formContext !== FormContextLevel.WALK_IN || selectedIds.length === 0) + return false; if (selectedIds.includes(roomId)) return false; if (selectedIds.length >= 2) return true; if ( diff --git a/booking-app/components/src/client/routes/booking/formPages/BookingFormDetailsPage.tsx b/booking-app/components/src/client/routes/booking/formPages/BookingFormDetailsPage.tsx index 7709bb8b..4e7de2b6 100644 --- a/booking-app/components/src/client/routes/booking/formPages/BookingFormDetailsPage.tsx +++ b/booking-app/components/src/client/routes/booking/formPages/BookingFormDetailsPage.tsx @@ -1,5 +1,6 @@ "use client"; +import { FormContextLevel } from "@/components/src/types"; import FormInput from "../components/FormInput"; import Grid from "@mui/material/Unstable_Grid2"; import React from "react"; @@ -7,21 +8,19 @@ import useCheckFormMissingData from "../hooks/useCheckFormMissingData"; interface Props { calendarEventId?: string; - isEdit?: boolean; - isWalkIn?: boolean; + formContext?: FormContextLevel; } export default function BookingFormDetailsPage({ calendarEventId, - isEdit = false, - isWalkIn = false, + formContext = FormContextLevel.FULL_FORM, }: Props) { useCheckFormMissingData(); return ( - + ); diff --git a/booking-app/components/src/client/routes/booking/formPages/SelectRoomPage.tsx b/booking-app/components/src/client/routes/booking/formPages/SelectRoomPage.tsx index 8d416515..bc5c094f 100644 --- a/booking-app/components/src/client/routes/booking/formPages/SelectRoomPage.tsx +++ b/booking-app/components/src/client/routes/booking/formPages/SelectRoomPage.tsx @@ -7,6 +7,7 @@ import { BookingContext } from "../bookingProvider"; import { CalendarDatePicker } from "../components/CalendarDatePicker"; import CalendarVerticalResource from "../components/CalendarVerticalResource"; import { DatabaseContext } from "../../components/Provider"; +import { FormContextLevel } from "@/components/src/types"; import Grid from "@mui/material/Unstable_Grid2"; import { SelectRooms } from "../components/SelectRooms"; import { WALK_IN_ROOMS } from "@/components/src/policy"; @@ -14,14 +15,12 @@ import useCheckFormMissingData from "../hooks/useCheckFormMissingData"; interface Props { calendarEventId?: string; - isEdit?: boolean; - isWalkIn?: boolean; + formContext?: FormContextLevel; } export default function SelectRoomPage({ calendarEventId, - isEdit = false, - isWalkIn = false, + formContext = FormContextLevel.FULL_FORM, }: Props) { const { roomSettings } = useContext(DatabaseContext); const { selectedRooms, setSelectedRooms } = useContext(BookingContext); @@ -30,6 +29,8 @@ export default function SelectRoomPage({ const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const isWalkIn = formContext === FormContextLevel.WALK_IN; + const roomsToShow = useMemo(() => { return !isWalkIn ? roomSettings @@ -49,7 +50,7 @@ export default function SelectRoomPage({ Spaces @@ -60,7 +61,7 @@ export default function SelectRoomPage({ diff --git a/booking-app/components/src/client/routes/booking/formPages/UserRolePage.tsx b/booking-app/components/src/client/routes/booking/formPages/UserRolePage.tsx index 4397ae4d..62f48a9a 100644 --- a/booking-app/components/src/client/routes/booking/formPages/UserRolePage.tsx +++ b/booking-app/components/src/client/routes/booking/formPages/UserRolePage.tsx @@ -1,7 +1,7 @@ "use client"; import { Box, Button, Typography } from "@mui/material"; -import { Department, Role } from "../../../../types"; +import { Department, FormContextLevel, Role } from "../../../../types"; import React, { useContext, useEffect } from "react"; import { BookingContext } from "../bookingProvider"; @@ -28,14 +28,12 @@ const Container = styled(Box)(({ theme }) => ({ interface Props { calendarEventId?: string; - isEdit?: boolean; - isWalkIn?: boolean; + formContext?: FormContextLevel; } export default function UserRolePage({ calendarEventId, - isEdit = false, - isWalkIn = false, + formContext = FormContextLevel.FULL_FORM, }: Props) { const { role, department, setDepartment, setRole } = useContext(BookingContext); @@ -50,10 +48,14 @@ export default function UserRolePage({ }, []); const handleNextClick = () => { - if (isEdit && calendarEventId != null) { + if (formContext === FormContextLevel.EDIT && calendarEventId != null) { router.push("/edit/selectRoom/" + calendarEventId); } else { - router.push(isWalkIn ? "/walk-in/selectRoom" : "/book/selectRoom"); + router.push( + formContext === FormContextLevel.WALK_IN + ? "/walk-in/selectRoom" + : "/book/selectRoom" + ); } }; 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 5a696bdc..81c0f108 100644 --- a/booking-app/components/src/client/routes/booking/hooks/useSubmitBooking.tsx +++ b/booking-app/components/src/client/routes/booking/hooks/useSubmitBooking.tsx @@ -1,11 +1,11 @@ +import { FormContextLevel, Inputs } from "../../../../types"; import { useCallback, useContext } from "react"; import { BookingContext } from "../bookingProvider"; import { DatabaseContext } from "../../components/Provider"; -import { Inputs } from "../../../../types"; import { useRouter } from "next/navigation"; -export default function useSubmitBooking(isEdit: boolean, isWalkIn: boolean) { +export default function useSubmitBooking(formContext: FormContextLevel) { const router = useRouter(); const { liaisonUsers, userEmail, reloadBookings, reloadBookingStatuses } = useContext(DatabaseContext); @@ -21,6 +21,9 @@ export default function useSubmitBooking(isEdit: boolean, isWalkIn: boolean) { setSubmitting, } = useContext(BookingContext); + const isEdit = formContext === FormContextLevel.EDIT; + const isWalkIn = formContext === FormContextLevel.WALK_IN; + const registerEvent = useCallback( async (data: Inputs, isAutoApproval: boolean, calendarEventId?: string) => { if ( diff --git a/booking-app/components/src/types.ts b/booking-app/components/src/types.ts index fcbc1ca6..55d6e3c6 100644 --- a/booking-app/components/src/types.ts +++ b/booking-app/components/src/types.ts @@ -95,6 +95,15 @@ export enum Department { } export type DevBranch = "development" | "staging" | "production" | ""; +// what context are we entering the form in? +// increasing levels of edit power +export enum FormContextLevel { + MODIFICATION = 0, + WALK_IN = 1, + EDIT = 2, + FULL_FORM = 99, +} + export type Inputs = { firstName: string; lastName: string; From bfa0a0384ae13bdea7ef8d597cf93a31805e1c98 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Wed, 25 Sep 2024 21:59:32 -0400 Subject: [PATCH 06/16] add pages for modification flow --- booking-app/app/modification/[id]/page.tsx | 12 +++ .../app/modification/form/[id]/page.tsx | 16 ++++ booking-app/app/modification/layout.tsx | 13 +++ .../app/modification/selectRoom/[id]/page.tsx | 16 ++++ .../modification/ModificationLandingPage.tsx | 81 +++++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 booking-app/app/modification/[id]/page.tsx create mode 100644 booking-app/app/modification/form/[id]/page.tsx create mode 100644 booking-app/app/modification/layout.tsx create mode 100644 booking-app/app/modification/selectRoom/[id]/page.tsx create mode 100644 booking-app/components/src/client/routes/modification/ModificationLandingPage.tsx diff --git a/booking-app/app/modification/[id]/page.tsx b/booking-app/app/modification/[id]/page.tsx new file mode 100644 index 00000000..57794c67 --- /dev/null +++ b/booking-app/app/modification/[id]/page.tsx @@ -0,0 +1,12 @@ +// app/modification/[id].tsx + +"use client"; + +import ModificationLandingPage from "@/components/src/client/routes/modification/ModificationLandingPage"; +import React from "react"; + +const HomePage: React.FC = ({ params }: { params: { id: string } }) => ( + +); + +export default HomePage; diff --git a/booking-app/app/modification/form/[id]/page.tsx b/booking-app/app/modification/form/[id]/page.tsx new file mode 100644 index 00000000..425aa8c2 --- /dev/null +++ b/booking-app/app/modification/form/[id]/page.tsx @@ -0,0 +1,16 @@ +// app/modification/form/[id].tsx + +"use client"; + +import BookingFormDetailsPage from "@/components/src/client/routes/booking/formPages/BookingFormDetailsPage"; +import { FormContextLevel } from "@/components/src/types"; +import React from "react"; + +const Form: React.FC = ({ params }: { params: { id: string } }) => ( + +); + +export default Form; diff --git a/booking-app/app/modification/layout.tsx b/booking-app/app/modification/layout.tsx new file mode 100644 index 00000000..490ade41 --- /dev/null +++ b/booking-app/app/modification/layout.tsx @@ -0,0 +1,13 @@ +// app/modification/layout.tsx +import BookingForm from "@/components/src/client/routes/booking/BookingForm"; +import React from "react"; + +type LayoutProps = { + children: React.ReactNode; +}; + +const BookingLayout: React.FC = ({ children }) => ( + {children} +); + +export default BookingLayout; diff --git a/booking-app/app/modification/selectRoom/[id]/page.tsx b/booking-app/app/modification/selectRoom/[id]/page.tsx new file mode 100644 index 00000000..2acf0093 --- /dev/null +++ b/booking-app/app/modification/selectRoom/[id]/page.tsx @@ -0,0 +1,16 @@ +// app/modification/selectRoom/[id].tsx + +"use client"; + +import { FormContextLevel } from "@/components/src/types"; +import React from "react"; +import SelectRoomPage from "@/components/src/client/routes/booking/formPages/SelectRoomPage"; + +const SelectRoom: React.FC = ({ params }: { params: { id: string } }) => ( + +); + +export default SelectRoom; diff --git a/booking-app/components/src/client/routes/modification/ModificationLandingPage.tsx b/booking-app/components/src/client/routes/modification/ModificationLandingPage.tsx new file mode 100644 index 00000000..104286b8 --- /dev/null +++ b/booking-app/components/src/client/routes/modification/ModificationLandingPage.tsx @@ -0,0 +1,81 @@ +"use client"; + +import { Box, List, ListItem, Typography } from "@mui/material"; + +import Button from "@mui/material/Button"; +import React from "react"; +import { styled } from "@mui/system"; +import { useRouter } from "next/navigation"; + +const Center = styled(Box)` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +const Modal = styled(Center)(({ theme }) => ({ + border: `1px solid ${theme.palette.custom.border}`, + borderRadius: 4, + alignItems: "flex-start", + marginTop: 20, + maxWidth: 800, +})); + +const Title = styled(Typography)` + font-weight: 700; + font-size: 20px; + line-height: 1.25; + margin-bottom: 12px; +`; + +const Bulleted = styled(List)` + list-style-type: disc; + padding: 0px 0px 0px 32px; + li { + display: list-item; + padding: 0; + } +`; + +interface Props { + calendarEventId: string; +} + +export default function ModificationLandingPage({ calendarEventId }: Props) { + const router = useRouter(); + + return ( +
+ 370🅙 Media Commons Reservation Form + + + Policy reminders for modifying a reservation + + + Reservation Modification Policy + +

You may modify

+ + Reservation end time + Selected rooms + Equipment checkout + Number of expected attendees + + +
+
+ ); +} From c72e8c2fd5305315d2a5d9c1794fe4d76639f8fd Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Wed, 25 Sep 2024 22:30:02 -0400 Subject: [PATCH 07/16] modification form progress --- .../booking/components/BookingStatusBar.tsx | 21 ++++++++----- .../booking/components/CalendarDatePicker.tsx | 25 +++++++++++----- .../components/CalendarVerticalResource.tsx | 3 +- .../routes/booking/components/Header.tsx | 30 ++++++++++++++----- .../routes/booking/components/Stepper.tsx | 12 ++++++-- .../booking/formPages/SelectRoomPage.tsx | 5 +++- .../booking/hooks/useCalculateOverlap.tsx | 6 ++-- 7 files changed, 72 insertions(+), 30 deletions(-) diff --git a/booking-app/components/src/client/routes/booking/components/BookingStatusBar.tsx b/booking-app/components/src/client/routes/booking/components/BookingStatusBar.tsx index e2a2916b..a4c757d2 100644 --- a/booking-app/components/src/client/routes/booking/components/BookingStatusBar.tsx +++ b/booking-app/components/src/client/routes/booking/components/BookingStatusBar.tsx @@ -11,6 +11,7 @@ import { usePathname } from "next/navigation"; interface Props { goBack: () => void; goNext: () => void; + hideBackButton: boolean; hideNextButton: boolean; } @@ -25,7 +26,9 @@ export default function BookingStatusBar(props: Props) { const showAlert = isBanned || needsSafetyTraining || - (bookingCalendarInfo != null && selectedRooms.length > 0); + (bookingCalendarInfo != null && + selectedRooms.length > 0 && + !pathname.includes("/modification")); // order of precedence matters // unfixable blockers > fixable blockers > non-blockers @@ -122,13 +125,15 @@ export default function BookingStatusBar(props: Props) { paddingRight="18px" > - + {!props.hideBackButton && ( + + )} {showAlert && ( diff --git a/booking-app/components/src/client/routes/booking/components/CalendarDatePicker.tsx b/booking-app/components/src/client/routes/booking/components/CalendarDatePicker.tsx index 6fa7d96b..4c8d1b23 100644 --- a/booking-app/components/src/client/routes/booking/components/CalendarDatePicker.tsx +++ b/booking-app/components/src/client/routes/booking/components/CalendarDatePicker.tsx @@ -1,11 +1,17 @@ -import { DateCalendar, LocalizationProvider } from '@mui/x-date-pickers'; -import React, { useContext, useEffect, useState } from 'react'; -import dayjs, { Dayjs } from 'dayjs'; +import { DateCalendar, LocalizationProvider } from "@mui/x-date-pickers"; +import React, { useContext, useEffect, useState } from "react"; +import dayjs, { Dayjs } from "dayjs"; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { BookingContext } from '../bookingProvider'; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { BookingContext } from "../bookingProvider"; +import { FormContextLevel } from "@/components/src/types"; -export const CalendarDatePicker = ({ handleChange }) => { +interface Props { + handleChange: (x: Date) => void; + formContext: FormContextLevel; +} + +export const CalendarDatePicker = ({ handleChange, formContext }: Props) => { const [date, setDate] = useState(dayjs(new Date())); const { bookingCalendarInfo } = useContext(BookingContext); @@ -21,14 +27,19 @@ export const CalendarDatePicker = ({ handleChange }) => { } }, []); + if (formContext === FormContextLevel.WALK_IN) { + return
; + } + return ( diff --git a/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx b/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx index 0d2e7901..f25e0f1d 100644 --- a/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx +++ b/booking-app/components/src/client/routes/booking/components/CalendarVerticalResource.tsx @@ -196,7 +196,8 @@ export default function CalendarVerticalResource({ // for editing an existing reservation const existingCalEventsFiltered = useMemo(() => { if ( - formContext !== FormContextLevel.EDIT || + (formContext !== FormContextLevel.EDIT && + formContext !== FormContextLevel.MODIFICATION) || calendarEventId == null || calendarEventId.length === 0 ) diff --git a/booking-app/components/src/client/routes/booking/components/Header.tsx b/booking-app/components/src/client/routes/booking/components/Header.tsx index 05372a47..98f5d67b 100644 --- a/booking-app/components/src/client/routes/booking/components/Header.tsx +++ b/booking-app/components/src/client/routes/booking/components/Header.tsx @@ -25,20 +25,24 @@ export const Header = () => { threshold: 100, }); - // /book, /walk-in, /edit/ - if (/^\/(book|walk-in|edit\/[^\/]+)$/.test(pathname)) { + // /book, /walk-in, /edit/, /modification/ + if (/^\/(book|walk-in|(?:edit|modification)\/[^\/]+)$/.test(pathname)) { return null; } const goBack = (() => { const match = pathname.match( - /^(\/(book|walk-in|edit))\/(selectRoom|form)(\/[a-zA-Z0-9_-]+)?$/ + /^(\/(book|walk-in|edit|modification))\/(selectRoom|form)(\/[a-zA-Z0-9_-]+)?$/ ); if (match) { const [, basePath, , step, idSegment] = match; const id = idSegment || ""; // If there's an ID, use it; otherwise, use an empty string. + if (basePath === "/modification" && step === "selectRoom") { + return () => {}; + } + switch (step) { case "selectRoom": return () => router.push(`${basePath}/role${id}`); @@ -54,7 +58,7 @@ export const Header = () => { const goNext = (() => { const match = pathname.match( - /^(\/(book|walk-in|edit))\/(selectRoom)(\/[a-zA-Z0-9_-]+)?$/ + /^(\/(book|walk-in|edit|modification))\/(selectRoom)(\/[a-zA-Z0-9_-]+)?$/ ); if (match) { @@ -72,13 +76,21 @@ export const Header = () => { } })(); - // /book/form, /walk-in/form, /edit/form/ + const hideBackButton = + /^(\/modification)\/selectRoom(\/[a-zA-Z0-9_-]+)?$/.test(pathname); + + // /book/form, /walk-in/form, /edit/form/, /modification/form/ const hideNextButton = - /^(\/(book|walk-in|edit))\/form(\/[a-zA-Z0-9_-]+)?$/.test(pathname); + /^(\/(book|walk-in|(?:edit|modification)))\/form(\/[a-zA-Z0-9_-]+)?$/.test( + pathname + ); // /book/selectRoom, /book/form, /walk-in/selectRoom, /walk-in/form, /edit/selectRoom/, /edit/form/ + // /modification/selectRoom/, /modification/form/ const showStatusBar = - /^(\/(book|walk-in|edit))\/(selectRoom|form)(\/[^\/]+)?$/.test(pathname); + /^(\/(book|walk-in|(?:edit|modification)))\/(selectRoom|form)(\/[^\/]+)?$/.test( + pathname + ); return ( {
{showStatusBar && ( - + )}
diff --git a/booking-app/components/src/client/routes/booking/components/Stepper.tsx b/booking-app/components/src/client/routes/booking/components/Stepper.tsx index 3e7b30a3..5f45562c 100644 --- a/booking-app/components/src/client/routes/booking/components/Stepper.tsx +++ b/booking-app/components/src/client/routes/booking/components/Stepper.tsx @@ -1,14 +1,20 @@ import { Box, Step, StepLabel, Stepper } from "@mui/material"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { usePathname } from "next/navigation"; -const steps = ["Affiliation", "Select Time", "Details", "Confirmation"]; - export default function BookingFormStepper() { const pathname = usePathname(); const [activeStep, setActiveStep] = useState(0); + const steps = useMemo(() => { + if (pathname.includes("/modification")) { + return ["Select Time", "Details", "Confirmation"]; + } else { + return ["Affiliation", "Select Time", "Details", "Confirmation"]; + } + }, [pathname]); + useEffect(() => { switch (pathname) { case "/walk-in/role": diff --git a/booking-app/components/src/client/routes/booking/formPages/SelectRoomPage.tsx b/booking-app/components/src/client/routes/booking/formPages/SelectRoomPage.tsx index bc5c094f..da37bf92 100644 --- a/booking-app/components/src/client/routes/booking/formPages/SelectRoomPage.tsx +++ b/booking-app/components/src/client/routes/booking/formPages/SelectRoomPage.tsx @@ -45,7 +45,10 @@ export default function SelectRoomPage({ spacing={{ xs: 0, md: 2 }} alignItems={{ xs: "center", md: "unset" }} > - {!isWalkIn && } + Spaces { if (bookingCalendarInfo == null) return false; - // check if /edit path and pull calendarEventId if true - const match = pathname.match(/^\/edit\/selectRoom\/([a-zA-Z0-9_-]+)$/); + // check if /edit or /modification path and pull calendarEventId if true + const match = pathname.match( + /^\/(?:edit|modification)\/selectRoom\/([a-zA-Z0-9_-]+)$/ + ); let calendarEventId: string; if (match) { calendarEventId = match[1]; From 4c474c23f0bd00e0e21d8d0b42a33424e618a6db Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Wed, 25 Sep 2024 23:06:39 -0400 Subject: [PATCH 08/16] booking form header refactor --- booking-app/app/book/confirmation/page.tsx | 7 +- booking-app/app/book/form/page.tsx | 7 +- booking-app/app/book/layout.tsx | 14 ++++ booking-app/app/book/role/page.tsx | 9 +-- booking-app/app/book/selectRoom/page.tsx | 9 +-- booking-app/app/edit/layout.tsx | 3 +- booking-app/app/walk-in/layout.tsx | 3 +- .../admin/components/BookingActions.tsx | 3 + .../src/client/routes/booking/BookingForm.tsx | 11 ++- .../routes/booking/components/Header.tsx | 67 +++++++------------ .../routes/booking/components/Stepper.tsx | 24 ++++--- booking-app/components/src/types.ts | 9 ++- 12 files changed, 78 insertions(+), 88 deletions(-) create mode 100644 booking-app/app/book/layout.tsx diff --git a/booking-app/app/book/confirmation/page.tsx b/booking-app/app/book/confirmation/page.tsx index 4fcf5261..da2bd04e 100644 --- a/booking-app/app/book/confirmation/page.tsx +++ b/booking-app/app/book/confirmation/page.tsx @@ -1,12 +1,7 @@ // app/book/confirmation/page.tsx -import BookingForm from "@/components/src/client/routes/booking/BookingForm"; import BookingFormConfirmationPage from "@/components/src/client/routes/booking/formPages/BookingFormConfirmationPage"; import React from "react"; -const Role: React.FC = () => ( - - - -); +const Role: React.FC = () => ; export default Role; diff --git a/booking-app/app/book/form/page.tsx b/booking-app/app/book/form/page.tsx index e4c4e625..3d049666 100644 --- a/booking-app/app/book/form/page.tsx +++ b/booking-app/app/book/form/page.tsx @@ -2,14 +2,9 @@ "use client"; -import BookingForm from "@/components/src/client/routes/booking/BookingForm"; import BookingFormDetailsPage from "@/components/src/client/routes/booking/formPages/BookingFormDetailsPage"; import React from "react"; -const Form: React.FC = () => ( - - - -); +const Form: React.FC = () => ; export default Form; diff --git a/booking-app/app/book/layout.tsx b/booking-app/app/book/layout.tsx new file mode 100644 index 00000000..4b76ee25 --- /dev/null +++ b/booking-app/app/book/layout.tsx @@ -0,0 +1,14 @@ +// app/book/layout.tsx +import BookingForm from "@/components/src/client/routes/booking/BookingForm"; +import { FormContextLevel } from "@/components/src/types"; +import React from "react"; + +type LayoutProps = { + children: React.ReactNode; +}; + +const BookingLayout: React.FC = ({ children }) => ( + {children} +); + +export default BookingLayout; diff --git a/booking-app/app/book/role/page.tsx b/booking-app/app/book/role/page.tsx index 22a2ecd8..79987ee5 100644 --- a/booking-app/app/book/role/page.tsx +++ b/booking-app/app/book/role/page.tsx @@ -1,12 +1,7 @@ +import React from "react"; // app/book/form/page.tsx -import BookingForm from "@/components/src/client/routes/booking/BookingForm"; import UserRolePage from "@/components/src/client/routes/booking/formPages/UserRolePage"; -import React from "react"; -const Role: React.FC = () => ( - - - -); +const Role: React.FC = () => ; export default Role; diff --git a/booking-app/app/book/selectRoom/page.tsx b/booking-app/app/book/selectRoom/page.tsx index 62d64c06..91521adc 100644 --- a/booking-app/app/book/selectRoom/page.tsx +++ b/booking-app/app/book/selectRoom/page.tsx @@ -2,14 +2,9 @@ "use client"; -import BookingForm from "@/components/src/client/routes/booking/BookingForm"; -import SelectRoomPage from "@/components/src/client/routes/booking/formPages/SelectRoomPage"; import React from "react"; +import SelectRoomPage from "@/components/src/client/routes/booking/formPages/SelectRoomPage"; -const SelectRoom: React.FC = () => ( - - - -); +const SelectRoom: React.FC = () => ; export default SelectRoom; diff --git a/booking-app/app/edit/layout.tsx b/booking-app/app/edit/layout.tsx index 8617f7a7..bca0fb3b 100644 --- a/booking-app/app/edit/layout.tsx +++ b/booking-app/app/edit/layout.tsx @@ -1,5 +1,6 @@ // app/edit/layout.tsx import BookingForm from "@/components/src/client/routes/booking/BookingForm"; +import { FormContextLevel } from "@/components/src/types"; import React from "react"; type LayoutProps = { @@ -7,7 +8,7 @@ type LayoutProps = { }; const BookingLayout: React.FC = ({ children }) => ( - {children} + {children} ); export default BookingLayout; diff --git a/booking-app/app/walk-in/layout.tsx b/booking-app/app/walk-in/layout.tsx index 3d06f4a3..99210e04 100644 --- a/booking-app/app/walk-in/layout.tsx +++ b/booking-app/app/walk-in/layout.tsx @@ -1,5 +1,6 @@ // app/walk-in/layout.tsx import BookingForm from "@/components/src/client/routes/booking/BookingForm"; +import { FormContextLevel } from "@/components/src/types"; import React from "react"; type LayoutProps = { @@ -7,7 +8,7 @@ type LayoutProps = { }; const BookingLayout: React.FC = ({ children }) => ( - {children} + {children} ); export default BookingLayout; 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 f10d1a61..bfd894bd 100644 --- a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx +++ b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx @@ -11,6 +11,7 @@ import { } from "@/components/src/server/db"; 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"; @@ -55,6 +56,7 @@ export default function BookingActions({ Actions.PLACEHOLDER ); const { reloadBookings, reloadBookingStatuses } = useContext(DatabaseContext); + const { reloadExistingCalendarEvents } = useContext(BookingContext); const [showError, setShowError] = useState(false); const router = useRouter(); const loadExistingBookingData = useExistingBooking(); @@ -143,6 +145,7 @@ export default function BookingActions({ [Actions.EDIT]: { action: async () => { loadExistingBookingData(calendarEventId); + reloadExistingCalendarEvents(); router.push("/edit/" + calendarEventId); }, optimisticNextStatus: status, diff --git a/booking-app/components/src/client/routes/booking/BookingForm.tsx b/booking-app/components/src/client/routes/booking/BookingForm.tsx index f3c2b39c..19de8aa4 100644 --- a/booking-app/components/src/client/routes/booking/BookingForm.tsx +++ b/booking-app/components/src/client/routes/booking/BookingForm.tsx @@ -1,12 +1,19 @@ "use client"; + import React, { useEffect } from "react"; + +import { FormContextLevel } from "@/components/src/types"; import { Header } from "./components/Header"; type BookingFormProps = { children: React.ReactNode; + formContext?: FormContextLevel; }; -export default function BookingForm({ children }: BookingFormProps) { +export default function BookingForm({ + children, + formContext = FormContextLevel.FULL_FORM, +}: BookingFormProps) { useEffect(() => { console.log( "DEPLOY MODE ENVIRONMENT:", @@ -44,7 +51,7 @@ export default function BookingForm({ children }: BookingFormProps) { return (
-
+
{children}
); diff --git a/booking-app/components/src/client/routes/booking/components/Header.tsx b/booking-app/components/src/client/routes/booking/components/Header.tsx index 05372a47..21f40cd5 100644 --- a/booking-app/components/src/client/routes/booking/components/Header.tsx +++ b/booking-app/components/src/client/routes/booking/components/Header.tsx @@ -1,9 +1,10 @@ import { Box, useScrollTrigger } from "@mui/material"; +import React, { useMemo } from "react"; import { usePathname, useRouter } from "next/navigation"; import BookingFormStepper from "./Stepper"; import BookingStatusBar from "./BookingStatusBar"; -import React from "react"; +import { FormContextLevel } from "@/components/src/types"; import { styled } from "@mui/system"; const StickyScroll = styled(Box)` @@ -16,7 +17,11 @@ const StickyScroll = styled(Box)` transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; `; -export const Header = () => { +interface Props { + formContext: FormContextLevel; +} + +export const Header = ({ formContext }: Props) => { const router = useRouter(); const pathname = usePathname(); @@ -25,60 +30,38 @@ export const Header = () => { threshold: 100, }); + // don't show on form landing pages // /book, /walk-in, /edit/ if (/^\/(book|walk-in|edit\/[^\/]+)$/.test(pathname)) { return null; } const goBack = (() => { - const match = pathname.match( - /^(\/(book|walk-in|edit))\/(selectRoom|form)(\/[a-zA-Z0-9_-]+)?$/ - ); - - if (match) { - const [, basePath, , step, idSegment] = match; - const id = idSegment || ""; // If there's an ID, use it; otherwise, use an empty string. + const step = pathname.split("/")[2]; // Get the step + const idSegment = pathname.split("/")[3] || ""; // Get the id if it exists - switch (step) { - case "selectRoom": - return () => router.push(`${basePath}/role${id}`); - case "form": - return () => router.push(`${basePath}/selectRoom${id}`); - default: - return () => {}; - } - } else { - return () => {}; + switch (step) { + case "selectRoom": + return () => router.push(`${formContext}/role/${idSegment}`); + case "form": + return () => router.push(`${formContext}/selectRoom/${idSegment}`); + default: + return () => {}; } })(); const goNext = (() => { - const match = pathname.match( - /^(\/(book|walk-in|edit))\/(selectRoom)(\/[a-zA-Z0-9_-]+)?$/ - ); + const step = pathname.split("/")[2]; // Get the step + const idSegment = pathname.split("/")[3] || ""; // Get the id segment if it exists - if (match) { - const [, basePath, , step, idSegment] = match; - const id = idSegment || ""; // If there's an ID, use it; otherwise, use an empty string. - - switch (step) { - case "selectRoom": - return () => router.push(`${basePath}/form${id}`); - default: - return () => {}; - } - } else { - return () => {}; + if (step === "selectRoom") { + return () => router.push(`${formContext}/form/${idSegment}`); } + return () => {}; })(); - // /book/form, /walk-in/form, /edit/form/ - const hideNextButton = - /^(\/(book|walk-in|edit))\/form(\/[a-zA-Z0-9_-]+)?$/.test(pathname); - - // /book/selectRoom, /book/form, /walk-in/selectRoom, /walk-in/form, /edit/selectRoom/, /edit/form/ - const showStatusBar = - /^(\/(book|walk-in|edit))\/(selectRoom|form)(\/[^\/]+)?$/.test(pathname); + const hideNextButton = pathname.includes("/form"); + const showStatusBar = pathname.match(/\/(selectRoom|form)/); return ( { } >
- + {showStatusBar && ( )} diff --git a/booking-app/components/src/client/routes/booking/components/Stepper.tsx b/booking-app/components/src/client/routes/booking/components/Stepper.tsx index 3e7b30a3..0f5f5dcd 100644 --- a/booking-app/components/src/client/routes/booking/components/Stepper.tsx +++ b/booking-app/components/src/client/routes/booking/components/Stepper.tsx @@ -1,36 +1,38 @@ import { Box, Step, StepLabel, Stepper } from "@mui/material"; import React, { useEffect, useState } from "react"; +import { FormContextLevel } from "@/components/src/types"; import { usePathname } from "next/navigation"; const steps = ["Affiliation", "Select Time", "Details", "Confirmation"]; -export default function BookingFormStepper() { +interface Props { + formContext: FormContextLevel; +} + +export default function BookingFormStepper({ formContext }: Props) { const pathname = usePathname(); const [activeStep, setActiveStep] = useState(0); useEffect(() => { - switch (pathname) { - case "/walk-in/role": - case "/book/role": + const step = pathname.split("/")[2]; // role, selectRoom, form + switch (step) { + case "role": setActiveStep(0); break; - case "/walk-in/selectRoom": - case "/book/selectRoom": + case "selectRoom": setActiveStep(1); break; - case "/walk-in/form": - case "/book/form": + case "form": setActiveStep(2); break; - case "/walk-in/confirmation": - case "/book/confirmation": + case "confirmation": setActiveStep(3); break; default: setActiveStep(0); } - }, [pathname]); + }, [pathname, formContext]); return ( diff --git a/booking-app/components/src/types.ts b/booking-app/components/src/types.ts index 55d6e3c6..ed195f43 100644 --- a/booking-app/components/src/types.ts +++ b/booking-app/components/src/types.ts @@ -96,12 +96,11 @@ export enum Department { export type DevBranch = "development" | "staging" | "production" | ""; // what context are we entering the form in? -// increasing levels of edit power export enum FormContextLevel { - MODIFICATION = 0, - WALK_IN = 1, - EDIT = 2, - FULL_FORM = 99, + EDIT = "/edit", + FULL_FORM = "/book", + MODIFICATION = "/modification", + WALK_IN = "/walk-in", } export type Inputs = { From 495efdfa28201ce531cc7ef48cc881433da10e77 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Wed, 25 Sep 2024 23:29:54 -0400 Subject: [PATCH 09/16] stepper for modifications --- booking-app/app/modification/layout.tsx | 5 ++- .../src/client/routes/booking/BookingForm.tsx | 4 +- .../routes/booking/components/Header.tsx | 1 + .../routes/booking/components/Stepper.tsx | 40 ++++++++++--------- .../booking/hooks/useCheckFormMissingData.tsx | 5 ++- 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/booking-app/app/modification/layout.tsx b/booking-app/app/modification/layout.tsx index 490ade41..61b1fc17 100644 --- a/booking-app/app/modification/layout.tsx +++ b/booking-app/app/modification/layout.tsx @@ -1,5 +1,6 @@ // app/modification/layout.tsx import BookingForm from "@/components/src/client/routes/booking/BookingForm"; +import { FormContextLevel } from "@/components/src/types"; import React from "react"; type LayoutProps = { @@ -7,7 +8,9 @@ type LayoutProps = { }; const BookingLayout: React.FC = ({ children }) => ( - {children} + + {children} + ); export default BookingLayout; diff --git a/booking-app/components/src/client/routes/booking/BookingForm.tsx b/booking-app/components/src/client/routes/booking/BookingForm.tsx index 19de8aa4..3b02978a 100644 --- a/booking-app/components/src/client/routes/booking/BookingForm.tsx +++ b/booking-app/components/src/client/routes/booking/BookingForm.tsx @@ -7,12 +7,12 @@ import { Header } from "./components/Header"; type BookingFormProps = { children: React.ReactNode; - formContext?: FormContextLevel; + formContext: FormContextLevel; }; export default function BookingForm({ children, - formContext = FormContextLevel.FULL_FORM, + formContext, }: BookingFormProps) { useEffect(() => { console.log( diff --git a/booking-app/components/src/client/routes/booking/components/Header.tsx b/booking-app/components/src/client/routes/booking/components/Header.tsx index c945d298..807fbd19 100644 --- a/booking-app/components/src/client/routes/booking/components/Header.tsx +++ b/booking-app/components/src/client/routes/booking/components/Header.tsx @@ -41,6 +41,7 @@ export const Header = ({ formContext }: Props) => { switch (step) { case "selectRoom": + if (formContext === FormContextLevel.MODIFICATION) return () => {}; return () => router.push(`${formContext}/role/${idSegment}`); case "form": return () => router.push(`${formContext}/selectRoom/${idSegment}`); diff --git a/booking-app/components/src/client/routes/booking/components/Stepper.tsx b/booking-app/components/src/client/routes/booking/components/Stepper.tsx index e42a0233..918c5f2f 100644 --- a/booking-app/components/src/client/routes/booking/components/Stepper.tsx +++ b/booking-app/components/src/client/routes/booking/components/Stepper.tsx @@ -8,36 +8,38 @@ interface Props { formContext: FormContextLevel; } +const routeToStepNames = { + role: "Affiliation", + selectRoom: "Select Time", + form: "Details", + confirmation: "Confirmation", +}; + export default function BookingFormStepper({ formContext }: Props) { const pathname = usePathname(); const [activeStep, setActiveStep] = useState(0); const steps = useMemo(() => { - if (pathname.includes("/modification")) { - return ["Select Time", "Details", "Confirmation"]; + if (formContext === FormContextLevel.MODIFICATION) { + return [ + routeToStepNames.selectRoom, + routeToStepNames.form, + routeToStepNames.confirmation, + ]; } else { - return ["Affiliation", "Select Time", "Details", "Confirmation"]; + return [ + routeToStepNames.role, + routeToStepNames.selectRoom, + routeToStepNames.form, + routeToStepNames.confirmation, + ]; } }, [pathname]); useEffect(() => { const step = pathname.split("/")[2]; // role, selectRoom, form - switch (step) { - case "role": - setActiveStep(0); - break; - case "selectRoom": - setActiveStep(1); - break; - case "form": - setActiveStep(2); - break; - case "confirmation": - setActiveStep(3); - break; - default: - setActiveStep(0); - } + const index = steps.indexOf(routeToStepNames[step]) ?? 0; + setActiveStep(index); }, [pathname, formContext]); return ( 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 9f4317c2..9fc25f7f 100644 --- a/booking-app/components/src/client/routes/booking/hooks/useCheckFormMissingData.tsx +++ b/booking-app/components/src/client/routes/booking/hooks/useCheckFormMissingData.tsx @@ -34,7 +34,10 @@ export default function useCheckFormMissingData() { } if (isMissing) { - router.push("/" + pathname.split("/")[1]); + const base = pathname.split("/")[1]; + const id = pathname.split("/")[3] ?? ""; + console.log("MISSING ID", id, pathname); + router.push(`/${base}/${id}`); } }, []); } From 9b848a7549008692b8d4d8d5ebd9ca269d5972ae Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Wed, 25 Sep 2024 23:43:35 -0400 Subject: [PATCH 10/16] hide auto approval msgs in modification mode --- .../booking/components/BookingStatusBar.tsx | 66 ++++++++++--------- .../components/CalendarVerticalResource.tsx | 10 ++- .../routes/booking/components/Header.tsx | 2 +- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/booking-app/components/src/client/routes/booking/components/BookingStatusBar.tsx b/booking-app/components/src/client/routes/booking/components/BookingStatusBar.tsx index a4c757d2..92e72998 100644 --- a/booking-app/components/src/client/routes/booking/components/BookingStatusBar.tsx +++ b/booking-app/components/src/client/routes/booking/components/BookingStatusBar.tsx @@ -3,21 +3,22 @@ import { Check, ChevronLeft, ChevronRight } from "@mui/icons-material"; import React, { useContext } from "react"; import { BookingContext } from "../bookingProvider"; +import { FormContextLevel } from "@/components/src/types"; import Grid from "@mui/material/Unstable_Grid2"; import useCalculateOverlap from "../hooks/useCalculateOverlap"; import useCheckAutoApproval from "../hooks/useCheckAutoApproval"; import { usePathname } from "next/navigation"; interface Props { + formContext: FormContextLevel; goBack: () => void; goNext: () => void; hideBackButton: boolean; hideNextButton: boolean; } -export default function BookingStatusBar(props: Props) { - const pathname = usePathname(); - const isWalkIn = pathname.includes("/walk-in"); +export default function BookingStatusBar({ formContext, ...props }: Props) { + const isWalkIn = formContext === FormContextLevel.WALK_IN; const { isAutoApproval, errorMessage } = useCheckAutoApproval(isWalkIn); const { bookingCalendarInfo, selectedRooms, isBanned, needsSafetyTraining } = useContext(BookingContext); @@ -26,20 +27,20 @@ export default function BookingStatusBar(props: Props) { const showAlert = isBanned || needsSafetyTraining || - (bookingCalendarInfo != null && - selectedRooms.length > 0 && - !pathname.includes("/modification")); + (bookingCalendarInfo != null && selectedRooms.length > 0); // order of precedence matters // unfixable blockers > fixable blockers > non-blockers - const state: { - message: React.ReactNode; - severity: AlertColor; - icon?: React.ReactNode; - variant?: "filled" | "standard" | "outlined"; - btnDisabled: boolean; - btnDisabledMessage?: string; - } = (() => { + const state: + | { + message: React.ReactNode; + severity: AlertColor; + icon?: React.ReactNode; + variant?: "filled" | "standard" | "outlined"; + btnDisabled: boolean; + btnDisabledMessage?: string; + } + | undefined = (() => { if (isBanned) return { btnDisabled: true, @@ -82,7 +83,7 @@ export default function BookingStatusBar(props: Props) { severity: "error", }; } - if (isAutoApproval) + if (isAutoApproval && formContext !== FormContextLevel.MODIFICATION) return { btnDisabled: false, btnDisabledMessage: null, @@ -90,24 +91,27 @@ export default function BookingStatusBar(props: Props) { severity: "success", icon: , }; - - return { - btnDisabled: false, - btnDisabledMessage: null, - message: ( -

- This request will require approval.{" "} - - Why? - -

- ), - severity: "warning", - }; + else if (formContext !== FormContextLevel.MODIFICATION) + return { + btnDisabled: false, + btnDisabledMessage: null, + message: ( +

+ This request will require approval.{" "} + + Why? + +

+ ), + severity: "warning", + }; + else { + return undefined; + } })(); const [disabled, disabledMessage] = (() => { - if (state.btnDisabled) { + if (state?.btnDisabled ?? false) { return [true, state.btnDisabledMessage]; } if (bookingCalendarInfo == null) { @@ -136,7 +140,7 @@ export default function BookingStatusBar(props: Props) { )} - {showAlert && ( + {showAlert && state && ( { - if (info.event.title.includes(NEW_TITLE_TAG)) { + if ( + info.event.title.includes(NEW_TITLE_TAG) && + formContext !== FormContextLevel.MODIFICATION + ) { setBookingCalendarInfo(null); } }; @@ -230,7 +234,7 @@ export default function CalendarVerticalResource({ googleCalendarPlugin, interactionPlugin, ]} - selectable={true} + selectable={formContext !== FormContextLevel.MODIFICATION} select={handleEventSelect} selectAllow={handleEventSelecting} selectOverlap={handleSelectOverlap} diff --git a/booking-app/components/src/client/routes/booking/components/Header.tsx b/booking-app/components/src/client/routes/booking/components/Header.tsx index 807fbd19..5d32a81b 100644 --- a/booking-app/components/src/client/routes/booking/components/Header.tsx +++ b/booking-app/components/src/client/routes/booking/components/Header.tsx @@ -78,7 +78,7 @@ export const Header = ({ formContext }: Props) => { {showStatusBar && ( )}
From b4ada6bd2865c99f6bec47519b2eb74e209886f1 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:01:10 -0400 Subject: [PATCH 11/16] submit modification --- booking-app/app/api/bookings/route.ts | 29 +- .../admin/components/BookingActions.tsx | 1 + .../components/BookingFormMediaServices.tsx | 9 +- .../routes/booking/components/FormInput.tsx | 705 ++++++++++-------- .../routes/booking/hooks/useSubmitBooking.tsx | 63 +- 5 files changed, 458 insertions(+), 349 deletions(-) diff --git a/booking-app/app/api/bookings/route.ts b/booking-app/app/api/bookings/route.ts index 468ccb85..0a854178 100644 --- a/booking-app/app/api/bookings/route.ts +++ b/booking-app/app/api/bookings/route.ts @@ -9,26 +9,27 @@ import { declineUrl, getBookingToolDeployUrl, } from "@/components/src/server/ui"; +import { deleteEvent, insertEvent } from "@/components/src/server/calendars"; import { firstApproverEmails, serverApproveInstantBooking, + serverBookingContents, serverDeleteDataByCalendarEventId, serverUpdateDataByCalendarEventId, } from "@/components/src/server/admin"; -import { deleteEvent, insertEvent } from "@/components/src/server/calendars"; +import { + serverFormatDate, + toFirebaseTimestampFromString, +} from "@/components/src/client/utils/serverDate"; +import { + serverGetNextSequentialId, + serverSaveDataToFirestore, +} from "@/lib/firebase/server/adminDb"; import { DateSelectArg } from "fullcalendar"; import { TableNames } from "@/components/src/policy"; import { Timestamp } from "firebase-admin/firestore"; import { sendHTMLEmail } from "@/app/lib/sendHTMLEmail"; -import { - serverGetNextSequentialId, - serverSaveDataToFirestore, -} from "@/lib/firebase/server/adminDb"; -import { - serverFormatDate, - toFirebaseTimestampFromString, -} from "@/components/src/client/utils/serverDate"; async function createBookingCalendarEvent( selectedRooms: RoomSetting[], @@ -188,6 +189,7 @@ export async function PUT(request: NextRequest) { const { email, selectedRooms, + allRooms, bookingCalendarInfo, data, isAutoApproval, @@ -200,13 +202,20 @@ export async function PUT(request: NextRequest) { { status: 500 }, ); } + + const existingContents = await serverBookingContents(calendarEventId); + const oldRoomIds = existingContents.roomId.split(",").map(x => x.trim()); + const oldRooms = allRooms.filter((room: RoomSetting) => + oldRoomIds.includes(room.roomId + ""), + ); + const selectedRoomIds = selectedRooms .map((r: { roomId: number }) => r.roomId) .join(", "); // delete existing cal events await Promise.all( - selectedRooms.map(async room => { + oldRooms.map(async room => { await deleteEvent(room.calendarId, calendarEventId, room.roomId); }), ); 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 d2bc52ca..3a6a8a7a 100644 --- a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx +++ b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx @@ -163,6 +163,7 @@ export default function BookingActions({ [Actions.MODIFICATION]: { action: async () => { loadExistingBookingData(calendarEventId); + reloadExistingCalendarEvents(); router.push("/modification/" + calendarEventId); }, optimisticNextStatus: status, diff --git a/booking-app/components/src/client/routes/booking/components/BookingFormMediaServices.tsx b/booking-app/components/src/client/routes/booking/components/BookingFormMediaServices.tsx index e759ac8d..58c70d88 100644 --- a/booking-app/components/src/client/routes/booking/components/BookingFormMediaServices.tsx +++ b/booking-app/components/src/client/routes/booking/components/BookingFormMediaServices.tsx @@ -39,6 +39,11 @@ export default function BookingFormMediaServices(props: Props) { const { selectedRooms } = useContext(BookingContext); const roomIds = selectedRooms.map((room) => room.roomId); + const limitedContexts = [ + FormContextLevel.WALK_IN, + FormContextLevel.MODIFICATION, + ]; + const checkboxes = useMemo(() => { const options: MediaServices[] = []; const checkRoomMediaServices = (list: number[]) => @@ -74,7 +79,7 @@ export default function BookingFormMediaServices(props: Props) { if (!e.target.checked) { // de-select boxes if switch says "no media services" field.onChange(""); - } else if (formContext === FormContextLevel.WALK_IN) { + } else if (limitedContexts.includes(formContext)) { field.onChange(MediaServices.CHECKOUT_EQUIPMENT); } @@ -88,7 +93,7 @@ export default function BookingFormMediaServices(props: Props) { > ); - if (formContext === FormContextLevel.WALK_IN) { + if (limitedContexts.includes(formContext)) { return (
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 707d438d..fec43f53 100644 --- a/booking-app/components/src/client/routes/booking/components/FormInput.tsx +++ b/booking-app/components/src/client/routes/booking/components/FormInput.tsx @@ -190,371 +190,418 @@ export default function FormInput({ calendarEventId, formContext }: Props) { router.push("/book/confirmation"); }; - return ( -
- - + const fullFormFields = ( + <> +
+ + + + -
-
- - - - + - + +
- -
+ {watch("role") === "Student" && ( +
+ - {watch("role") === "Student" && ( -
- + - + +
+ )} - -
- )} +
+ + + x.bookingType) + .sort((a, b) => a.localeCompare(b))} + {...{ control, errors, trigger }} + /> + + + Non-NYU guests will need to be sponsored through JRNY. For more + information about visitor, vendor, and affiliate access, + + click here + + . +

+ } + {...{ control, errors, trigger }} + /> +
-
- - - x.bookingType) - .sort((a, b) => a.localeCompare(b))} - {...{ control, errors, trigger }} - /> - - + {!isWalkIn && ( +
+ - Non-NYU guests will need to be sponsored through JRNY. For - more information about visitor, vendor, and affiliate access, + If your reservation is in 233 or 1201 and requires a specific + room setup that is different from the standard configuration, + it is the reservation holder’s responsibility to submit a - click here + work order with CBS - . + .
+ It is also the reservation holder's responsibility to ensure + the room is reset after use.

} {...{ control, errors, trigger }} /> -
- -
- {!isWalkIn && ( -
- - If your reservation is in 233 or 1201 and requires a - specific room setup that is different from the standard - configuration, it is the reservation holder’s - responsibility to submit a - - work order with CBS - - .
- It is also the reservation holder's responsibility to - ensure the room is reset after use. -

- } - {...{ control, errors, trigger }} - /> - {watch("roomSetup") === "yes" && ( - <> - - - - )} -
- )} -
- - {watch("mediaServices") !== undefined && - watch("mediaServices").length > 0 && ( - - If you selected any of the Media Services above, please - describe your needs in detail. -
- If you need to check out equipment, you can check our - inventory and include your request below. (Ie. 2x Small - Mocap Suits) -
-{" "} - - Inventory for Black Box 220 and Ballrooms 221-224 - -
-{" "} - - Inventory for Garage 103 - -
-

- } - {...{ control, errors, trigger }} - /> - )} -
- {!isWalkIn && ( -
- - It is required for the reservation holder to pay and - arrange for CBS cleaning services if the event includes - catering -

- } - required={false} + {watch("roomSetup") === "yes" && ( + <> + - {watch("catering") === "yes" && ( - <> - - - - )} -
- )} - {!isWalkIn && ( -
- - Only for large events with 75+ attendees, and bookings in - The Garage where the Willoughby entrance will be in use. - Once your booking is confirmed, it is your responsibility - to hire Campus Safety for your event. If appropriate, - please coordinate with your departmental Scheduling - Liaison to hire Campus Safety, as there is a fee. - - Click for Campus Safety Form - -

- } + - {watch("hireSecurity") === "yes" && ( - - )} -
+ )} -
- - {!isWalkIn && ( -
- + )} +
+ + {watch("mediaServices") !== undefined && + watch("mediaServices").length > 0 && ( + - {" "} - I confirm receipt of the + If you selected any of the Media Services above, please + describe your needs in detail. +
+ If you need to check out equipment, you can check our + inventory and include your request below. (Ie. 2x Small + Mocap Suits) +
-{" "} - 370J Pre-Event Checklist + Inventory for Black Box 220 and Ballrooms 221-224 - and acknowledge that it is my responsibility to setup - various event services as detailed within the checklist. - While the 370J Operations staff do setup cleaning services - through CBS, they do not facilitate hiring security, - catering, and arranging room setup services. -

- } - /> - - I agree to reset any and all requested rooms and common - spaces to their original state at the end of the event, - including cleaning and furniture return, and will notify - building staff of any problems, damage, or other concerns - affecting the condition and maintenance of the reserved - space. I understand that if I do not reset the room, I will - lose reservation privileges. -

- } - /> - - I have read the +
-{" "} - Booking Policy for 370 Jay Street Shared Spaces + Inventory for Garage 103 - and agree to follow all policies outlined. I understand that - I may lose access to spaces if I break this agreement. +

} + {...{ control, errors, trigger }} + /> + )} +
+ {!isWalkIn && ( +
+ + It is required for the reservation holder to pay and arrange + for CBS cleaning services if the event includes catering +

+ } + required={false} + {...{ control, errors, trigger }} + /> + {watch("catering") === "yes" && ( + <> + + + + )} +
+ )} + {!isWalkIn && ( +
+ + Only for large events with 75+ attendees, and bookings in The + Garage where the Willoughby entrance will be in use. Once your + booking is confirmed, it is your responsibility to hire Campus + Safety for your event. If appropriate, please coordinate with + your departmental Scheduling Liaison to hire Campus Safety, as + there is a fee. + + Click for Campus Safety Form + +

+ } + {...{ control, errors, trigger }} + /> + {watch("hireSecurity") === "yes" && ( + + )} +
+ )} +
+ + {!isWalkIn && ( +
+ + {" "} + I confirm receipt of the + + 370J Pre-Event Checklist + + and acknowledge that it is my responsibility to setup various + event services as detailed within the checklist. While the 370J + Operations staff do setup cleaning services through CBS, they do + not facilitate hiring security, catering, and arranging room + setup services. +

+ } + /> + + I agree to reset any and all requested rooms and common spaces + to their original state at the end of the event, including + cleaning and furniture return, and will notify building staff of + any problems, damage, or other concerns affecting the condition + and maintenance of the reserved space. I understand that if I do + not reset the room, I will lose reservation privileges. +

+ } + /> + + I have read the + + Booking Policy for 370 Jay Street Shared Spaces + + and agree to follow all policies outlined. I understand that I + may lose access to spaces if I break this agreement. +

+ } + /> +
+ )} + + + ); + + const modificationFormFields = ( + <> +
+ +
+
+
+ + {watch("mediaServices") !== undefined && + watch("mediaServices").length > 0 && ( + -
- )} - - + )} +
+ + + + ); + + let formFields = <>; + switch (formContext) { + case FormContextLevel.MODIFICATION: + formFields = modificationFormFields; + break; + default: + formFields = fullFormFields; + } + + return ( +
+ + +
{formFields}
); 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 81c0f108..6526c39d 100644 --- a/booking-app/components/src/client/routes/booking/hooks/useSubmitBooking.tsx +++ b/booking-app/components/src/client/routes/booking/hooks/useSubmitBooking.tsx @@ -1,4 +1,4 @@ -import { FormContextLevel, Inputs } from "../../../../types"; +import { FormContextLevel, Inputs, PagePermission } from "../../../../types"; import { useCallback, useContext } from "react"; import { BookingContext } from "../bookingProvider"; @@ -7,8 +7,14 @@ import { useRouter } from "next/navigation"; export default function useSubmitBooking(formContext: FormContextLevel) { const router = useRouter(); - const { liaisonUsers, userEmail, reloadBookings, reloadBookingStatuses } = - useContext(DatabaseContext); + const { + liaisonUsers, + userEmail, + reloadBookings, + reloadBookingStatuses, + pagePermission, + roomSettings, + } = useContext(DatabaseContext); const { bookingCalendarInfo, department, @@ -23,6 +29,7 @@ export default function useSubmitBooking(formContext: FormContextLevel) { const isEdit = formContext === FormContextLevel.EDIT; const isWalkIn = formContext === FormContextLevel.WALK_IN; + const isModification = formContext === FormContextLevel.MODIFICATION; const registerEvent = useCallback( async (data: Inputs, isAutoApproval: boolean, calendarEventId?: string) => { @@ -46,17 +53,57 @@ export default function useSubmitBooking(formContext: FormContextLevel) { } } + if (isModification && pagePermission === PagePermission.BOOKING) { + // only a PA/admin can do a modification + setSubmitting("error"); + return; + } + let email: string; setSubmitting("submitting"); - if (isWalkIn && data.netId) { + if ((isWalkIn || isModification) && data.netId) { email = data.netId + "@nyu.edu"; } else { email = userEmail || data.missingEmail; } - const endpoint = isWalkIn ? "/api/walkIn" : "/api/bookings"; - fetch(`${process.env.NEXT_PUBLIC_BASE_URL}${endpoint}`, { - method: isWalkIn || !isEdit ? "POST" : "PUT", + const requestParams = ((): { + endpoint: string; + method: "POST" | "PUT"; + body?: Object; + } => { + switch (formContext) { + case FormContextLevel.EDIT: + return { + endpoint: "/api/bookings", + method: "PUT", + body: { calendarEventId, allRooms: roomSettings }, + }; + case FormContextLevel.MODIFICATION: + return { + endpoint: "/api/bookings", + method: "PUT", + body: { + calendarEventId, + isAutoApproval: true, + allRooms: roomSettings, + }, + }; + case FormContextLevel.WALK_IN: + return { + endpoint: "/api/walkIn", + method: "POST", + }; + default: + return { + endpoint: "/api/bookings", + method: "POST", + }; + } + })(); + + fetch(`${process.env.NEXT_PUBLIC_BASE_URL}${requestParams.endpoint}`, { + method: requestParams.method, headers: { "Content-Type": "application/json", }, @@ -67,7 +114,7 @@ export default function useSubmitBooking(formContext: FormContextLevel) { liaisonUsers, data, isAutoApproval, - ...(isEdit && calendarEventId && { calendarEventId }), + ...(requestParams.body ?? {}), }), }) .then((res) => { From 8f8bedde2ed149b7ee053e94a7173245bc151996 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:10:13 -0400 Subject: [PATCH 12/16] fix calendar description text --- .../components/src/server/calendars.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/booking-app/components/src/server/calendars.ts b/booking-app/components/src/server/calendars.ts index 2ea6c0cd..9e6bcd9a 100644 --- a/booking-app/components/src/server/calendars.ts +++ b/booking-app/components/src/server/calendars.ts @@ -69,17 +69,19 @@ const bookingContentsToDescription = (bookingContents: BookingFormDetails) => { listItem("Expected Attendance", bookingContents.expectedAttendance), bookingContents.roomSetup === "yes" && "**" + listItem("Room Setup", bookingContents.setupDetails) + "**", - listItem("Title", bookingContents.title), - bookingContents.mediaServices && - bookingContents.mediaServices.length > 0 && - listItem("Media Services", bookingContents.mediaServices), - bookingContents.mediaServicesDetails.length > 0 && - listItem("Media Services Details", bookingContents.mediaServicesDetails), - (bookingContents.catering === "yes" || - bookingContents.cateringService.length > 0) && - listItem("Catering", bookingContents.cateringService), - bookingContents.hireSecurity === "yes" && - listItem("Hire Security", bookingContents.hireSecurity), + bookingContents.mediaServices && bookingContents.mediaServices.length > 0 + ? listItem("Media Services", bookingContents.mediaServices) + : "", + bookingContents.mediaServicesDetails.length > 0 + ? listItem("Media Services Details", bookingContents.mediaServicesDetails) + : "", + bookingContents.catering === "yes" || + bookingContents.cateringService.length > 0 + ? listItem("Catering", bookingContents.cateringService) + : "", + bookingContents.hireSecurity === "yes" + ? listItem("Hire Security", bookingContents.hireSecurity) + : "", "

Cancellation Policy

", ]; //@ts-ignore From e4c5a52bf6b2e34856b7fe902dbb97b72741aa31 Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Thu, 26 Sep 2024 13:27:57 -0400 Subject: [PATCH 13/16] clean up settings tables --- .../src/client/routes/components/ListTable.tsx | 14 ++++++++++---- .../src/client/routes/components/Table.tsx | 13 ++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/booking-app/components/src/client/routes/components/ListTable.tsx b/booking-app/components/src/client/routes/components/ListTable.tsx index 83bde3e9..40736738 100644 --- a/booking-app/components/src/client/routes/components/ListTable.tsx +++ b/booking-app/components/src/client/routes/components/ListTable.tsx @@ -1,4 +1,4 @@ -import { Box, TableCell } from "@mui/material"; +import { Box, TableCell, styled } from "@mui/material"; import React, { useMemo } from "react"; import ListTableRow from "./ListTableRow"; @@ -15,6 +15,12 @@ interface Props { topRow: React.ReactNode; } +const ListTableWrapper = styled(Table)` + th { + padding-right: 16px; + } +`; + export default function ListTable(props: Props) { const refresh = props.rowsRefresh; const topRow = props.topRow; @@ -24,7 +30,7 @@ export default function ListTable(props: Props) { if (props.rows.length === 0) { return []; } - return Object.keys(props.rows[0]) as string[]; + return Object.keys(props.rows[0]).filter((x) => x !== "id") as string[]; }, [props.rows]); const columns = useMemo( @@ -40,7 +46,7 @@ export default function ListTable(props: Props) { ); return ( - + {props?.rows.map((row, index: number) => ( ))} -
+ ); } diff --git a/booking-app/components/src/client/routes/components/Table.tsx b/booking-app/components/src/client/routes/components/Table.tsx index 1ba3f30f..a4ac8153 100644 --- a/booking-app/components/src/client/routes/components/Table.tsx +++ b/booking-app/components/src/client/routes/components/Table.tsx @@ -59,15 +59,22 @@ export const TableEmpty = styled(Box)` `; interface Props { + className?: string; columns: React.ReactNode[]; children: React.ReactNode[]; topRow: React.ReactNode; sx?: SxProps; } -export default function Table({ columns, children, topRow, sx }: Props) { +export default function Table({ + className, + columns, + children, + topRow, + sx, +}: Props) { return ( - <> + @@ -83,6 +90,6 @@ export default function Table({ columns, children, topRow, sx }: Props) { {children} - + ); } From a2b308101f098f507e230aacbe9e15d2af32c50a Mon Sep 17 00:00:00 2001 From: lucia <51058748+lucia-gomez@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:33:09 -0400 Subject: [PATCH 14/16] export DB as CSV --- booking-app/app/api/bookings/export/route.ts | 57 +++++++++++++++++++ .../admin/components/ExportDatabase.tsx | 56 ++++++++++++++++++ .../routes/admin/components/Settings.tsx | 3 + booking-app/package-lock.json | 36 ++++++++++++ booking-app/package.json | 1 + 5 files changed, 153 insertions(+) create mode 100644 booking-app/app/api/bookings/export/route.ts create mode 100644 booking-app/components/src/client/routes/admin/components/ExportDatabase.tsx diff --git a/booking-app/app/api/bookings/export/route.ts b/booking-app/app/api/bookings/export/route.ts new file mode 100644 index 00000000..717916af --- /dev/null +++ b/booking-app/app/api/bookings/export/route.ts @@ -0,0 +1,57 @@ +import { Booking, BookingStatus } from "@/components/src/types"; +import { NextRequest, NextResponse } from "next/server"; + +import { TableNames } from "@/components/src/policy"; +import { parse } from "json2csv"; +import { serverFetchAllDataFromCollection } from "@/lib/firebase/server/adminDb"; + +export async function GET(request: NextRequest) { + const bookings = await serverFetchAllDataFromCollection( + TableNames.BOOKING, + ); + const statuses = await serverFetchAllDataFromCollection( + TableNames.BOOKING_STATUS, + ); + + // need to find corresponding status row for booking row + const idsToData: { + [key: string]: { + booking: Booking; + status: BookingStatus; + }; + } = {}; + + for (let booking of bookings) { + const calendarEventId = booking.calendarEventId; + const statusMatch = statuses.filter( + row => row.calendarEventId === calendarEventId, + )[0]; + idsToData[calendarEventId] = { + booking, + status: statusMatch, + }; + } + + const rows = Object.entries(idsToData) + .map(([_, { booking, status }]) => { + const { requestNumber, ...otherBooking } = booking; + return { requestNumber, ...otherBooking, ...status }; + }) + .sort((a, b) => a.requestNumber - b.requestNumber); + + try { + const csv = parse(rows); + return new NextResponse(csv, { + status: 200, + headers: { + "Content-Type": "text/csv", + "Content-Disposition": 'attachment; filename="data.csv"', + }, + }); + } catch (error) { + return NextResponse.json( + { error: "Failed to fetch CSV data" }, + { status: 400 }, + ); + } +} diff --git a/booking-app/components/src/client/routes/admin/components/ExportDatabase.tsx b/booking-app/components/src/client/routes/admin/components/ExportDatabase.tsx new file mode 100644 index 00000000..9db181ab --- /dev/null +++ b/booking-app/components/src/client/routes/admin/components/ExportDatabase.tsx @@ -0,0 +1,56 @@ +import { Box, Button, Typography } from "@mui/material"; +import React, { useState } from "react"; + +import AlertToast from "../../components/AlertToast"; + +export default function ExportDatabase() { + const [loading, setLoading] = useState(false); + const [showError, setShowError] = useState(false); + + const onClick = async () => { + setLoading(true); + try { + const response = await fetch("/api/bookings/export"); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(new Blob([blob])); + + // Automatically trigger the download + const link = document.createElement("a"); + link.href = url; + link.download = "data.csv"; + link.click(); + + // Clean up + window.URL.revokeObjectURL(url); + } catch (ex) { + setShowError(true); + console.error("error exporting database", ex); + } finally { + setLoading(false); + } + }; + + return ( + + Export Database +

Export database booking contents as a downloadable CSV file

+ + + + setShowError(false)} + /> +
+ ); +} diff --git a/booking-app/components/src/client/routes/admin/components/Settings.tsx b/booking-app/components/src/client/routes/admin/components/Settings.tsx index fb6e176e..4add3488 100644 --- a/booking-app/components/src/client/routes/admin/components/Settings.tsx +++ b/booking-app/components/src/client/routes/admin/components/Settings.tsx @@ -5,6 +5,7 @@ import { AdminUsers } from "./AdminUsers"; import { BannedUsers } from "./Ban"; import BookingTypes from "./BookingTypes"; import { Departments } from "./Departments"; +import ExportDatabase from "./ExportDatabase"; import FinalApproverSetting from "./PolicySettings"; import Grid from "@mui/material/Unstable_Grid2"; import { Liaisons } from "./Liaisons"; @@ -20,6 +21,7 @@ const tabs = [ { label: "Ban", id: "ban" }, { label: "Booking Types", id: "bookingTypes" }, { label: "Policy Settings", id: "policy" }, + { label: "Export", id: "export" }, ]; export default function Settings() { @@ -49,6 +51,7 @@ export default function Settings() { {tab === "departments" && } {tab === "bookingTypes" && } {tab === "policy" && } + {tab === "export" && } ); diff --git a/booking-app/package-lock.json b/booking-app/package-lock.json index 752525e8..eaf44726 100644 --- a/booking-app/package-lock.json +++ b/booking-app/package-lock.json @@ -31,6 +31,7 @@ "fullcalendar": "^6.1.14", "googleapis": "^140.0.1", "handlebars": "^4.7.8", + "json2csv": "^6.0.0-alpha.2", "mysql2": "^3.10.1", "next": "14.2.4", "prop-types": "^15.8.1", @@ -3772,6 +3773,11 @@ "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", "dev": true }, + "node_modules/@streamparser/json": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.6.tgz", + "integrity": "sha512-vL9EVn/v+OhZ+Wcs6O4iKE9EUpwHUqHmCtNUMWjqp+6dr85+XPOSGTEsqYNq1Vn04uk9SWlOVmx9J48ggJVT2Q==" + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -8211,6 +8217,31 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json2csv": { + "version": "6.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-6.0.0-alpha.2.tgz", + "integrity": "sha512-nJ3oP6QxN8z69IT1HmrJdfVxhU1kLTBVgMfRnNZc37YEY+jZ4nU27rBGxT4vaqM/KUCavLRhntmTuBFqZLBUcA==", + "dependencies": { + "@streamparser/json": "^0.0.6", + "commander": "^6.2.0", + "lodash.get": "^4.4.2" + }, + "bin": { + "json2csv": "bin/json2csv.js" + }, + "engines": { + "node": ">= 12", + "npm": ">= 6.13.0" + } + }, + "node_modules/json2csv/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -8401,6 +8432,11 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", diff --git a/booking-app/package.json b/booking-app/package.json index ba14c6ac..b600f1f7 100644 --- a/booking-app/package.json +++ b/booking-app/package.json @@ -34,6 +34,7 @@ "fullcalendar": "^6.1.14", "googleapis": "^140.0.1", "handlebars": "^4.7.8", + "json2csv": "^6.0.0-alpha.2", "mysql2": "^3.10.1", "next": "14.2.4", "prop-types": "^15.8.1", From 73c867b6e0022e11c7baba69664b31cbaaf48da6 Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Thu, 26 Sep 2024 15:36:52 -0400 Subject: [PATCH 15/16] Update agreement text --- .../routes/booking/components/FormInput.tsx | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) 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 fec43f53..4ab11790 100644 --- a/booking-app/components/src/client/routes/booking/components/FormInput.tsx +++ b/booking-app/components/src/client/routes/booking/components/FormInput.tsx @@ -494,17 +494,18 @@ export default function FormInput({ calendarEventId, formContext }: Props) { {" "} I confirm receipt of the - 370J Pre-Event Checklist + 370J Media Commons Event Service Rates/Additional Information - and acknowledge that it is my responsibility to setup various - event services as detailed within the checklist. While the 370J - Operations staff do setup cleaning services through CBS, they do - not facilitate hiring security, catering, and arranging room - setup services. + document that contains information regarding event needs and + services. I acknowledge that it is my responsibility to set up + catering and Campus Media if needed for my reservation. I + understand that the 370J Media Commons Operations staff will + setup CBS cleaning services, facilitate hiring security, and + arrange room setup services if needed for my reservation.

} /> @@ -514,12 +515,13 @@ export default function FormInput({ calendarEventId, formContext }: Props) { onChange={setResetRoom} description={

- I agree to reset any and all requested rooms and common spaces - to their original state at the end of the event, including - cleaning and furniture return, and will notify building staff of - any problems, damage, or other concerns affecting the condition - and maintenance of the reserved space. I understand that if I do - not reset the room, I will lose reservation privileges. + I agree to reset all rooms and common spaces I have used to + their original state at the end of my reservation, including + returning equipment, resetting furniture, and cleaning up after + myself. I will notify Media Commons staff of any problems, + damage, or other concerns affecting the condition and + maintenance of the reserved space. I understand that if I do not + reset the room, I may lose access to the Media Commons.

} /> From 22da003d552a393b6b365469091706060463a3ed Mon Sep 17 00:00:00 2001 From: "riho.takagi" Date: Thu, 26 Sep 2024 16:02:51 -0400 Subject: [PATCH 16/16] Update agreement text --- .../src/client/routes/booking/components/FormInput.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 4ab11790..6e3c7415 100644 --- a/booking-app/components/src/client/routes/booking/components/FormInput.tsx +++ b/booking-app/components/src/client/routes/booking/components/FormInput.tsx @@ -533,14 +533,14 @@ export default function FormInput({ calendarEventId, formContext }: Props) {

I have read the - Booking Policy for 370 Jay Street Shared Spaces + Booking Policy for 370J Media Commons and agree to follow all policies outlined. I understand that I - may lose access to spaces if I break this agreement. + may lose access to the Media Commons if I break this agreement.

} />