diff --git a/booking-app/app/api/approve/route.ts b/booking-app/app/api/approve/route.ts
index 73204525..67684bbd 100644
--- a/booking-app/app/api/approve/route.ts
+++ b/booking-app/app/api/approve/route.ts
@@ -4,7 +4,6 @@ import { serverApproveBooking } from "@/components/src/server/admin";
export async function POST(req: NextRequest) {
const { id, email } = await req.json();
-
try {
await serverApproveBooking(id, email);
return NextResponse.json(
@@ -14,8 +13,8 @@ export async function POST(req: NextRequest) {
} catch (error) {
console.error(`booking_id: ${id} Error approving:`, error);
return NextResponse.json(
- { error: `Failed to approve booking. id: ${id}` },
- { status: 500 },
+ { error: error.message },
+ { status: error.status },
);
}
}
diff --git a/booking-app/app/equipment/page.tsx b/booking-app/app/equipment/page.tsx
new file mode 100644
index 00000000..97004678
--- /dev/null
+++ b/booking-app/app/equipment/page.tsx
@@ -0,0 +1,16 @@
+// app/liaison/page.tsx
+
+"use client";
+
+import Equipment from "@/components/src/client/routes/equipment/Equipment";
+import { Suspense } from "react";
+
+const EquipmentPage: React.FC = () => {
+ return (
+ Loading...}>
+
+
+ );
+};
+
+export default EquipmentPage;
diff --git a/booking-app/clientInfo b/booking-app/clientInfo
new file mode 100644
index 00000000..368be10f
--- /dev/null
+++ b/booking-app/clientInfo
@@ -0,0 +1 @@
+o4Qd0YDOKtCR95EUyQ2Rbp5UbNUa:XkfbKenefg7ntnRR1D3r3nTo_lQa
diff --git a/booking-app/components/src/client/routes/admin/components/Approvers.tsx b/booking-app/components/src/client/routes/admin/components/Approvers.tsx
new file mode 100644
index 00000000..4162ea8e
--- /dev/null
+++ b/booking-app/components/src/client/routes/admin/components/Approvers.tsx
@@ -0,0 +1,16 @@
+import { Typography } from "@mui/material";
+import { EquipmentUsers } from "./EquipmentUsers";
+import { Liaisons } from "./Liaisons";
+
+export const Approvers = () => (
+
+
+ Liaison Users
+
+
+
+ Equipment Users
+
+
+
+);
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 5e8ac63f..e2efca82 100644
--- a/booking-app/components/src/client/routes/admin/components/BookingActions.tsx
+++ b/booking-app/components/src/client/routes/admin/components/BookingActions.tsx
@@ -36,7 +36,7 @@ export default function BookingActions(props: Props) {
const [selectedAction, setSelectedAction] = useState(
Actions.PLACEHOLDER
);
- const { reloadBookings } = useContext(DatabaseContext);
+ const { reloadFutureBookings } = useContext(DatabaseContext);
const [showError, setShowError] = useState(false);
const [reason, setReason] = useState();
@@ -54,7 +54,7 @@ export default function BookingActions(props: Props) {
};
const reload = async () => {
- reloadBookings();
+ reloadFutureBookings();
};
const onError = () => {
diff --git a/booking-app/components/src/client/routes/admin/components/EquipmentUsers.tsx b/booking-app/components/src/client/routes/admin/components/EquipmentUsers.tsx
new file mode 100644
index 00000000..1d0fa3fc
--- /dev/null
+++ b/booking-app/components/src/client/routes/admin/components/EquipmentUsers.tsx
@@ -0,0 +1,60 @@
+import { useContext, useMemo } from "react";
+
+import { ApproverLevel, TableNames } from "../../../../policy";
+import { formatDate } from "../../../utils/date";
+import AddRow from "../../components/AddRow";
+import ListTable from "../../components/ListTable";
+import { DatabaseContext } from "../../components/Provider";
+
+const AddEquipmentForm = ({ equipmentEmails, reloadEquipmentEmails }) => {
+ return (
+
+ );
+};
+
+export const EquipmentUsers = () => {
+ const { equipmentUsers, reloadApproverUsers } = useContext(DatabaseContext);
+ const equipmentEmails = useMemo(
+ () => equipmentUsers.map((user) => user.email),
+ [equipmentUsers]
+ );
+
+ const rows = useMemo(() => {
+ const filtered = equipmentUsers.map((liaison) => {
+ const { level, ...other } = liaison;
+ return other;
+ });
+ const sorted = filtered.sort((a, b) =>
+ a.department.localeCompare(b.department)
+ );
+ return sorted as unknown as { [key: string]: string }[];
+ }, [equipmentUsers]);
+
+ return (
+
+ }
+ columnFormatters={{ createdAt: formatDate }}
+ />
+ );
+};
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 4f96ca9e..30525ed0 100644
--- a/booking-app/components/src/client/routes/admin/components/Settings.tsx
+++ b/booking-app/components/src/client/routes/admin/components/Settings.tsx
@@ -1,23 +1,23 @@
import { Divider, ListItemButton, ListItemText, Stack } from "@mui/material";
+import Grid from "@mui/material/Unstable_Grid2";
+import { useState } from "react";
import { AdminUsers } from "./AdminUsers";
+import { Approvers } from "./Approvers";
import { BannedUsers } from "./Ban";
import BookingTypes from "./BookingTypes";
import { Departments } from "./Departments";
import ExportDatabase from "./ExportDatabase";
-import Grid from "@mui/material/Unstable_Grid2";
-import { Liaisons } from "./Liaisons";
import { PAUsers } from "./PAUsers";
import PolicySettings from "./PolicySettings";
import SafetyTrainedUsers from "./SafetyTraining";
import SyncCalendars from "./SyncCalendars";
-import { useState } from "react";
const tabs = [
{ label: "Safety Training", id: "safetyTraining" },
{ label: "PA Users", id: "pa" },
{ label: "Admin Users", id: "admin" },
- { label: "Liaisons", id: "liaisons" },
+ { label: "Approvers", id: "approvers" },
{ label: "Departments", id: "departments" },
{ label: "Ban", id: "ban" },
{ label: "Booking Types", id: "bookingTypes" },
@@ -51,7 +51,7 @@ export default function Settings() {
{tab === "safetyTraining" && }
{tab === "pa" && }
{tab === "admin" && }
- {tab === "liaisons" && }
+ {tab === "approvers" && }
{tab === "ban" && }
{tab === "departments" && }
{tab === "bookingTypes" && }
diff --git a/booking-app/components/src/client/routes/admin/hooks/useBookingActions.tsx b/booking-app/components/src/client/routes/admin/hooks/useBookingActions.tsx
index daf7a0c4..59aa07d5 100644
--- a/booking-app/components/src/client/routes/admin/hooks/useBookingActions.tsx
+++ b/booking-app/components/src/client/routes/admin/hooks/useBookingActions.tsx
@@ -1,4 +1,3 @@
-import { BookingStatusLabel, PageContextLevel } from "@/components/src/types";
import {
cancel,
checkOut,
@@ -7,13 +6,14 @@ import {
decline,
noShow,
} from "@/components/src/server/db";
+import { BookingStatusLabel, PageContextLevel } from "@/components/src/types";
import { useContext, useMemo, useState } from "react";
+import { Timestamp } from "@firebase/firestore";
+import { useRouter } from "next/navigation";
import { BookingContext } from "../../booking/bookingProvider";
import { DatabaseContext } from "../../components/Provider";
-import { Timestamp } from "@firebase/firestore";
import useExistingBooking from "./useExistingBooking";
-import { useRouter } from "next/navigation";
export enum Actions {
CANCEL = "Cancel",
@@ -146,6 +146,7 @@ export default function useBookingActions({
status !== BookingStatusLabel.CHECKED_IN &&
status !== BookingStatusLabel.CHECKED_OUT &&
status !== BookingStatusLabel.NO_SHOW &&
+ status !== BookingStatusLabel.APPROVED &&
startDate.toDate() > date
) {
options.push(Actions.EDIT);
@@ -184,6 +185,7 @@ export default function useBookingActions({
}, [status]);
const liaisonOptions = [Actions.FIRST_APPROVE, Actions.DECLINE];
+ const equipmentOptions = [Actions.MODIFICATION, Actions.DECLINE];
const adminOptions = useMemo(() => {
if (
@@ -215,6 +217,8 @@ export default function useBookingActions({
return paOptions;
case PageContextLevel.LIAISON:
return liaisonOptions;
+ case PageContextLevel.EQUIPMENT:
+ return equipmentOptions;
default:
return adminOptions;
}
diff --git a/booking-app/components/src/client/routes/admin/hooks/useExistingBooking.tsx b/booking-app/components/src/client/routes/admin/hooks/useExistingBooking.tsx
index eead2d63..da022390 100644
--- a/booking-app/components/src/client/routes/admin/hooks/useExistingBooking.tsx
+++ b/booking-app/components/src/client/routes/admin/hooks/useExistingBooking.tsx
@@ -12,10 +12,10 @@ export default function useExistingBooking() {
setBookingCalendarInfo,
setFormData,
} = useContext(BookingContext);
- const { bookings, roomSettings } = useContext(DatabaseContext);
+ const { futureBookings, roomSettings } = useContext(DatabaseContext);
const findBooking = (calendarEventId: string) =>
- bookings.filter(
+ futureBookings.filter(
(booking) => booking.calendarEventId === calendarEventId
)[0];
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 d29cd9aa..7b028ece 100644
--- a/booking-app/components/src/client/routes/booking/hooks/useSubmitBooking.tsx
+++ b/booking-app/components/src/client/routes/booking/hooks/useSubmitBooking.tsx
@@ -10,7 +10,7 @@ export default function useSubmitBooking(formContext: FormContextLevel) {
const {
liaisonUsers,
userEmail,
- reloadBookings,
+ reloadFutureBookings,
pagePermission,
roomSettings,
} = useContext(DatabaseContext);
@@ -122,7 +122,7 @@ export default function useSubmitBooking(formContext: FormContextLevel) {
setFormData(undefined);
setHasShownMocapModal(false);
- reloadBookings();
+ reloadFutureBookings();
setSubmitting("success");
})
.catch((error) => {
@@ -136,7 +136,7 @@ export default function useSubmitBooking(formContext: FormContextLevel) {
liaisonUsers,
userEmail,
router,
- reloadBookings,
+ reloadFutureBookings,
department,
role,
]
diff --git a/booking-app/components/src/client/routes/components/AddRow.tsx b/booking-app/components/src/client/routes/components/AddRow.tsx
index 697779e0..7fb22f2b 100644
--- a/booking-app/components/src/client/routes/components/AddRow.tsx
+++ b/booking-app/components/src/client/routes/components/AddRow.tsx
@@ -1,12 +1,12 @@
-import { Box, IconButton, TextField } from "@mui/material";
+import { IconButton, TextField } from "@mui/material";
import React, { useMemo, useState } from "react";
+import { Timestamp } from "@firebase/firestore";
import { AddCircleOutline } from "@mui/icons-material";
import Grid from "@mui/material/Unstable_Grid2/Grid2";
-import Loading from "./Loading";
-import { TableNames } from "../../../policy";
-import { Timestamp } from "@firebase/firestore";
import { clientSaveDataToFirestore } from "../../../../../lib/firebase/firebase";
+import { TableNames } from "../../../policy";
+import Loading from "./Loading";
interface Props {
addDuplicateErrorMessage?: string;
@@ -18,9 +18,9 @@ interface Props {
rowsRefresh: () => Promise;
title: string;
extra?: {
- components: React.ReactNode[];
+ components?: React.ReactNode[];
values: { [key: string]: any };
- updates: ((x: string) => void)[];
+ updates?: ((x: string) => void)[];
};
}
diff --git a/booking-app/components/src/client/routes/components/Provider.tsx b/booking-app/components/src/client/routes/components/Provider.tsx
index e27d7e43..d348345c 100644
--- a/booking-app/components/src/client/routes/components/Provider.tsx
+++ b/booking-app/components/src/client/routes/components/Provider.tsx
@@ -1,3 +1,5 @@
+import { ApproverLevel, TableNames } from "@/components/src/policy";
+import React, { createContext, useEffect, useMemo, useState } from "react";
import {
AdminUser,
Approver,
@@ -14,19 +16,23 @@ import {
Settings,
UserApiData,
} from "../../../types";
-import { ApproverLevel, TableNames } from "@/components/src/policy";
-import React, { createContext, useEffect, useMemo, useState } from "react";
-import { clientFetchAllDataFromCollection } from "@/lib/firebase/firebase";
-import { fetchAllFutureBooking } from "@/components/src/server/db";
import { useAuth } from "@/components/src/client/routes/components/AuthProvider";
+import {
+ fetchAllBookings,
+ fetchAllFutureBooking,
+} from "@/components/src/server/db";
+import { clientFetchAllDataFromCollection } from "@/lib/firebase/firebase";
+import { Timestamp } from "firebase-admin/firestore";
export interface DatabaseContextType {
adminUsers: AdminUser[];
bannedUsers: Ban[];
- bookings: Booking[];
+ futureBookings: Booking[];
+ allBookings: Booking[];
bookingsLoading: boolean;
liaisonUsers: Approver[];
+ equipmentUsers: Approver[];
departmentNames: DepartmentType[];
operationHours: OperationHours[];
pagePermission: PagePermission;
@@ -41,21 +47,24 @@ export interface DatabaseContextType {
reloadAdminUsers: () => Promise;
reloadApproverUsers: () => Promise;
reloadBannedUsers: () => Promise;
- reloadBookings: () => Promise;
+ reloadFutureBookings: () => Promise;
reloadDepartmentNames: () => Promise;
reloadOperationHours: () => Promise;
reloadPaUsers: () => Promise;
reloadBookingTypes: () => Promise;
reloadSafetyTrainedUsers: () => Promise;
setUserEmail: (x: string) => void;
+ fetchAllBookings: () => Promise;
}
export const DatabaseContext = createContext({
adminUsers: [],
bannedUsers: [],
- bookings: [],
+ futureBookings: [],
+ allBookings: [],
bookingsLoading: true,
liaisonUsers: [],
+ equipmentUsers: [],
departmentNames: [],
operationHours: [],
pagePermission: PagePermission.BOOKING,
@@ -70,13 +79,14 @@ export const DatabaseContext = createContext({
reloadAdminUsers: async () => {},
reloadApproverUsers: async () => {},
reloadBannedUsers: async () => {},
- reloadBookings: async () => {},
+ reloadFutureBookings: async () => {},
reloadDepartmentNames: async () => {},
reloadOperationHours: async () => {},
reloadPaUsers: async () => {},
reloadBookingTypes: async () => {},
reloadSafetyTrainedUsers: async () => {},
setUserEmail: (x: string) => {},
+ fetchAllBookings: async () => {},
});
export const DatabaseProvider = ({
@@ -85,10 +95,12 @@ export const DatabaseProvider = ({
children: React.ReactNode;
}) => {
const [bannedUsers, setBannedUsers] = useState([]);
- const [bookings, setBookings] = useState([]);
+ const [futureBookings, setFutureBookings] = useState([]);
const [bookingsLoading, setBookingsLoading] = useState(true);
+ const [allBookings, setAllBookings] = useState([]);
const [adminUsers, setAdminUsers] = useState([]);
const [liaisonUsers, setLiaisonUsers] = useState([]);
+ const [equipmentUsers, setEquipmentUsers] = useState([]);
const [departmentNames, setDepartmentName] = useState([]);
const [operationHours, setOperationHours] = useState([]);
const [paUsers, setPaUsers] = useState([]);
@@ -104,9 +116,12 @@ export const DatabaseProvider = ({
const [userApiData, setUserApiData] = useState(
undefined
);
+ const [lastItem, setLastItem] = useState(null);
+ const LIMIT = 3;
const { user } = useAuth();
const netId = useMemo(() => userEmail?.split("@")[0], [userEmail]);
+
useEffect(() => {
const fetchUserApiData = async () => {
if (!netId) return;
@@ -125,21 +140,30 @@ export const DatabaseProvider = ({
// page permission updates with respect to user email, admin list, PA list
const pagePermission = useMemo(() => {
+ // Early return if no email
if (!userEmail) return PagePermission.BOOKING;
- if (adminUsers.map((admin) => admin.email).includes(userEmail))
- return PagePermission.ADMIN;
- console.log("liaisonUsers", liaisonUsers);
- console.log("userEmail", userEmail);
- console.log(
- "liaisonUsers.map((liaison) => liaison.email).includes(userEmail)",
- liaisonUsers.map((liaison) => liaison.email).includes(userEmail)
- );
- if (liaisonUsers.map((liaison) => liaison.email).includes(userEmail)) {
- return PagePermission.LIAISON;
- } else if (paUsers.map((pa) => pa.email).includes(userEmail))
- return PagePermission.PA;
- else return PagePermission.BOOKING;
- }, [userEmail, adminUsers, paUsers, liaisonUsers]);
+
+ // Pre-compute email lists once
+ const adminEmails = adminUsers.map((admin) => admin.email);
+ const liaisonEmails = liaisonUsers.map((liaison) => liaison.email);
+ const paEmails = paUsers.map((pa) => pa.email);
+ const equipmentEmails = equipmentUsers.map((e) => e.email);
+
+ // Check permissions
+ if (adminEmails.includes(userEmail)) return PagePermission.ADMIN;
+ if (equipmentEmails.includes(userEmail)) return PagePermission.EQUIPMENT;
+ if (liaisonEmails.includes(userEmail)) return PagePermission.LIAISON;
+ if (paEmails.includes(userEmail)) return PagePermission.PA;
+
+ return PagePermission.BOOKING;
+ }, [
+ userEmail,
+ // Make sure we're using the actual arrays in dependencies
+ JSON.stringify(adminUsers),
+ JSON.stringify(liaisonUsers),
+ JSON.stringify(paUsers),
+ JSON.stringify(equipmentUsers),
+ ]);
useEffect(() => {
if (!bookingsLoading) {
@@ -149,7 +173,7 @@ export const DatabaseProvider = ({
fetchDepartmentNames();
fetchSettings();
} else {
- fetchBookings();
+ fetchFutureBookings();
}
}, [bookingsLoading, user]);
@@ -160,80 +184,42 @@ export const DatabaseProvider = ({
fetchRoomSettings();
}, [user]);
+ useEffect(() => {
+ if (
+ pagePermission === PagePermission.ADMIN ||
+ pagePermission === PagePermission.LIAISON ||
+ pagePermission === PagePermission.PA
+ )
+ fetchBookings();
+ }, [pagePermission]);
+
const fetchActiveUserEmail = () => {
if (!user) return;
setUserEmail(user.email);
};
- const fetchBookings = async () => {
- fetchAllFutureBooking(TableNames.BOOKING)
+ const fetchFutureBookings = async () => {
+ fetchAllFutureBooking()
.then((fetchedData) => {
- const bookings = fetchedData.map((item: any) => ({
- id: item.id,
- requestNumber: item.requestNumber,
- calendarEventId: item.calendarEventId,
- email: item.email,
- startDate: item.startDate,
- endDate: item.endDate,
- roomId: String(item.roomId),
- user: item.user,
- room: item.room,
- startTime: item.startTime,
- endTime: item.endTime,
- status: item.status,
- firstName: item.firstName,
- lastName: item.lastName,
- secondaryName: item.secondaryName,
- nNumber: item.nNumber,
- netId: item.netId,
- phoneNumber: item.phoneNumber,
- department: item.department,
- otherDepartment: item.otherDepartment,
- role: item.role,
- sponsorFirstName: item.sponsorFirstName,
- sponsorLastName: item.sponsorLastName,
- sponsorEmail: item.sponsorEmail,
- title: item.title,
- description: item.description,
- bookingType: item.bookingType,
- attendeeAffiliation: item.attendeeAffiliation,
- roomSetup: item.roomSetup,
- setupDetails: item.setupDetails,
- mediaServices: item.mediaServices,
- mediaServicesDetails: item.mediaServicesDetails,
- equipmentCheckedOut: item.equipmentCheckedOut,
- catering: item.catering,
- hireSecurity: item.hireSecurity,
- expectedAttendance: item.expectedAttendance,
- cateringService: item.cateringService,
- missingEmail: item?.missingEmail,
- chartFieldForCatering: item.chartFieldForCatering,
- chartFieldForSecurity: item.chartFieldForSecurity,
- chartFieldForRoomSetup: item.chartFieldForRoomSetup,
- requestedAt: item.requestedAt,
- firstApprovedAt: item.firstApprovedAt,
- firstApprovedBy: item.firstApprovedBy,
- finalApprovedAt: item.finalApprovedAt,
- finalApprovedBy: item.finalApprovedBy,
- declinedAt: item.declinedAt,
- declinedBy: item.declinedBy,
- declineReason: item.declineReason,
- canceledAt: item.canceledAt,
- canceledBy: item.canceledBy,
- checkedInAt: item.checkedInAt,
- checkedInBy: item.checkedInBy,
- checkedOutAt: item.checkedOutAt,
- checkedOutBy: item.checkedOutBy,
- noShowedAt: item.noShowedAt,
- noShowedBy: item.noShowedBy,
- walkedInAt: item.walkedInAt,
- }));
- setBookings(bookings);
+ setFutureBookings(fetchedData as Booking[]);
setBookingsLoading(false);
})
.catch((error) => console.error("Error fetching data:", error));
};
+ const fetchBookings = async () => {
+ try {
+ const bookingsResponse: Booking[] = await fetchAllBookings(
+ LIMIT,
+ lastItem
+ );
+ setLastItem(bookingsResponse[bookingsResponse.length - 1].requestedAt);
+ setAllBookings((oldBookings) => [...oldBookings, ...bookingsResponse]);
+ } catch (error) {
+ console.error("Error fetching data:", error);
+ }
+ };
+
const fetchAdminUsers = async () => {
clientFetchAllDataFromCollection(TableNames.ADMINS)
.then((fetchedData) => {
@@ -341,14 +327,20 @@ export const DatabaseProvider = ({
level: Number(item.level),
}));
const liaisons = all.filter((x) => x.level === ApproverLevel.FIRST);
+ const equipmentUsers = all.filter(
+ (x) => x.level === ApproverLevel.EQUIPMENT
+ );
+
const finalApprover = all.filter(
(x) => x.level === ApproverLevel.FINAL
)[0];
setLiaisonUsers(liaisons);
+ setEquipmentUsers(equipmentUsers);
setPolicySettings({ finalApproverEmail: finalApprover.email });
})
.catch((error) => console.error("Error fetching data:", error));
};
+
const fetchDepartmentNames = async () => {
clientFetchAllDataFromCollection(TableNames.DEPARTMENTS)
.then((fetchedData) => {
@@ -426,8 +418,10 @@ export const DatabaseProvider = ({
value={{
adminUsers,
bannedUsers,
- bookings,
+ futureBookings,
+ allBookings,
liaisonUsers,
+ equipmentUsers,
departmentNames,
operationHours,
paUsers,
@@ -443,13 +437,14 @@ export const DatabaseProvider = ({
reloadAdminUsers: fetchAdminUsers,
reloadApproverUsers: fetchApproverUsers,
reloadBannedUsers: fetchBannedUsers,
- reloadBookings: fetchBookings,
+ reloadFutureBookings: fetchFutureBookings,
reloadDepartmentNames: fetchDepartmentNames,
reloadOperationHours: fetchOperationHours,
reloadPaUsers: fetchPaUsers,
reloadBookingTypes: fetchBookingTypes,
reloadSafetyTrainedUsers: fetchSafetyTrainedUsers,
setUserEmail,
+ fetchAllBookings: fetchBookings,
}}
>
{children}
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 c3968286..d9145fcf 100644
--- a/booking-app/components/src/client/routes/components/bookingTable/Bookings.tsx
+++ b/booking-app/components/src/client/routes/components/bookingTable/Bookings.tsx
@@ -1,5 +1,5 @@
import { Booking, BookingRow, PageContextLevel } from "../../../../types";
-import { Box, TableCell } from "@mui/material";
+import { Box, TableCell, Typography } from "@mui/material";
import React, {
useCallback,
useContext,
@@ -20,6 +20,7 @@ import MoreInfoModal from "./MoreInfoModal";
import SortableTableCell from "./SortableTableCell";
import useAllowedStatuses from "./hooks/useAllowedStatuses";
import { useBookingFilters } from "./hooks/useBookingFilters";
+import { Button } from "@mui/material";
interface BookingsProps {
pageContext: PageContextLevel;
@@ -30,7 +31,7 @@ export const Bookings: React.FC = ({
pageContext,
calendarEventId,
}) => {
- const { bookings, bookingsLoading, reloadBookings } =
+ const { futureBookings, bookingsLoading, reloadFutureBookings, fetchAllBookings, allBookings } =
useContext(DatabaseContext);
const allowedStatuses = useAllowedStatuses(pageContext);
@@ -45,7 +46,7 @@ export const Bookings: React.FC = ({
const isUserView = pageContext === PageContextLevel.USER;
useEffect(() => {
- reloadBookings();
+ reloadFutureBookings();
}, []);
const filteredRows = useBookingFilters({
@@ -102,7 +103,7 @@ export const Bookings: React.FC = ({
}, [pageContext, statusFilters, allowedStatuses, selectedDateRange]);
const bottomSection = useMemo(() => {
- if (bookingsLoading && bookings.length === 0) {
+ if (bookingsLoading && futureBookings.length === 0) {
return (
@@ -196,6 +197,7 @@ export const Bookings: React.FC = ({
/>
))}
+
{isUserView && }
{bottomSection}
{modalData != null && (
@@ -204,6 +206,35 @@ export const Bookings: React.FC = ({
closeModal={() => setModalData(null)}
/>
)}
+ {!isUserView && (
+
+
+ Previous Bookings
+
+
+ {allBookings.map((row: BookingRow) => (
+
+ ))}
+
+
+
+
+
+ )}
);
};
diff --git a/booking-app/components/src/client/routes/components/bookingTable/EquipmentCheckoutToggle.tsx b/booking-app/components/src/client/routes/components/bookingTable/EquipmentCheckoutToggle.tsx
index 23170f88..3dd4c6b4 100644
--- a/booking-app/components/src/client/routes/components/bookingTable/EquipmentCheckoutToggle.tsx
+++ b/booking-app/components/src/client/routes/components/bookingTable/EquipmentCheckoutToggle.tsx
@@ -15,7 +15,7 @@ export default function EquipmentCheckoutToggle({ booking, status }: Props) {
const [loading, setLoading] = useState(false);
const [optimisticStatus, setOptimisticStatus] = useState(status);
const originalStatus = useRef(status);
- const { reloadBookings } = useContext(DatabaseContext);
+ const { reloadFutureBookings } = useContext(DatabaseContext);
const handleEquipToggleChange = async (
event: React.ChangeEvent
@@ -32,7 +32,7 @@ export default function EquipmentCheckoutToggle({ booking, status }: Props) {
equipmentCheckedOut: newStatus,
}
);
- await reloadBookings();
+ await reloadFutureBookings();
} catch (ex) {
console.error(ex);
// Revert to the original status if there's an error
diff --git a/booking-app/components/src/client/routes/components/bookingTable/hooks/useBookingFilters.ts b/booking-app/components/src/client/routes/components/bookingTable/hooks/useBookingFilters.ts
index 712b6878..9bdc772f 100644
--- a/booking-app/components/src/client/routes/components/bookingTable/hooks/useBookingFilters.ts
+++ b/booking-app/components/src/client/routes/components/bookingTable/hooks/useBookingFilters.ts
@@ -29,7 +29,7 @@ export function useBookingFilters(props: Props): BookingRow[] {
selectedDateRange,
selectedStatusFilters,
} = props;
- const { bookings, liaisonUsers, userEmail } = useContext(DatabaseContext);
+ const { futureBookings, liaisonUsers, userEmail } = useContext(DatabaseContext);
const allowedStatuses = useAllowedStatuses(pageContext);
const [currentTime, setCurrentTime] = useState(new Date());
@@ -43,11 +43,11 @@ export function useBookingFilters(props: Props): BookingRow[] {
const rows: BookingRow[] = useMemo(
() =>
- bookings.map((booking) => ({
+ futureBookings.map((booking) => ({
...booking,
status: getBookingStatus(booking),
})),
- [bookings]
+ [futureBookings]
);
const filteredRows = useMemo(() => {
diff --git a/booking-app/components/src/client/routes/components/navBar.tsx b/booking-app/components/src/client/routes/components/navBar.tsx
index f74108dc..43cf45ad 100644
--- a/booking-app/components/src/client/routes/components/navBar.tsx
+++ b/booking-app/components/src/client/routes/components/navBar.tsx
@@ -77,6 +77,9 @@ export default function NavBar() {
case PagePermission.LIAISON:
router.push("/liaison");
break;
+ case PagePermission.EQUIPMENT:
+ router.push("/equipment");
+ break;
}
};
@@ -103,6 +106,8 @@ export default function NavBar() {
setSelectedView(PagePermission.ADMIN);
} else if (pathname.includes("/liaison")) {
setSelectedView(PagePermission.LIAISON);
+ } else if (pathname.includes("/equipment")) {
+ setSelectedView(PagePermission.EQUIPMENT);
}
}, [pathname]);
@@ -133,6 +138,8 @@ export default function NavBar() {
pagePermission === PagePermission.LIAISON ||
pagePermission === PagePermission.ADMIN;
const showAdmin = pagePermission === PagePermission.ADMIN;
+ const showEquipment =
+ pagePermission === PagePermission.ADMIN || PagePermission.EQUIPMENT;
return (
);
}, [pagePermission, selectedView]);
diff --git a/booking-app/components/src/client/routes/equipment/Equipment.tsx b/booking-app/components/src/client/routes/equipment/Equipment.tsx
new file mode 100644
index 00000000..81ce9a75
--- /dev/null
+++ b/booking-app/components/src/client/routes/equipment/Equipment.tsx
@@ -0,0 +1,40 @@
+import { Box, Tab, Tabs } from "@mui/material";
+import { useContext, useState } from "react";
+import { PageContextLevel, PagePermission } from "../../../types";
+
+import { Bookings } from "../components/bookingTable/Bookings";
+import { DatabaseContext } from "../components/Provider";
+
+const Equipment = () => {
+ const { pagePermission } = useContext(DatabaseContext);
+
+ const [tab, setTab] = useState("bookings");
+
+ const userHasPermission =
+ pagePermission === PagePermission.ADMIN ||
+ pagePermission === PagePermission.EQUIPMENT;
+
+ return (
+
+ {!userHasPermission ? (
+ You do not have permission to view this page.
+ ) : (
+
+ setTab(newVal)}
+ textColor="primary"
+ indicatorColor="primary"
+ >
+
+
+ {tab === "bookings" && (
+
+ )}
+
+ )}
+
+ );
+};
+
+export default Equipment;
diff --git a/booking-app/components/src/policy.ts b/booking-app/components/src/policy.ts
index 9fc7c8ac..d82c2b30 100644
--- a/booking-app/components/src/policy.ts
+++ b/booking-app/components/src/policy.ts
@@ -3,8 +3,8 @@ import {
MEDIA_COMMONS_OPERATION_EMAIL,
} from "./mediaCommonsPolicy";
-import { BookingStatusLabel } from "./types";
import { clientGetFinalApproverEmailFromDatabase } from "@/lib/firebase/firebase";
+import { BookingStatusLabel } from "./types";
export enum TableNames {
ADMINS = "usersAdmin",
@@ -35,6 +35,7 @@ export const BOOKING_TABLE_HIDE_STATUS_TIME_ELAPSED = [
export enum ApproverLevel {
FIRST = 1,
FINAL = 2,
+ EQUIPMENT = 3,
}
/********** CONTACTS ************/
diff --git a/booking-app/components/src/server/admin.ts b/booking-app/components/src/server/admin.ts
index c410bd3f..ee9e6fdb 100644
--- a/booking-app/components/src/server/admin.ts
+++ b/booking-app/components/src/server/admin.ts
@@ -7,8 +7,10 @@ import {
serverGetFinalApproverEmail,
serverUpdateInFirestore,
} from "@/lib/firebase/server/adminDb";
-import { TableNames, getApprovalCcEmail } from "../policy";
+import { ApproverLevel, TableNames, getApprovalCcEmail } from "../policy";
import {
+ AdminUser,
+ Approver,
ApproverType,
BookingFormDetails,
BookingStatus,
@@ -110,68 +112,85 @@ export const serverApproveInstantBooking = (id: string) => {
// both first approve and second approve flows hit here
export const serverApproveBooking = async (id: string, email: string) => {
- const bookingStatus = await serverGetDataByCalendarEventId(
- TableNames.BOOKING,
- id
- );
- const firstApproveDateRange =
- bookingStatus && bookingStatus.firstApprovedAt
- ? bookingStatus.firstApprovedAt.toDate()
- : null;
+ try {
+ const bookingStatus = await serverGetDataByCalendarEventId(
+ TableNames.BOOKING,
+ id
+ );
+ const isFinalApproval = bookingStatus?.firstApprovedAt?.toDate() ?? null;
- console.log("first approve date", firstApproveDateRange);
+ if (isFinalApproval) {
+ await finalApprove(id, email);
+ } else {
+ await firstApprove(id, email);
+ }
+ } catch (error) {
+ throw error.status ? error : { status: 500, message: error.message };
+ }
+};
- // if already first approved, then this is a second approve
- if (firstApproveDateRange !== null) {
- serverFinalApprove(id, email);
- await serverApproveEvent(id);
- } else {
- console.log("email", email);
- serverFirstApprove(id, email);
-
- const response = await fetch(
- `${process.env.NEXT_PUBLIC_BASE_URL}/api/calendarEvents`,
- {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
+const firstApprove = async (id, email) => {
+ serverFirstApprove(id, email);
+
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_BASE_URL}/api/calendarEvents`,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ calendarEventId: id,
+ newValues: {
+ statusPrefix: BookingStatusLabel.PENDING,
},
- body: JSON.stringify({
- calendarEventId: id,
- newValues: {
- statusPrefix: BookingStatusLabel.PENDING,
- },
- }),
- }
- );
- const contents = await serverBookingContents(id);
+ }),
+ }
+ );
+ const contents = await serverBookingContents(id);
- const emailContents = {
- ...contents,
- headerMessage: "This is a request email for final approval.",
- };
- const recipient = await serverGetFinalApproverEmail();
- const formData = {
- templateName: "booking_detail",
- contents: emailContents,
- targetEmail: recipient,
- status: BookingStatusLabel.PENDING,
- eventTitle: contents.title || "",
- requestNumber: contents.requestNumber,
- bodyMessage: "",
- approverType: ApproverType.FINAL_APPROVER,
+ const emailContents = {
+ ...contents,
+ headerMessage: "This is a request email for final approval.",
+ };
+ const recipient = await serverGetFinalApproverEmail();
+ const formData = {
+ templateName: "booking_detail",
+ 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`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData),
+ });
+};
+const finalApprove = async (id, email) => {
+ const finalApprovers = (await approvers()).filter(
+ (a) => a.level === ApproverLevel.FINAL
+ );
+ const finalApproverEmails = [...(await admins()), ...finalApprovers].map(
+ (a) => a.email
+ );
+
+ const canPerformSecondApproval = finalApproverEmails.includes(email);
+ if (!canPerformSecondApproval) {
+ throw {
+ success: false,
+ message:
+ "Unauthorized: Only final approvers or admin users can perform second approval",
+ status: 403,
};
- const res = await fetch(
- `${process.env.NEXT_PUBLIC_BASE_URL}/api/sendEmail`,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(formData),
- }
- );
}
+ serverFinalApprove(id, email);
+ await serverApproveEvent(id);
};
export const serverSendConfirmationEmail = async (
@@ -287,7 +306,17 @@ export const serverApproveEvent = async (id: string) => {
);
};
-export const approvers = async () => {
+export const admins = async (): Promise => {
+ const fetchedData = await serverFetchAllDataFromCollection(TableNames.ADMINS);
+ const filtered = fetchedData.map((item: any) => ({
+ id: item.id,
+ email: item.email,
+ createdAt: item.createdAt,
+ }));
+ return filtered;
+};
+
+export const approvers = async (): Promise => {
const fetchedData = await serverFetchAllDataFromCollection(
TableNames.APPROVERS
);
@@ -295,6 +324,7 @@ export const approvers = async () => {
id: item.id,
email: item.email,
department: item.department,
+ level: item.level,
createdAt: item.createdAt,
}));
return filtered;
diff --git a/booking-app/components/src/server/db.ts b/booking-app/components/src/server/db.ts
index 343ae12b..fd97dabf 100644
--- a/booking-app/components/src/server/db.ts
+++ b/booking-app/components/src/server/db.ts
@@ -5,35 +5,48 @@ import {
Days,
OperationHours,
} from "../types";
-import {
- ApproverLevel,
- TableNames,
- clientGetFinalApproverEmail,
- getCancelCcEmail,
-} from "../policy";
-import { Timestamp, where } from "@firebase/firestore";
+
import {
clientFetchAllDataFromCollection,
clientGetDataByCalendarEventId,
clientSaveDataToFirestore,
clientUpdateDataInFirestore,
+ getPaginatedData,
} from "@/lib/firebase/firebase";
+import { Timestamp, where } from "@firebase/firestore";
+import {
+ ApproverLevel,
+ TableNames,
+ clientGetFinalApproverEmail,
+ getApprovalCcEmail,
+ getCancelCcEmail,
+} from "../policy";
import { clientUpdateDataByCalendarEventId } from "@/lib/firebase/client/clientDb";
-import { getBookingToolDeployUrl } from "./ui";
import { roundTimeUp } from "../client/utils/date";
+import { getBookingToolDeployUrl } from "./ui";
-export const fetchAllFutureBooking = async (
- collectionName: TableNames
-): Promise => {
+export const fetchAllFutureBooking = async (): Promise => {
const now = Timestamp.now();
const futureQueryConstraints = [where("endDate", ">", now)];
- return clientFetchAllDataFromCollection(
- collectionName,
+ return clientFetchAllDataFromCollection(
+ TableNames.BOOKING,
futureQueryConstraints
);
};
+export const fetchAllBookings = async (
+ limit: number,
+ last: any
+): Promise => {
+ return getPaginatedData(
+ TableNames.BOOKING,
+ limit,
+ "requestedAt",
+ last
+ );
+};
+
export const getOldSafetyTrainingEmails = () => {
//TODO: implement this
return [];
@@ -120,6 +133,12 @@ export const cancel = async (id: string, email: string) => {
headerMessage,
BookingStatusLabel.CANCELED
);
+ clientSendBookingDetailEmail(
+ id,
+ getApprovalCcEmail(process.env.NEXT_PUBLIC_BRANCH_NAME),
+ headerMessage,
+ BookingStatusLabel.NO_SHOW
+ );
clientSendBookingDetailEmail(
id,
getCancelCcEmail(),
@@ -287,6 +306,12 @@ export const noShow = async (id: string, email: string) => {
headerMessage,
BookingStatusLabel.NO_SHOW
);
+ clientSendBookingDetailEmail(
+ id,
+ getApprovalCcEmail(process.env.NEXT_PUBLIC_BRANCH_NAME),
+ headerMessage,
+ BookingStatusLabel.NO_SHOW
+ );
clientSendConfirmationEmail(
id,
BookingStatusLabel.NO_SHOW,
diff --git a/booking-app/components/src/types.ts b/booking-app/components/src/types.ts
index 7901a284..258ca767 100644
--- a/booking-app/components/src/types.ts
+++ b/booking-app/components/src/types.ts
@@ -194,6 +194,7 @@ export enum PagePermission {
BOOKING = 0,
PA,
LIAISON,
+ EQUIPMENT,
ADMIN,
}
@@ -201,6 +202,7 @@ export enum PageContextLevel {
USER = 0,
LIAISON,
PA,
+ EQUIPMENT,
ADMIN,
}
diff --git a/booking-app/lib/firebase/firebase.ts b/booking-app/lib/firebase/firebase.ts
index 34741c9f..d777eb13 100644
--- a/booking-app/lib/firebase/firebase.ts
+++ b/booking-app/lib/firebase/firebase.ts
@@ -10,8 +10,10 @@ import {
getDoc,
getDocs,
limit,
+ orderBy,
query,
setDoc,
+ startAfter,
updateDoc,
where,
} from "@firebase/firestore";
@@ -65,6 +67,68 @@ export const clientFetchAllDataFromCollection = async (
return data;
};
+export const clientFetchAllDataFromCollectionWithLimitAndOffset = async (
+ collectionName: TableNames,
+ limitNumber: number,
+ offset: number
+): Promise => {
+ const db = getDb();
+ const colRef = collection(db, collectionName);
+ const q = query(colRef, limit(limitNumber), where("offset", ">=", offset));
+ const snapshot = await getDocs(q);
+ const data = snapshot.docs.map((document) => ({
+ id: document.id,
+ ...(document.data() as unknown as T),
+ }));
+ return data;
+}
+
+export const getPaginatedData = async (
+ collectionName,
+ itemsPerPage = 10,
+ orderByField = 'requestedAt',
+ lastVisible = null
+ ) : Promise => {
+ try {
+ const db = getDb();
+
+ // Create reference to collection
+ const colRef = collection(db, collectionName);
+
+ // Build query
+ let q = query(
+ colRef,
+ orderBy(orderByField, 'desc'),
+ limit(itemsPerPage)
+ );
+
+ // If we have a last visible item, start after it
+ if (lastVisible) {
+ console.log(lastVisible);
+ q = query(
+ colRef,
+ orderBy(orderByField, 'desc'),
+ startAfter(lastVisible),
+ limit(itemsPerPage)
+ );
+ }
+
+ // Execute query
+ const snapshot = await getDocs(q);
+
+ // Convert snapshot to data array
+ const items = snapshot.docs.map(doc => ({
+ id: doc.id,
+ ...doc.data() as unknown as T
+ }));
+ console.log(items)
+ return items;
+ } catch (error) {
+ console.error('Error getting paginated data:', error);
+ throw error;
+ }
+};
+
export const clientGetFinalApproverEmailFromDatabase = async (): Promise<
string | null
> => {
diff --git a/booking-app/playwright-report/index.html b/booking-app/playwright-report/index.html
new file mode 100644
index 00000000..8c9e3b4e
--- /dev/null
+++ b/booking-app/playwright-report/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/booking-app/test-results/.last-run.json b/booking-app/test-results/.last-run.json
new file mode 100644
index 00000000..cbcc1fba
--- /dev/null
+++ b/booking-app/test-results/.last-run.json
@@ -0,0 +1,4 @@
+{
+ "status": "passed",
+ "failedTests": []
+}
\ No newline at end of file