diff --git a/booking-app/app/equipment/page.tsx b/booking-app/app/equipment/page.tsx new file mode 100644 index 0000000..9700467 --- /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 0000000..368be10 --- /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 0000000..4162ea8 --- /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/EquipmentUsers.tsx b/booking-app/components/src/client/routes/admin/components/EquipmentUsers.tsx new file mode 100644 index 0000000..1d0fa3f --- /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 4f96ca9..30525ed 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 88f76bb..59aa07d 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", @@ -185,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 ( @@ -216,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/components/AddRow.tsx b/booking-app/components/src/client/routes/components/AddRow.tsx index 697779e..7fb22f2 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 d99cda9..d348345 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,15 +16,15 @@ import { Settings, UserApiData, } from "../../../types"; -import { ApproverLevel, TableNames } from "@/components/src/policy"; -import React, { createContext, useEffect, useMemo, useState } from "react"; import { useAuth } from "@/components/src/client/routes/components/AuthProvider"; -import { fetchAllBookings, fetchAllFutureBooking } from "@/components/src/server/db"; +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[]; @@ -30,6 +32,7 @@ export interface DatabaseContextType { allBookings: Booking[]; bookingsLoading: boolean; liaisonUsers: Approver[]; + equipmentUsers: Approver[]; departmentNames: DepartmentType[]; operationHours: OperationHours[]; pagePermission: PagePermission; @@ -61,6 +64,7 @@ export const DatabaseContext = createContext({ allBookings: [], bookingsLoading: true, liaisonUsers: [], + equipmentUsers: [], departmentNames: [], operationHours: [], pagePermission: PagePermission.BOOKING, @@ -96,6 +100,7 @@ export const DatabaseProvider = ({ 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([]); @@ -137,24 +142,27 @@ export const DatabaseProvider = ({ const pagePermission = useMemo(() => { // Early return if no email if (!userEmail) return PagePermission.BOOKING; - + // 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 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(paUsers), + JSON.stringify(equipmentUsers), ]); useEffect(() => { @@ -176,9 +184,13 @@ export const DatabaseProvider = ({ fetchRoomSettings(); }, [user]); - useEffect(()=>{ - if(pagePermission === PagePermission.ADMIN || pagePermission === PagePermission.LIAISON || pagePermission === PagePermission.PA) - fetchBookings(); + useEffect(() => { + if ( + pagePermission === PagePermission.ADMIN || + pagePermission === PagePermission.LIAISON || + pagePermission === PagePermission.PA + ) + fetchBookings(); }, [pagePermission]); const fetchActiveUserEmail = () => { @@ -196,8 +208,11 @@ export const DatabaseProvider = ({ }; const fetchBookings = async () => { - try{ - const bookingsResponse : Booking[] = await fetchAllBookings(LIMIT, lastItem); + try { + const bookingsResponse: Booking[] = await fetchAllBookings( + LIMIT, + lastItem + ); setLastItem(bookingsResponse[bookingsResponse.length - 1].requestedAt); setAllBookings((oldBookings) => [...oldBookings, ...bookingsResponse]); } catch (error) { @@ -312,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) => { @@ -400,6 +421,7 @@ export const DatabaseProvider = ({ futureBookings, allBookings, liaisonUsers, + equipmentUsers, departmentNames, operationHours, paUsers, diff --git a/booking-app/components/src/client/routes/components/navBar.tsx b/booking-app/components/src/client/routes/components/navBar.tsx index f74108d..43cf45a 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 0000000..81ce9a7 --- /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 9fc7c8a..d82c2b3 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/db.ts b/booking-app/components/src/server/db.ts index fd97dab..819f634 100644 --- a/booking-app/components/src/server/db.ts +++ b/booking-app/components/src/server/db.ts @@ -137,7 +137,7 @@ export const cancel = async (id: string, email: string) => { id, getApprovalCcEmail(process.env.NEXT_PUBLIC_BRANCH_NAME), headerMessage, - BookingStatusLabel.NO_SHOW + BookingStatusLabel.CANCELED ); clientSendBookingDetailEmail( id, diff --git a/booking-app/components/src/types.ts b/booking-app/components/src/types.ts index 7901a28..258ca76 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, }