Skip to content

Commit

Permalink
Merge pull request #432 from ITPNYU/main
Browse files Browse the repository at this point in the history
Prod Release 0930
  • Loading branch information
rlho authored Sep 30, 2024
2 parents 9392264 + 337ccd0 commit 6f44d66
Show file tree
Hide file tree
Showing 27 changed files with 479 additions and 452 deletions.
175 changes: 175 additions & 0 deletions booking-app/app/api/syncCalendars/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { toFirebaseTimestampFromString } from "@/components/src/client/utils/serverDate";
import { TableNames } from "@/components/src/policy";
import { Booking, MediaServices } from "@/components/src/types";
import admin from "@/lib/firebase/server/firebaseAdmin";
import { getCalendarClient } from "@/lib/googleClient";
import { Timestamp } from "@firebase/firestore";
import { NextResponse } from "next/server";

const db = admin.firestore();
const createBookingWithDefaults = (
partialBooking: Partial<Booking>,
): Booking => {
return {
title: "",
description: "",
email: "",
firstName: "",
lastName: "",
secondaryName: "",
nNumber: "",
netId: "",
phoneNumber: "",
department: "",
role: "",
sponsorFirstName: "",
sponsorLastName: "",
sponsorEmail: "",
bookingType: "",
attendeeAffiliation: "",
roomSetup: "",
setupDetails: "",
mediaServices: "",
mediaServicesDetails: "",
catering: "",
hireSecurity: "",
expectedAttendance: "",
cateringService: "",
chartFieldForCatering: "",
chartFieldForSecurity: "",
chartFieldForRoomSetup: "",
calendarEventId: "",
roomId: "",
requestNumber: 0,
equipmentCheckedOut: false,
startDate: null,
endDate: null,
...partialBooking,
};
};
const findNyuEmail = (event: any): string => {
const attendees = event.attendees || [];
const nyuEmail = attendees.find(
(attendee: any) => attendee.email && attendee.email.endsWith("@nyu.edu"),
);
return nyuEmail ? nyuEmail.email : "";
};
export async function POST(request: Request) {
try {
const calendar = await getCalendarClient();
// Fetch all calendar IDs from the Resource table
const resourcesSnapshot = await db.collection("resources").get();
const resources = resourcesSnapshot.docs.map(doc => ({
id: doc.id,
calendarId: doc.data().calendarId,
roomId: doc.data().roomId,
}));

let totalNewBookings = 0;
let targetBookings = 0;
for (const resource of resources) {
try {
// Fetch events for each calendar
let pageToken: string | undefined;
const now = new Date();
const threeMonthsAgo = new Date(now.getFullYear(), now.getMonth(), 1);
const threeMonthsLater = new Date(
now.getFullYear(),
now.getMonth() + 4,
0,
);
const timeMin = threeMonthsAgo.toISOString();
const timeMax = threeMonthsLater.toISOString();
const events = await calendar.events.list({
calendarId: resource.calendarId,
timeMin: timeMin,
timeMax: timeMax,
maxResults: 500, // Maximum allowed by Google Calendar API
singleEvents: true,
orderBy: "startTime",
pageToken: pageToken,
});

for (const event of events.data.items || []) {
const bookingRef = db
.collection("bookings")
.where("calendarEventId", "==", event.id);
const bookingSnapshot = await bookingRef.get();
const nyuEmail = findNyuEmail(event);
if (bookingSnapshot.empty && nyuEmail) {
targetBookings++;
console.log("calendarEventId", event.id);
console.log("title", event.summary);
}

if (bookingSnapshot.empty && nyuEmail) {
// Create a new booking
const calendarEventId = event.id;
const newBooking = createBookingWithDefaults({
title: event.summary || "",
description: event.description || "",
email: nyuEmail || "",
startDate: toFirebaseTimestampFromString(
event.start?.dateTime,
) as Timestamp,
endDate: toFirebaseTimestampFromString(
event.end?.dateTime,
) as Timestamp,
calendarEventId: calendarEventId || "",
equipmentCheckedOut: true,
roomId: resource.roomId,
mediaServices: MediaServices.CHECKOUT_EQUIPMENT,
});
console.log("newBooking", newBooking);
const bookingDocRef = await db
.collection(TableNames.BOOKING)
.add(newBooking);

console.log(`New Booking created with ID: ${bookingDocRef.id}`);

const newBookingStatus = {
calendarEventId: calendarEventId,
email: nyuEmail,
requestedAt: admin.firestore.FieldValue.serverTimestamp(),
firstApprovedAt: admin.firestore.FieldValue.serverTimestamp(),
finalApprovedAt: admin.firestore.FieldValue.serverTimestamp(),
};
console.log("newBookingStatus", newBookingStatus);
const statusDocRef = await db
.collection(TableNames.BOOKING_STATUS)
.add(newBookingStatus);
console.log(
`New BookingStatus created with ID: ${statusDocRef.id}`,
);

totalNewBookings++;
}
pageToken = events.data.nextPageToken;
}
while (pageToken);
console.log("targetBookings", targetBookings);
} catch (error) {
console.error(
`Error processing calendar ${resource.calendarId}:`,
error,
);
// Continue with the next calendar
}
}

return NextResponse.json(
{
message: `${totalNewBookings} new bookings have been synchronized.`,
},
{ status: 200 },
);
} catch (error) {
console.error("Error syncing calendars:", error);
return NextResponse.json(
{
error: "An error occurred while syncing calendars.",
},
{ status: 500 },
);
}
}
5 changes: 4 additions & 1 deletion booking-app/app/book/confirmation/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// app/book/confirmation/page.tsx
import BookingFormConfirmationPage from "@/components/src/client/routes/booking/formPages/BookingFormConfirmationPage";
import { FormContextLevel } from "@/components/src/types";
import React from "react";

