diff --git a/booking-app/app/api/syncCalendars/route.ts b/booking-app/app/api/syncCalendars/route.ts new file mode 100644 index 00000000..c5a21c53 --- /dev/null +++ b/booking-app/app/api/syncCalendars/route.ts @@ -0,0 +1,154 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { Booking, MediaServices } from "@/components/src/types"; +import admin from "@/firebaseAdmin"; +import { getCalendarClient } from "@/lib/googleClient"; +import { toFirebaseTimestampFromString } from "@/components/src/client/utils/serverDate"; +import { Timestamp } from "@firebase/firestore"; +import { NextResponse } from "next/server"; +import { TableNames } from "@/components/src/policy"; + +const db = admin.firestore(); +const createBookingWithDefaults = ( + partialBooking: Partial, +): 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; + for (const resource of resources) { + try { + // Fetch events for each calendar + const now = new Date(); + const events = await calendar.events.list({ + calendarId: resource.calendarId, + timeMin: now.toISOString(), + maxResults: 100, // Adjust as needed + singleEvents: true, + orderBy: "startTime", + }); + + for (const event of events.data.items || []) { + const bookingRef = db + .collection("bookings") + .where("calendarEventId", "==", event.id); + const bookingSnapshot = await bookingRef.get(); + + if (bookingSnapshot.empty) { + // Create a new booking + const calendarEventId = event.id; + const nyuEmail = findNyuEmail(event); + 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, + }); + 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(), + }; + const statusDocRef = await db + .collection(TableNames.BOOKING_STATUS) + .add(newBookingStatus); + console.log( + `New BookingStatus created with ID: ${statusDocRef.id}`, + ); + + totalNewBookings++; + } + } + } 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 }, + ); + } +} 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 4add3488..f1654c19 100644 --- a/booking-app/components/src/client/routes/admin/components/Settings.tsx +++ b/booking-app/components/src/client/routes/admin/components/Settings.tsx @@ -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" }, @@ -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() { @@ -52,6 +54,7 @@ export default function Settings() { {tab === "bookingTypes" && } {tab === "policy" && } {tab === "export" && } + {tab === "syncCalendars" && } ); diff --git a/booking-app/components/src/client/routes/admin/components/SyncCalendars.tsx b/booking-app/components/src/client/routes/admin/components/SyncCalendars.tsx new file mode 100644 index 00000000..6f8d6def --- /dev/null +++ b/booking-app/components/src/client/routes/admin/components/SyncCalendars.tsx @@ -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 ( + + + {" "} + Sync Current Semester Calendar Events + +

+ This function saves existing events from the current semester's calendar + to the database. +

+ + + + setShowAlert(false)} + /> +
+ ); +}; + +export default SyncCalendars;