diff --git a/booking-app/app/api/bookings/route.ts b/booking-app/app/api/bookings/route.ts index d7179e42..e1e75be5 100644 --- a/booking-app/app/api/bookings/route.ts +++ b/booking-app/app/api/bookings/route.ts @@ -1,7 +1,15 @@ import { - serverFormatDate, - toFirebaseTimestampFromString, -} from "@/components/src/client/utils/serverDate"; + BookingFormDetails, + BookingStatusLabel, + RoomSetting, +} from "@/components/src/types"; +import { NextRequest, NextResponse } from "next/server"; +import { + approvalUrl, + declineUrl, + getBookingToolDeployUrl, +} from "@/components/src/server/ui"; +import { deleteEvent, insertEvent } from "@/components/src/server/calendars"; import { firstApproverEmails, serverApproveInstantBooking, @@ -9,24 +17,19 @@ import { serverDeleteFieldsByCalendarEventId, serverUpdateDataByCalendarEventId, } from "@/components/src/server/admin"; -import { deleteEvent, insertEvent } from "@/components/src/server/calendars"; -import { getBookingToolDeployUrl } from "@/components/src/server/ui"; import { - ApproverType, - BookingFormDetails, - BookingStatusLabel, - RoomSetting, -} from "@/components/src/types"; + serverFormatDate, + toFirebaseTimestampFromString, +} from "@/components/src/client/utils/serverDate"; import { serverGetNextSequentialId, serverSaveDataToFirestore, } from "@/lib/firebase/server/adminDb"; -import { NextRequest, NextResponse } from "next/server"; -import { sendHTMLEmail } from "@/app/lib/sendHTMLEmail"; +import { DateSelectArg } from "fullcalendar"; import { TableNames } from "@/components/src/policy"; import { Timestamp } from "firebase-admin/firestore"; -import { DateSelectArg } from "fullcalendar"; +import { sendHTMLEmail } from "@/app/lib/sendHTMLEmail"; async function createBookingCalendarEvent( selectedRooms: RoomSetting[], @@ -85,7 +88,7 @@ async function handleBookingApprovalEmails( ); const emailPromises = recipients.map(recipient => sendHTMLEmail({ - templateName: "booking_detail", + templateName: "approval_email", contents: { ...otherContentsStrings, roomId: selectedRoomIds, @@ -98,7 +101,6 @@ async function handleBookingApprovalEmails( eventTitle: contents.title, requestNumber: contents.requestNumber ?? sequentialId, body: "", - approverType: ApproverType.LIAISON, }), ); await Promise.all(emailPromises); @@ -115,6 +117,8 @@ async function handleBookingApprovalEmails( email, startDate: bookingCalendarInfo?.startStr, endDate: bookingCalendarInfo?.endStr, + approvalUrl: approvalUrl(calendarEventId), + declineUrl: declineUrl(calendarEventId), bookingToolUrl: getBookingToolDeployUrl(), headerMessage: "This is a request email for first approval.", requestNumber: sequentialId, diff --git a/booking-app/app/api/sendEmail/route.ts b/booking-app/app/api/sendEmail/route.ts index 107465e1..c9da616c 100644 --- a/booking-app/app/api/sendEmail/route.ts +++ b/booking-app/app/api/sendEmail/route.ts @@ -11,7 +11,6 @@ export async function POST(req: NextRequest) { eventTitle, requestNumber, bodyMessage, - approverType, } = await req.json(); // if (!templateName || !contents || !targetEmail || !status || !eventTitle) { @@ -30,7 +29,6 @@ export async function POST(req: NextRequest) { eventTitle, requestNumber, body: bodyMessage || "", - approverType, }); return NextResponse.json( { message: "Email sent successfully" }, diff --git a/booking-app/app/liaison/page.tsx b/booking-app/app/liaison/page.tsx index 598d8307..59799d7f 100644 --- a/booking-app/app/liaison/page.tsx +++ b/booking-app/app/liaison/page.tsx @@ -3,22 +3,7 @@ "use client"; import Liaison from "@/components/src/client/routes/liaison/Liaison"; -import { useSearchParams } from "next/navigation"; -import { Suspense } from "react"; -const LiaisonWithParams: React.FC = () => { - const searchParams = useSearchParams(); - const calendarEventId = searchParams.get("calendarEventId"); - - return ; -}; - -const LiaisonPage: React.FC = () => { - return ( - Loading...}> - - - ); -}; +const LiaisonPage: React.FC = () => ; export default LiaisonPage; diff --git a/booking-app/app/lib/sendHTMLEmail.ts b/booking-app/app/lib/sendHTMLEmail.ts index 9086d050..8f4ecd09 100644 --- a/booking-app/app/lib/sendHTMLEmail.ts +++ b/booking-app/app/lib/sendHTMLEmail.ts @@ -1,9 +1,10 @@ -import { serverFormatDate } from "@/components/src/client/utils/serverDate"; +import { approvalUrl, declineUrl } from "@/components/src/server/ui"; + +import fs from "fs"; import { getEmailBranchTag } from "@/components/src/server/emails"; -import { ApproverType } from "@/components/src/types"; import { getGmailClient } from "@/lib/googleClient"; -import fs from "fs"; import path from "path"; +import { serverFormatDate } from "@/components/src/client/utils/serverDate"; let Handlebars; @@ -23,7 +24,6 @@ interface SendHTMLEmailParams { eventTitle: string; requestNumber: number; body: string; - approverType?: ApproverType; } export const sendHTMLEmail = async (params: SendHTMLEmailParams) => { @@ -35,30 +35,10 @@ export const sendHTMLEmail = async (params: SendHTMLEmailParams) => { eventTitle, requestNumber, body, - approverType, } = params; const subj = `${getEmailBranchTag()}${status} - Media Commons request #${requestNumber}: "${eventTitle}"`; - const getUrlPathByApproverType = ( - calendarEventId, - approverType?: ApproverType, - ): string => { - let path: string; - switch (approverType) { - case ApproverType.LIAISON: - path = "/liaison"; - break; - case ApproverType.FINAL_APPROVER: - path = "/admin"; - break; - default: - path = "/"; - } - - return `${process.env.NEXT_PUBLIC_BASE_URL}${path}?calendarEventId=${calendarEventId}`; - }; - const templatePath = path.join( process.cwd(), "app/templates", @@ -66,10 +46,6 @@ export const sendHTMLEmail = async (params: SendHTMLEmailParams) => { ); const templateSource = fs.readFileSync(templatePath, "utf8"); const template = Handlebars.compile(templateSource); - const approvalUrl = getUrlPathByApproverType( - contents.calendarEventId, - approverType, - ); const htmlBody = template({ eventTitle, @@ -78,7 +54,8 @@ export const sendHTMLEmail = async (params: SendHTMLEmailParams) => { contents, startDate: serverFormatDate(contents.startDate), endDate: serverFormatDate(contents.endDate), - approvalUrl, + approvalUrl: approvalUrl(contents.calendarEventId), + declineUrl: declineUrl(contents.calendarEventId), }); const messageParts = [ diff --git a/booking-app/app/templates/approval_email.html b/booking-app/app/templates/approval_email.html new file mode 100644 index 00000000..7bccef41 --- /dev/null +++ b/booking-app/app/templates/approval_email.html @@ -0,0 +1,114 @@ + + + + + + +
+
+

{{ contents.headerMessage }}

+
+
+

Room Reservation Request

+

Room Number: {{ contents.roomId }}

+ +

First name: {{ contents.firstName }}

+

Last name: {{ contents.lastName }}

+ +

Email: {{ contents.email }}

+

Start date: {{ startDate }}

+

End date: {{ endDate }}

+

Secondary Point of Contact: {{ contents.secondaryName }}

+

NYU N-number: {{ contents.nNumber }}

+

Net Id: {{ contents.netId }}

+

Phone Number: {{ contents.phoneNumber }}

+

Department: {{ contents.department }}

+

Requestor's Role: {{ contents.role}}

+

Sponsor First Name: {{ contents.sponsorFirstName }}

+

Sponsor Last Name: {{ contents.sponsorLastName }}

+

Sponsor Email: {{ contents.sponsorEmail }}

+

Title: {{ contents.title }}

+

Description: {{ contents.description }}

+

Booking Type: {{ contents.bookingType }}

+

Expected Attendance: {{ contents.expectedAttendance }}

+

Attendee Affiliations: {{ contents.attendeeAffiliation }}

+

+ Room setup needed?: {{ contents.roomSetup }} {{ contents.setupDetails }} +

+

Chartfield for Room setup?: {{ contents.chartFieldForRoomSetup }}

+

+ Media Services: {{ contents.mediaServices }} {{ + contents.mediaServicesDetails }} +

+

Catering: {{ contents.catering }}

+

Catering Details: {{ contents.cateringService }}

+

Chartfield for Catering?: {{ contents.chartFieldForCatering }}

+

Hire Security: {{ contents.hireSecurity }}

+

+ Chartfield for Hiring Security?: {{ contents.chartFieldForSecurity }} +

+ + Approve + Decline + Open Booking Tool +
+ + diff --git a/booking-app/app/templates/booking_detail.html b/booking-app/app/templates/booking_detail.html index 22e8f22e..a0fdf777 100644 --- a/booking-app/app/templates/booking_detail.html +++ b/booking-app/app/templates/booking_detail.html @@ -1,192 +1,105 @@ - - -
+
+
+

{{ contents.headerMessage }}

+

Room Number: {{ contents.roomId }}

-
Requestor
- - - - - - - - - - - - - - - - - - - - - -
NetID / Name -
- {{ contents.netId }}
{{ contents.firstName }} {{ - contents.lastName }} -
-
Contact Info -
- {{ contents.email }}
- {{ contents.phoneNumber }} -
-
N-Number{{ contents.nNumber }}
Secondary Contact{{ contents.secondaryName }}
Sponsor - {{ contents.sponsorEmail }}
- {{ contents.sponsorFirstName }} {{ contents.sponsorLastName }} -
-
- -
-
Details
- - - - - - - - - - - - - - - - - - - - - -
Title{{ contents.title }}
Description{{ contents.description }}
Booking Type{{ contents.bookingType }}
Expected Attendance{{ contents.expectedAttendance }}
Attendee Affiliation{{ contents.attendeeAffiliation }}
-
- -
-
Services
- - - - - - - - - - - - - - - - - -
Room Setup -
- {{ contents.roomSetup }} {{#if contents.setupDetails}}
{{ - contents.setupDetails }}{{/if}} {{#if - contents.chartFieldForRoomSetup}}
{{ - contents.chartFieldForRoomSetup }}{{/if}} -
-
Media Services -
- {{contents.mediaServices}} {{#if - contents.mediaServicesDetails}}
{{contents.mediaServicesDetails}}{{/if}} -
-
Catering -
- {{contents.cateringService}} {{#if - contents.chartFieldForCatering}}
- {{contents.chartFieldForCatering}}{{/if}} -
-
Security -
- {{contents.hireSecurity}} {{#if - contents.chartFieldForSecurity}}
{{contents.chartFieldForSecurity}}{{/if}} -
-
- Open Booking Tool - to approve or decline this request. +

First name: {{ contents.firstName }}

+

Last name: {{ contents.lastName }}

+

Email: {{ contents.email }}

+

Start date: {{ startDate }}

+

End date: {{ endDate }}

+

Secondary Point of Contact: {{ contents.secondaryName }}

+

NYU N-number: {{ contents.nNumber }}

+

Net Id: {{ contents.netId }}

+

Phone Number: {{ contents.phoneNumber }}

+

Department: {{ contents.department }}

+

Requestor's Role: {{ contents.role}}

+

Sponsor First Name: {{ contents.sponsorFirstName }}

+

Sponsor Last Name: {{ contents.sponsorLastName }}

+

Sponsor Email: {{ contents.sponsorEmail }}

+

Title: {{ contents.title }}

+

Description: {{ contents.description }}

+

Booking Type: {{ contents.bookingType }}

+

Expected Attendance: {{ contents.expectedAttendance }}

+

Attendee Affiliations: {{ contents.attendeeAffiliation }}

+

+ Room setup needed?: {{ contents.roomSetup }} {{ contents.setupDetails }} +

+

Chartfield for Room setup?: {{ contents.chartFieldForRoomSetup }}

+

+ Media Services: {{ contents.mediaServices }} {{ + contents.mediaServicesDetails }} +

+

Catering: {{ contents.catering }}

+

Catering Details: {{ contents.cateringService }}

+

Chartfield for Catering?: {{ contents.chartFieldForCatering }}

+

Hire Security: {{ contents.hireSecurity }}

+

+ Chartfield for Hiring Security?: {{ contents.chartFieldForSecurity }} +

diff --git a/booking-app/components/src/client/routes/approval.html b/booking-app/components/src/client/routes/approval.html new file mode 100644 index 00000000..5f730014 --- /dev/null +++ b/booking-app/components/src/client/routes/approval.html @@ -0,0 +1,10 @@ + + + + + + + + Approved! + + diff --git a/booking-app/components/src/client/routes/booking/approval_email.html b/booking-app/components/src/client/routes/booking/approval_email.html new file mode 100644 index 00000000..49680548 --- /dev/null +++ b/booking-app/components/src/client/routes/booking/approval_email.html @@ -0,0 +1,109 @@ + + + + + + +
+
+

+
+
+

Room Reservation Request

+

Room Number:

+ +

First name:

+

Last name:

+ +

Email:

+

Start date:

+

End date:

+

Secondary Point of Contact:

+

NYU N-number:

+

Net Id:

+

Phone Number:

+

Department:

+

Requestor's Role:

+

Sponsor First Name:

+

Sponsor Last Name:

+

Sponsor Email:

+

Title:

+

Description:

+

Booking Type:

+

Expected Attendance:

+

Attendee Affiliations:

+

Room setup needed?:

+

Chartfield for Room setup?:

+

Media Services:

+

Catering:

+

Catering Details:

+

Chartfield for Catering?:

+

Hire Security:

+

Chartfield for Hiring Security?:

+ + Approve + Decline + Open Booking Tool +
+ + diff --git a/booking-app/components/src/client/routes/booking/booking_detail.html b/booking-app/components/src/client/routes/booking/booking_detail.html new file mode 100644 index 00000000..635bd7bd --- /dev/null +++ b/booking-app/components/src/client/routes/booking/booking_detail.html @@ -0,0 +1,98 @@ + + + + + + +
+
+
+

+

Room Number:

+ +

First name:

+

Last name:

+

Email:

+

Start date:

+

End date:

+

Secondary Point of Contact:

+

NYU N-number:

+

Net Id:

+

Phone Number:

+

Department:

+

Requestor's Role:

+

Sponsor First Name:

+

Sponsor Last Name:

+

Sponsor Email:

+

Title:

+

Description:

+

Booking Type:

+

Expected Attendance:

+

Attendee Affiliations:

+

Room setup needed?:

+

Chartfield for Room setup?:

+

Media Services:

+

Catering:

+

Catering Details:

+

Chartfield for Catering?:

+

Hire Security:

+

Chartfield for Hiring Security?:

+
+ + diff --git a/booking-app/components/src/client/routes/components/bookingTable/Bookings.tsx b/booking-app/components/src/client/routes/components/bookingTable/Bookings.tsx index 2ffecb0b..95b6e5cb 100644 --- a/booking-app/components/src/client/routes/components/bookingTable/Bookings.tsx +++ b/booking-app/components/src/client/routes/components/bookingTable/Bookings.tsx @@ -1,3 +1,4 @@ +import { Booking, BookingRow, PageContextLevel } from "../../../../types"; import { Box, TableCell } from "@mui/material"; import React, { useCallback, @@ -6,30 +7,25 @@ import React, { useMemo, useState, } from "react"; -import { Booking, BookingRow, PageContextLevel } from "../../../../types"; import Table, { TableEmpty } from "../Table"; -import Loading from "../Loading"; -import { DatabaseContext } from "../Provider"; import BookMoreButton from "./BookMoreButton"; import BookingTableFilters from "./BookingTableFilters"; import BookingTableRow from "./BookingTableRow"; -import MoreInfoModal from "./MoreInfoModal"; -import SortableTableCell from "./SortableTableCell"; import { ColumnSortOrder } from "./hooks/getColumnComparator"; +import { DatabaseContext } from "../Provider"; import { DateRangeFilter } from "./hooks/getDateFilter"; +import Loading from "../Loading"; +import MoreInfoModal from "./MoreInfoModal"; +import SortableTableCell from "./SortableTableCell"; import useAllowedStatuses from "./hooks/useAllowedStatuses"; import { useBookingFilters } from "./hooks/useBookingFilters"; interface BookingsProps { pageContext: PageContextLevel; - calendarEventId?: string; } -export const Bookings: React.FC = ({ - pageContext, - calendarEventId, -}) => { +export const Bookings: React.FC = ({ pageContext }) => { const { bookings, bookingsLoading, reloadBookings } = useContext(DatabaseContext); const allowedStatuses = useAllowedStatuses(pageContext); diff --git a/booking-app/components/src/client/routes/liaison/Liaison.tsx b/booking-app/components/src/client/routes/liaison/Liaison.tsx index 19279f01..e808e304 100644 --- a/booking-app/components/src/client/routes/liaison/Liaison.tsx +++ b/booking-app/components/src/client/routes/liaison/Liaison.tsx @@ -1,15 +1,14 @@ import { Box, Tab, Tabs } from "@mui/material"; -import { useContext, useMemo, useState } from "react"; import { PageContextLevel, PagePermission } from "../../../types"; +import React, { useContext, useMemo, useState } from "react"; import { Bookings } from "../components/bookingTable/Bookings"; import { CenterLoading } from "../components/Loading"; import { DatabaseContext } from "../components/Provider"; -const Liaison = ({ calendarEventId }) => { +const Liaison = () => { const { liaisonUsers, pagePermission, userEmail } = useContext(DatabaseContext); - const [tab, setTab] = useState("bookings"); const liaisonEmails = useMemo( @@ -40,10 +39,7 @@ const Liaison = ({ calendarEventId }) => { {tab === "bookings" && ( - + )}
)} diff --git a/booking-app/components/src/client/routes/reject.html b/booking-app/components/src/client/routes/reject.html new file mode 100644 index 00000000..a0d042ec --- /dev/null +++ b/booking-app/components/src/client/routes/reject.html @@ -0,0 +1,10 @@ + + + + + + + + Decline the request + + diff --git a/booking-app/components/src/server/admin.ts b/booking-app/components/src/server/admin.ts index c410bd3f..a123b89c 100644 --- a/booking-app/components/src/server/admin.ts +++ b/booking-app/components/src/server/admin.ts @@ -1,3 +1,9 @@ +import { + BookingFormDetails, + BookingStatus, + BookingStatusLabel, + RoomSetting, +} from "../types"; import { Constraint, serverDeleteData, @@ -8,13 +14,7 @@ import { serverUpdateInFirestore, } from "@/lib/firebase/server/adminDb"; import { TableNames, getApprovalCcEmail } from "../policy"; -import { - ApproverType, - BookingFormDetails, - BookingStatus, - BookingStatusLabel, - RoomSetting, -} from "../types"; +import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; import { Timestamp } from "firebase-admin/firestore"; @@ -23,6 +23,9 @@ export const serverBookingContents = (id: string) => { .then((bookingObj) => { const updatedBookingObj = Object.assign({}, bookingObj, { headerMessage: "This is a request email for final approval.", + approvalUrl: approvalUrl(id), + bookingToolUrl: getBookingToolDeployUrl(), + declineUrl: declineUrl(id), }); return updatedBookingObj as unknown as BookingFormDetails; @@ -152,14 +155,13 @@ export const serverApproveBooking = async (id: string, email: string) => { }; const recipient = await serverGetFinalApproverEmail(); const formData = { - templateName: "booking_detail", + templateName: "approval_email", contents: emailContents, targetEmail: recipient, status: BookingStatusLabel.PENDING, eventTitle: contents.title || "", requestNumber: contents.requestNumber, bodyMessage: "", - approverType: ApproverType.FINAL_APPROVER, }; const res = await fetch( `${process.env.NEXT_PUBLIC_BASE_URL}/api/sendEmail`, @@ -186,8 +188,7 @@ export const serverSendBookingDetailEmail = async ( calendarEventId: string, email: string, headerMessage: string, - status: BookingStatusLabel, - approverType?: ApproverType + status: BookingStatusLabel ) => { const contents = await serverBookingContents(calendarEventId); contents.headerMessage = headerMessage; @@ -199,7 +200,6 @@ export const serverSendBookingDetailEmail = async ( eventTitle: contents.title, requestNumber: contents.requestNumber ?? "--", bodyMessage: "", - approverType: approverType, }; const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/sendEmail`, { method: "POST", diff --git a/booking-app/components/src/server/db.ts b/booking-app/components/src/server/db.ts index f15895b7..ba619a38 100644 --- a/booking-app/components/src/server/db.ts +++ b/booking-app/components/src/server/db.ts @@ -1,17 +1,22 @@ import { - clientFetchAllDataFromCollection, - clientGetDataByCalendarEventId, - clientUpdateDataInFirestore, -} from "@/lib/firebase/firebase"; -import { Timestamp, where } from "@firebase/firestore"; + Approver, + BookingFormDetails, + BookingStatusLabel, + PolicySettings, +} from "../types"; import { ApproverLevel, TableNames, clientGetFinalApproverEmail, getCancelCcEmail, } from "../policy"; -import { Approver, BookingFormDetails, BookingStatusLabel } from "../types"; -import { getBookingToolDeployUrl } from "./ui"; +import { Timestamp, where } from "@firebase/firestore"; +import { approvalUrl, declineUrl, getBookingToolDeployUrl } from "./ui"; +import { + clientFetchAllDataFromCollection, + clientGetDataByCalendarEventId, + clientUpdateDataInFirestore, +} from "@/lib/firebase/firebase"; import { clientUpdateDataByCalendarEventId } from "@/lib/firebase/client/clientDb"; import { roundTimeUp } from "../client/utils/date"; @@ -266,7 +271,9 @@ export const clientBookingContents = (id: string) => { .then((bookingObj) => { const updatedBookingObj = Object.assign({}, bookingObj, { headerMessage: "This is a request email for final approval.", + approvalUrl: approvalUrl(id), bookingToolUrl: getBookingToolDeployUrl(), + declineUrl: declineUrl(id), }); return updatedBookingObj as unknown as BookingFormDetails; diff --git a/booking-app/components/src/server/ui.ts b/booking-app/components/src/server/ui.ts index b32622e8..294f31ec 100644 --- a/booking-app/components/src/server/ui.ts +++ b/booking-app/components/src/server/ui.ts @@ -1,7 +1,7 @@ import { DevBranch } from "../types"; export const getBookingToolDeployUrl = () => { - switch (process.env.NEXT_PUBLIC_BRANCH_NAME as DevBranch) { + switch (process.env.BRANCH_NAME as DevBranch) { case "development": return "https://development-dot-flowing-mantis-389917.uc.r.appspot.com/"; case "staging": @@ -10,3 +10,9 @@ export const getBookingToolDeployUrl = () => { return "https://flowing-mantis-389917.uc.r.appspot.com/"; } }; + +export const approvalUrl = (calendarEventId: string) => + `${process.env.NEXT_PUBLIC_BASE_URL}/approve?calendarEventId=${calendarEventId}`; + +export const declineUrl = (calendarEventId: string) => + `${process.env.NEXT_PUBLIC_BASE_URL}/decline?calendarEventId=${calendarEventId}`; diff --git a/booking-app/components/src/types.ts b/booking-app/components/src/types.ts index 217b0a62..00a6267f 100644 --- a/booking-app/components/src/types.ts +++ b/booking-app/components/src/types.ts @@ -40,6 +40,9 @@ export type BookingRow = Booking & { }; export type BookingFormDetails = Booking & { + approvalUrl: string; + bookingToolUrl: string; + declinedUrl: string; headerMessage?: string; }; @@ -108,11 +111,6 @@ export enum Department { } export type DevBranch = "development" | "staging" | "production" | ""; -export enum ApproverType { - LIAISON = "liaison", - FINAL_APPROVER = "admin", -} - // what context are we entering the form in? export enum FormContextLevel { EDIT = "/edit", diff --git a/reject.html b/reject.html new file mode 100644 index 00000000..db9ac5dd --- /dev/null +++ b/reject.html @@ -0,0 +1,10 @@ + + + + + + + + Reject the request + +