const Role: React.FC = () => <BookingFormConfirmationPage />;
const Role: React.FC = () => (
<BookingFormConfirmationPage formContext={FormContextLevel.FULL_FORM} />
);

export default Role;
9 changes: 9 additions & 0 deletions booking-app/app/liaison/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// app/liaison/page.tsx

"use client";

import Liaison from "@/components/src/client/routes/liaison/Liaison";

const LiaisonPage: React.FC = () => <Liaison />;

export default LiaisonPage;
10 changes: 10 additions & 0 deletions booking-app/app/walk-in/confirmation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// app/walk-in/confirmation/page.tsx
import BookingFormConfirmationPage from "@/components/src/client/routes/booking/formPages/BookingFormConfirmationPage";
import { FormContextLevel } from "@/components/src/types";
import React from "react";

const Role: React.FC = () => (
<BookingFormConfirmationPage formContext={FormContextLevel.WALK_IN} />
);

export default Role;
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,15 @@ export default function BookingActions({
options.push(Actions.MODIFICATION);
} else if (status === BookingStatusLabel.NO_SHOW) {
options.push(Actions.CHECK_IN);
} else if (status === BookingStatusLabel.WALK_IN) {
options.push(Actions.CHECK_OUT);
options.push(Actions.MODIFICATION);
}
return options;
}, [status]);

const liaisonOptions = [Actions.FIRST_APPROVE, Actions.DECLINE];

const adminOptions = useMemo(() => {
if (
status === BookingStatusLabel.CANCELED ||
Expand Down Expand Up @@ -236,6 +241,8 @@ export default function BookingActions({
return userOptions;
case PageContextLevel.PA:
return paOptions;
case PageContextLevel.LIAISON:
return liaisonOptions;
default:
return adminOptions;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Grid from "@mui/material/Unstable_Grid2";
import { Liaisons } from "./Liaisons";
import { PAUsers } from "./PAUsers";
import SafetyTrainedUsers from "./SafetyTraining";
import SyncCalendars from "./SyncCalendars";

const tabs = [
{ label: "Safety Training", id: "safetyTraining" },
Expand All @@ -22,6 +23,7 @@ const tabs = [
{ label: "Booking Types", id: "bookingTypes" },
{ label: "Policy Settings", id: "policy" },
{ label: "Export", id: "export" },
{ label: "Sync Calendars", id: "syncCalendars" },
];

export default function Settings() {
Expand All @@ -33,10 +35,13 @@ export default function Settings() {
divider={<Divider sx={{ borderColor: "#21212114" }} />}
sx={{ border: "1px solid #21212114", borderRadius: "4px" }}
>
{tabs.map((tab) => (
<div key={tab.label}>
<ListItemButton onClick={() => setTab(tab.id)}>
<ListItemText primary={tab.label} />
{tabs.map((currentTab) => (
<div key={currentTab.label}>
<ListItemButton
onClick={() => setTab(currentTab.id)}
selected={tab === currentTab.id}
>
<ListItemText primary={currentTab.label} />
</ListItemButton>
</div>
))}
Expand All @@ -52,6 +57,7 @@ export default function Settings() {
{tab === "bookingTypes" && <BookingTypes />}
{tab === "policy" && <FinalApproverSetting />}
{tab === "export" && <ExportDatabase />}
{tab === "syncCalendars" && <SyncCalendars />}
</Grid>
</Grid>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Box, Button, Typography } from "@mui/material";
import React, { useState } from "react";
import AlertToast from "../../components/AlertToast";

const SyncCalendars = () => {
const [loading, setLoading] = useState(false);
const [showAlert, setShowAlert] = useState(false);
const [alertSeverity, setAlertSeverity] = useState<"error" | "success">(
"success"
);
const [message, setMessage] = useState("");

const handleSync = async () => {
setLoading(true);
setShowAlert(false);
try {
const response = await fetch("/api/syncCalendars", { method: "POST" });
const data = await response.json();
if (response.ok) {
setMessage(`Sync successful: ${data.message}`);
setAlertSeverity("success");
} else {
setMessage(`Error: ${data.error}`);
setAlertSeverity("error");
}
} catch (error) {
setMessage("An error occurred while syncing calendars.");
setAlertSeverity("error");
} finally {
setLoading(false);
setShowAlert(true);
}
};

return (
<Box>
<Typography variant="h6">
{" "}
Sync Current Semester Calendar Events
</Typography>
<p>
This function saves existing events from the current semester's calendar
to the database.
</p>
<Box sx={{ marginTop: 2 }}>
<Button onClick={handleSync} variant="contained" disabled={loading}>
Sync Calendar Events
</Button>
</Box>
<AlertToast
message={message}
severity={alertSeverity}
open={showAlert}
handleClose={() => setShowAlert(false)}
/>
</Box>
);
};

export default SyncCalendars;
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ export default function BookingStatusBar({ formContext, ...props }: Props) {
message: (
<p>
You have not taken safety training, which is required for at least
one of the rooms you have selected
one of the rooms you have selected.{" "}
<a
href="https://sites.google.com/nyu.edu/370jmediacommons/reservations/safety-training"
target="_blank"
rel="noopener noreferrer"
>
Sign up here
</a>
</p>
),
severity: "error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,7 @@ export default function FormInput({ calendarEventId, formContext }: Props) {

// setFormData(data);
registerEvent(data, isAutoApproval, calendarEventId);
// // TODO confirmation page for walk-in
router.push("/book/confirmation");
router.push(isWalkIn ? "/walk-in/confirmation" : "/book/confirmation");
};

const fullFormFields = (
Expand Down
Loading

0 comments on commit 6f44d66

Please sign in to comment.