diff --git a/media_commons_booking_app/src/client/routes/admin/components/Admin.tsx b/media_commons_booking_app/src/client/routes/admin/components/Admin.tsx index cea5f0f5..6be3937e 100644 --- a/media_commons_booking_app/src/client/routes/admin/components/Admin.tsx +++ b/media_commons_booking_app/src/client/routes/admin/components/Admin.tsx @@ -114,7 +114,7 @@ export default function Admin() { {tab === 'adminUsers' && } {tab === 'paUsers' && } {tab === 'liaesons' && } - {tab === 'bookings' && } + {tab === 'bookings' && } )} diff --git a/media_commons_booking_app/src/client/routes/admin/components/BookingActions.tsx b/media_commons_booking_app/src/client/routes/admin/components/BookingActions.tsx index b1e682f0..9266ab71 100644 --- a/media_commons_booking_app/src/client/routes/admin/components/BookingActions.tsx +++ b/media_commons_booking_app/src/client/routes/admin/components/BookingActions.tsx @@ -1,40 +1,63 @@ -import React, { useContext, useMemo, useState } from 'react'; +import React, { useContext, useState } from 'react'; import { BookingStatusLabel } from '../../../../types'; import { DatabaseContext } from '../../components/Provider'; import Loading from '../../../utils/Loading'; import { serverFunctions } from '../../../utils/serverFunctions'; -import { useLocation } from 'react-router'; interface Props { calendarEventId: string; + isAdminView: boolean; + isUserView: boolean; + setOptimisticStatus: (x: BookingStatusLabel) => void; status: BookingStatusLabel; } -export default function BookingActions({ status, calendarEventId }: Props) { - const [loading, setLoading] = useState(false); +export default function BookingActions({ + status, + calendarEventId, + isAdminView, + isUserView, + setOptimisticStatus, +}: Props) { + const [uiLoading, setUiLoading] = useState(false); const { reloadBookings, reloadBookingStatuses } = useContext(DatabaseContext); - const location = useLocation(); - const isAdminPage = useMemo(() => location.pathname === '/admin', [location]); - const reload = async () => { await Promise.all([reloadBookings(), reloadBookingStatuses()]); }; - const ActionButton = (text: string, action: () => Promise) => ( + const onError = () => alert('Failed to perform action on booking'); + + const ActionButton = ( + text: string, + action: () => Promise, + optimisticNextStatus: BookingStatusLabel, + confirmation?: boolean + ) => ( ); - if (loading) { + if (uiLoading) { return ( @@ -50,39 +73,87 @@ export default function BookingActions({ status, calendarEventId }: Props) { ); } - const paBtns = ( - <> - {status !== BookingStatusLabel.CHECKED_IN && - ActionButton('Check In', () => - serverFunctions.checkin(calendarEventId) - )} - {status !== BookingStatusLabel.NO_SHOW && - ActionButton('No Show', () => serverFunctions.noShow(calendarEventId))} - - ); + if (isUserView) { + if (status === BookingStatusLabel.CANCELED) { + return ; + } + return ( + +
+ {ActionButton( + 'Cancel', + () => serverFunctions.cancel(calendarEventId), + BookingStatusLabel.CANCELED, + true + )} +
+ + ); + } - if (!isAdminPage) { + const paBtns = () => { + const checkInBtn = ActionButton( + 'Check In', + () => serverFunctions.checkin(calendarEventId), + BookingStatusLabel.CHECKED_IN + ); + const noShowBtn = ActionButton( + 'No Show', + () => serverFunctions.noShow(calendarEventId), + BookingStatusLabel.NO_SHOW + ); + + if (status === BookingStatusLabel.APPROVED) { + return ( + <> + {checkInBtn} + {noShowBtn} + + ); + } else if (status === BookingStatusLabel.CHECKED_IN) { + return noShowBtn; + } else if (status === BookingStatusLabel.NO_SHOW) { + return checkInBtn; + } + }; + + if (!isAdminView) { return ( -
{paBtns}
+
{paBtns()}
); } + if ( + status === BookingStatusLabel.CANCELED || + status === BookingStatusLabel.REJECTED + ) { + return ; + } + return ( - +
{status === BookingStatusLabel.PRE_APPROVED && - ActionButton('2nd Approve', () => - serverFunctions.approveBooking(calendarEventId) + ActionButton( + '2nd Approve', + () => serverFunctions.approveBooking(calendarEventId), + BookingStatusLabel.APPROVED )} {status === BookingStatusLabel.REQUESTED && - ActionButton('1st Approve', () => - serverFunctions.approveBooking(calendarEventId) + ActionButton( + '1st Approve', + () => serverFunctions.approveBooking(calendarEventId), + BookingStatusLabel.PRE_APPROVED )} - {ActionButton('Reject', () => serverFunctions.reject(calendarEventId))} - {ActionButton('Cancel', () => serverFunctions.cancel(calendarEventId))} - {paBtns} + {ActionButton( + 'Reject', + () => serverFunctions.reject(calendarEventId), + BookingStatusLabel.REJECTED, + true + )} + {paBtns()}
); diff --git a/media_commons_booking_app/src/client/routes/admin/components/BookingTableRow.tsx b/media_commons_booking_app/src/client/routes/admin/components/BookingTableRow.tsx new file mode 100644 index 00000000..31611e0d --- /dev/null +++ b/media_commons_booking_app/src/client/routes/admin/components/BookingTableRow.tsx @@ -0,0 +1,101 @@ +import { Booking, BookingStatusLabel } from '../../../../types'; +import React, { useContext, useState } from 'react'; + +import BookingActions from './BookingActions'; +import { DatabaseContext } from '../../components/Provider'; +import { formatDate } from '../../../utils/date'; +import getBookingStatus from '../hooks/getBookingStatus'; + +interface Props { + booking: Booking; + isAdminView: boolean; + isUserView: boolean; +} + +export default function BookingTableRow({ + booking, + isAdminView, + isUserView, +}: Props) { + const { bookingStatuses } = useContext(DatabaseContext); + const status = getBookingStatus(booking, bookingStatuses); + + const [optimisticStatus, setOptimisticStatus] = + useState(); + + return ( + + + {optimisticStatus ?? status} + {booking.roomId} + +
+
+
+ {booking.firstName} {booking.lastName} +
+
{booking.email}
+
+ {booking.phoneNumber} +
+
+
+ + +
+
{formatDate(booking.startDate)}
+
+ + +
+
{formatDate(booking.endDate)}
+
+ + {booking.secondaryName} + {isAdminView && {booking.nNumber}} + {booking.netId} + {booking.department} + {booking.role} + + {booking.sponsorFirstName} {booking.sponsorLastName} + + {booking.sponsorEmail} + {booking.title} + {booking.description} + {booking.expectedAttendance} + {booking.attendeeAffiliation} + + {booking.roomSetup} + {booking.setupDetails && ( + <> +
+ Details +
+ {booking.setupDetails} + + )} + + {booking.chartFieldForRoomSetup} + + {booking.mediaServices} + {booking.mediaServicesDetails && ( + <> +
+ Details +
+ {booking.mediaServicesDetails} + + )} + + {booking.catering} + {booking.cateringService} + {booking.chartFieldForCatering} + {booking.hireSecurity} + {booking.chartFieldForSecurity} + + ); +} diff --git a/media_commons_booking_app/src/client/routes/admin/components/Bookings.tsx b/media_commons_booking_app/src/client/routes/admin/components/Bookings.tsx index c3c90347..f5806c05 100644 --- a/media_commons_booking_app/src/client/routes/admin/components/Bookings.tsx +++ b/media_commons_booking_app/src/client/routes/admin/components/Bookings.tsx @@ -1,12 +1,13 @@ -import React, { useContext, useMemo } from 'react'; +import React, { useContext, useEffect, useMemo } from 'react'; -import BookingActions from './BookingActions'; +import { BookingStatusLabel } from '../../../../types'; +import BookingTableRow from './BookingTableRow'; import { DatabaseContext } from '../../components/Provider'; -import { formatDate } from '../../../utils/date'; import getBookingStatus from '../hooks/getBookingStatus'; interface BookingsProps { - showNnumber: boolean; + isAdminView?: boolean; + isPaView?: boolean; isUserView?: boolean; } @@ -17,14 +18,35 @@ const TableHeader = (text: string) => ( ); export const Bookings: React.FC = ({ - showNnumber = false, + isAdminView = false, + isPaView = false, isUserView = false, }) => { - const { bookings, bookingStatuses, userEmail } = useContext(DatabaseContext); + const { + bookings, + bookingStatuses, + userEmail, + reloadBookings, + reloadBookingStatuses, + } = useContext(DatabaseContext); + + useEffect(() => { + reloadBookingStatuses(); + reloadBookings(); + }, []); const filteredBookings = useMemo(() => { + const paViewStatuses = [ + BookingStatusLabel.APPROVED, + BookingStatusLabel.CHECKED_IN, + BookingStatusLabel.NO_SHOW, + ]; if (isUserView) return bookings.filter((booking) => booking.email === userEmail); + if (isPaView) + return bookings.filter((booking) => + paViewStatuses.includes(getBookingStatus(booking, bookingStatuses)) + ); return bookings; }, [isUserView, bookings]); @@ -34,16 +56,15 @@ export const Bookings: React.FC = ({ - {!isUserView && TableHeader('Action')} + {TableHeader('Action')} {TableHeader('Status')} {TableHeader('Room ID')} {TableHeader('Contact')} {TableHeader('Booking Start')} {TableHeader('Booking End')} {TableHeader('Secondary Name')} - {showNnumber && TableHeader('N Number')} + {isAdminView && TableHeader('N Number')} {TableHeader('Net ID')} - {/* {TableHeader('Phone Number')} */} {TableHeader('Department')} {TableHeader('Role')} {TableHeader('Sponsor Name')} @@ -63,105 +84,12 @@ export const Bookings: React.FC = ({ - {filteredBookings.map((booking, index) => { - const status = getBookingStatus(booking, bookingStatuses); - return ( - - {!isUserView && ( - - )} - - - - - - - {showNnumber && ( - - )} - - {/* */} - - - - - - - - - - - - - - - - - - ); - })} + {filteredBookings.map((booking, index) => ( + + ))}
{status}{booking.roomId} -
-
-
- {booking.firstName} {booking.lastName} -
-
- {booking.email} -
-
- {booking.phoneNumber} -
-
-
-
-
-
{formatDate(booking.startDate)}
-
-
-
-
{formatDate(booking.endDate)}
-
-
{booking.secondaryName}{booking.nNumber}{booking.netId}{booking.phoneNumber}{booking.department}{booking.role} - {booking.sponsorFirstName} {booking.sponsorLastName} - {booking.sponsorEmail}{booking.title} - {booking.description} - - {booking.expectedAttendance} - - {booking.attendeeAffiliation} - - {booking.roomSetup} - {booking.setupDetails && ( - <> -
- Details -
- {booking.setupDetails} - - )} -
- {booking.chartFieldForRoomSetup} - - {booking.mediaServices} - {booking.mediaServicesDetails && ( - <> -
- Details -
- {booking.mediaServicesDetails} - - )} -
{booking.catering}{booking.cateringService} - {booking.chartFieldForCatering} - {booking.hireSecurity} - {booking.chartFieldForSecurity} -
diff --git a/media_commons_booking_app/src/client/routes/admin/hooks/getBookingStatus.ts b/media_commons_booking_app/src/client/routes/admin/hooks/getBookingStatus.ts index fe7fb9e6..a004686b 100644 --- a/media_commons_booking_app/src/client/routes/admin/hooks/getBookingStatus.ts +++ b/media_commons_booking_app/src/client/routes/admin/hooks/getBookingStatus.ts @@ -8,14 +8,38 @@ export default function getBookingStatus( const bookingStatusMatch = bookingStatuses.filter( (row) => row.calendarEventId === booking.calendarEventId )[0]; + if (bookingStatusMatch === undefined) return BookingStatusLabel.UNKNOWN; - if (bookingStatusMatch.checkedInAt !== '') { - return BookingStatusLabel.CHECKED_IN; - } else if (bookingStatusMatch.noShowedAt !== '') { - return BookingStatusLabel.NO_SHOW; - } else if (bookingStatusMatch.canceledAt !== '') { - return BookingStatusLabel.CANCELED; - } else if (bookingStatusMatch.rejectedAt !== '') { + + const timeStringtoDate = (time: string) => + time.length > 0 ? new Date(time) : new Date(0); + + const checkedInTimestamp = timeStringtoDate(bookingStatusMatch.checkedInAt); + const noShowTimestamp = timeStringtoDate(bookingStatusMatch.noShowedAt); + const canceledTimestamp = timeStringtoDate(bookingStatusMatch.canceledAt); + + // if any of checkedInAt, noShowedAt, canceledAt have a date, return the most recent + if ( + checkedInTimestamp.getTime() !== 0 || + noShowTimestamp.getTime() !== 0 || + canceledTimestamp.getTime() !== 0 + ) { + let mostRecentTimestamp: Date = checkedInTimestamp; + let label = BookingStatusLabel.CHECKED_IN; + + if (noShowTimestamp > mostRecentTimestamp) { + mostRecentTimestamp = noShowTimestamp; + label = BookingStatusLabel.NO_SHOW; + } + + if (canceledTimestamp > mostRecentTimestamp) { + mostRecentTimestamp = canceledTimestamp; + label = BookingStatusLabel.CANCELED; + } + return label; + } + + if (bookingStatusMatch.rejectedAt !== '') { return BookingStatusLabel.REJECTED; } else if (bookingStatusMatch.secondApprovedAt !== '') { return BookingStatusLabel.APPROVED; diff --git a/media_commons_booking_app/src/client/routes/booking/components/Calendars.tsx b/media_commons_booking_app/src/client/routes/booking/components/Calendars.tsx index db872a36..e2029ea1 100644 --- a/media_commons_booking_app/src/client/routes/booking/components/Calendars.tsx +++ b/media_commons_booking_app/src/client/routes/booking/components/Calendars.tsx @@ -5,6 +5,7 @@ import { DateSelectArg } from '@fullcalendar/core'; import { RoomCalendar } from './RoomCalendar'; import { RoomSetting } from '../../../../types'; import { formatDate } from '../../../utils/date'; +import { HIDING_STATUS } from '../../../../policy'; type CalendarProps = { allRooms: RoomSetting[]; @@ -29,6 +30,9 @@ export const Calendars = ({ const allEvents = calendarApi.getEvents(); return allEvents.some((event) => { if (event.title.includes(TITLE_TAG)) return false; + if (HIDING_STATUS.some((status) => event.title.includes(status))) + return false; + return ( (event.start >= info.start && event.start < info.end) || (event.end > info.start && event.end <= info.end) || diff --git a/media_commons_booking_app/src/client/routes/booking/components/FormInput.tsx b/media_commons_booking_app/src/client/routes/booking/components/FormInput.tsx index 47febc80..a6aa8892 100644 --- a/media_commons_booking_app/src/client/routes/booking/components/FormInput.tsx +++ b/media_commons_booking_app/src/client/routes/booking/components/FormInput.tsx @@ -518,14 +518,18 @@ const FormInput = ({ handleParentSubmit }) => { )}
- + {roomNumber.some((room) => + [103, 220, 221, 222, 223, 224, 230, 233, 260].includes(Number(room)) + ) && ( + + )} {roomNumber.includes('103') && ( )} - {roomNumber.includes('202') || - (roomNumber.includes('1201') && ( - - ))} + {roomNumber.some((room) => [202, 1201].includes(Number(room))) && ( + + )}
{watch('mediaServices') !== undefined && diff --git a/media_commons_booking_app/src/client/routes/booking/components/RoomCalendar.tsx b/media_commons_booking_app/src/client/routes/booking/components/RoomCalendar.tsx index 35a57f6f..f8d69851 100644 --- a/media_commons_booking_app/src/client/routes/booking/components/RoomCalendar.tsx +++ b/media_commons_booking_app/src/client/routes/booking/components/RoomCalendar.tsx @@ -12,6 +12,7 @@ import googleCalendarPlugin from '@fullcalendar/google-calendar'; import interactionPlugin from '@fullcalendar/interaction'; // for selectable import { serverFunctions } from '../../../utils/serverFunctions'; import timeGridPlugin from '@fullcalendar/timegrid'; // a plugin! +import { HIDING_STATUS } from '../../../../policy'; const TITLE_TAG = '[Click to Delete]'; @@ -47,9 +48,49 @@ export const RoomCalendar = ({ ); }, []); + function renderEventContent(eventInfo) { + const match = eventInfo.event.title.match(/\[(.*?)\]/); + const title = match ? match[1] : eventInfo.event.title; + + const startTime = new Intl.DateTimeFormat('en-US', { + hour: '2-digit', + minute: '2-digit', + }).format(eventInfo.event.start); + const endTime = new Intl.DateTimeFormat('en-US', { + hour: '2-digit', + minute: '2-digit', + }).format(eventInfo.event.end); + + let backgroundColor = ''; + // Change the background color of the event depending on its title + if (eventInfo.event.title.includes(BookingStatusLabel.REQUESTED)) { + backgroundColor = '#d60000'; + } else if ( + eventInfo.event.title.includes(BookingStatusLabel.PRE_APPROVED) + ) { + backgroundColor = '#f6c026'; + } else if (eventInfo.event.title.includes(BookingStatusLabel.APPROVED)) { + backgroundColor = '#33b679'; + } + return ( +
+ {title} +
+ ); + } + const fetchCalendarEvents = async (calendarId) => { serverFunctions.getCalendarEvents(calendarId).then((rows) => { - setEvents(rows); + const filteredEvents = rows.filter((row) => { + return !HIDING_STATUS.some((status) => row.title.includes(status)); + }); + setEvents(filteredEvents); }); }; @@ -142,32 +183,7 @@ export const RoomCalendar = ({ right: '', }} themeSystem="bootstrap5" - eventDidMount={function (info) { - // Change the title status only - const match = info.event.title.match(/\[(.*?)\]/); - if (match) { - let el = info.el.querySelector('.fc-event-title'); - if (el != null) { - el.textContent = match[1]; - } - } - // Change the background color of the event depending on its title - if (info.event.title.includes(BookingStatusLabel.REQUESTED)) { - info.el.style.backgroundColor = '#d60000'; - } else if ( - info.event.title.includes(BookingStatusLabel.PRE_APPROVED) - ) { - info.el.style.backgroundColor = '#f6c026'; - } else if (info.event.title.includes(BookingStatusLabel.APPROVED)) { - info.el.style.backgroundColor = '#33b679'; - } else if (info.event.title.includes(BookingStatusLabel.REJECTED)) { - info.el.style.display = 'none'; - } else if (info.event.title.includes(BookingStatusLabel.CANCELED)) { - info.el.style.display = 'none'; - } else if (info.event.title.includes(BookingStatusLabel.NO_SHOW)) { - info.el.style.display = 'none'; - } - }} + eventContent={renderEventContent} editable={true} initialView={selectedRooms.length > 1 ? 'timeGridDay' : 'timeGridDay'} navLinks={true} diff --git a/media_commons_booking_app/src/client/routes/components/AddEmail.tsx b/media_commons_booking_app/src/client/routes/components/AddEmail.tsx index c8e8697e..03e08f99 100644 --- a/media_commons_booking_app/src/client/routes/components/AddEmail.tsx +++ b/media_commons_booking_app/src/client/routes/components/AddEmail.tsx @@ -19,7 +19,7 @@ export default function AddEmail({ userList, userListRefresh, }: Props) { - const [emailToAdd, setEmailToAdd] = useState(); + const [emailToAdd, setEmailToAdd] = useState(''); const [loading, setLoading] = useState(false); const userEmails = useMemo( @@ -28,7 +28,7 @@ export default function AddEmail({ ); const addUser = async () => { - if (!emailToAdd) return; + if (!emailToAdd || emailToAdd.length === 0) return; if (userEmails.includes(emailToAdd)) { alert('This user is already registered'); diff --git a/media_commons_booking_app/src/client/routes/components/EmailListTable.tsx b/media_commons_booking_app/src/client/routes/components/EmailListTable.tsx index 12e5d5a2..8d8fa440 100644 --- a/media_commons_booking_app/src/client/routes/components/EmailListTable.tsx +++ b/media_commons_booking_app/src/client/routes/components/EmailListTable.tsx @@ -1,6 +1,6 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; -import Loading from '../../utils/Loading'; +import EmailListTableRow from './EmailListTableRow'; import { TableNames } from '../../../policy'; // This is a wrapper for google.script.run that lets us use promises. import { serverFunctions } from '../../utils/serverFunctions'; @@ -20,8 +20,6 @@ export default function EmailListTable(props: Props) { const refresh = props.userListRefresh; const columnFormatters = props.columnFormatters || {}; - const [loading, setLoading] = useState(false); - const columnNames = useMemo(() => { if (props.userList.length === 0) { return []; @@ -29,23 +27,6 @@ export default function EmailListTable(props: Props) { return Object.keys(props.userList[0]) as Array as string[]; }, [props.userList]); - const onRemove = async (user: T) => { - setLoading(true); - try { - await serverFunctions.removeFromListByEmail(props.tableName, user.email); - await refresh(); - } catch (ex) { - console.error(ex); - alert('Failed to remove user'); - } finally { - setLoading(false); - } - }; - - if (loading) { - return ; - } - if (props.userList.length === 0) { return

No results

; } @@ -68,31 +49,18 @@ export default function EmailListTable(props: Props) { - {props.userList.map((user, index: number) => { - return ( - - {/* all column values */} - {columnNames.map((columnName, idx) => ( - - {columnFormatters[columnName] - ? columnFormatters[columnName](user[columnName]) - : user[columnName]} - - ))} - - - - - ); - })} + {props.userList.map((user, index: number) => ( + + serverFunctions.removeFromListByEmail( + props.tableName, + user.email + ) + } + {...{ columnNames, columnFormatters, index, user, refresh }} + /> + ))} diff --git a/media_commons_booking_app/src/client/routes/components/EmailListTableRow.tsx b/media_commons_booking_app/src/client/routes/components/EmailListTableRow.tsx new file mode 100644 index 00000000..f2368ec1 --- /dev/null +++ b/media_commons_booking_app/src/client/routes/components/EmailListTableRow.tsx @@ -0,0 +1,88 @@ +import React, { useMemo, useState } from 'react'; + +import Loading from '../../utils/Loading'; +import { TableNames } from '../../../policy'; +// This is a wrapper for google.script.run that lets us use promises. +import { serverFunctions } from '../../utils/serverFunctions'; + +interface EmailField { + email: string; +} + +interface Props { + columnFormatters?: { [key: string]: (value: string) => string }; + columnNames: string[]; + index: number; + refresh: () => Promise; + removeUser: () => Promise; + user: T; +} + +export default function EmailListTableRow( + props: Props +) { + const { columnFormatters, columnNames, index, refresh, removeUser, user } = + props; + const [uiLoading, setUiLoading] = useState(false); + const [isRemoved, setIsRemoved] = useState(false); + + const onError = () => { + alert('Failed to remove user: ' + user.email); + }; + + /** + * Google Sheets API writes are slow. + * To avoid UI lag, assume write will succeed and optimistically remove UI element before response completes. + * If write fails, alert the user and restore UI. + * Only devs should see this error behavior unless something is very broken + */ + const onRemove = async () => { + setUiLoading(true); + setIsRemoved(true); // optimistically hide component + try { + removeUser() + .catch(() => { + onError(); + setIsRemoved(false); + }) + .finally(refresh); + } catch (ex) { + console.error(ex); + onError(); + } finally { + setUiLoading(false); + } + }; + + if (uiLoading) { + return ; + } + + if (isRemoved) { + return null; + } + + return ( + + {/* all column values */} + {columnNames.map((columnName, idx) => ( + + {columnFormatters[columnName] + ? columnFormatters[columnName](user[columnName]) + : user[columnName]} + + ))} + + + + + ); +} diff --git a/media_commons_booking_app/src/client/routes/components/Provider.tsx b/media_commons_booking_app/src/client/routes/components/Provider.tsx index 62e011d4..363d8346 100644 --- a/media_commons_booking_app/src/client/routes/components/Provider.tsx +++ b/media_commons_booking_app/src/client/routes/components/Provider.tsx @@ -94,6 +94,13 @@ export const DatabaseProvider = ({ children }) => { fetchLiaisonUsers(); fetchRoomSettings(); }); + + // refresh booking data every 10s; + setInterval(() => { + console.log('UPDATING'); + fetchBookings(); + fetchBookingStatuses(); + }, 10000); }, []); const fetchActiveUserEmail = () => { diff --git a/media_commons_booking_app/src/client/routes/myBookings/myBookingsPage.tsx b/media_commons_booking_app/src/client/routes/myBookings/myBookingsPage.tsx index 5c2d7962..d1e1346d 100644 --- a/media_commons_booking_app/src/client/routes/myBookings/myBookingsPage.tsx +++ b/media_commons_booking_app/src/client/routes/myBookings/myBookingsPage.tsx @@ -2,5 +2,5 @@ import { Bookings } from '../admin/components/Bookings'; import React from 'react'; export default function MyBookingsPage() { - return ; + return ; } diff --git a/media_commons_booking_app/src/client/routes/pa/PAPage.tsx b/media_commons_booking_app/src/client/routes/pa/PAPage.tsx index 32c83d07..d47eb072 100644 --- a/media_commons_booking_app/src/client/routes/pa/PAPage.tsx +++ b/media_commons_booking_app/src/client/routes/pa/PAPage.tsx @@ -59,7 +59,7 @@ const PAPage = () => { {tab === 'safety_training' && } - {tab === 'bookings' && } + {tab === 'bookings' && } )} diff --git a/media_commons_booking_app/src/policy.ts b/media_commons_booking_app/src/policy.ts index e00c26e3..044575f1 100644 --- a/media_commons_booking_app/src/policy.ts +++ b/media_commons_booking_app/src/policy.ts @@ -1,6 +1,6 @@ /********** GOOGLE SHEETS ************/ -import { DevBranch } from './types'; +import { BookingStatusLabel, DevBranch } from './types'; /** ACTIVE master Google Sheet */ export const ACTIVE_SHEET_ID = '1MnWbn6bvNyMiawddtYYx0tRW4NMgvugl0I8zBO3sy68'; @@ -79,3 +79,9 @@ export const SAFETY_TRAINING_REQUIRED_ROOM = [ ]; export const INSTANT_APPROVAL_ROOMS = ['221', '222', '223', '224', '233']; + +export const HIDING_STATUS = [ + BookingStatusLabel.NO_SHOW, + BookingStatusLabel.CANCELED, + BookingStatusLabel.REJECTED, +]; diff --git a/media_commons_booking_app/src/server/emails.ts b/media_commons_booking_app/src/server/emails.ts index af8169b5..31d37d4f 100644 --- a/media_commons_booking_app/src/server/emails.ts +++ b/media_commons_booking_app/src/server/emails.ts @@ -7,7 +7,10 @@ export const sendTextEmail = ( body: string ) => { const subj = `${status}: Media Commons request for \"${eventTitle}\"`; - GmailApp.sendEmail(targetEmail, subj, body); + const options = { + replyTo: 'mediacommons.reservations@nyu.edu', + }; + GmailApp.sendEmail(targetEmail, subj, body, options); }; const getEmailBranchTag = () => { @@ -29,7 +32,6 @@ export const sendHTMLEmail = ( eventTitle: string, body ) => { - console.log('contents', contents); const subj = `${getEmailBranchTag()}${status}: Media Commons request for \"${eventTitle}\"`; const htmlTemplate = HtmlService.createTemplateFromFile(templateName); for (const key in contents) { @@ -38,6 +40,7 @@ export const sendHTMLEmail = ( const htmlBody = htmlTemplate.evaluate().getContent(); const options = { htmlBody, + replyTo: 'mediacommons.reservations@nyu.edu', }; GmailApp.sendEmail(targetEmail, subj, body, options); };