From d23ac73e37f1ffa526380f60a96410791aa04fc0 Mon Sep 17 00:00:00 2001 From: SB1019 Date: Mon, 8 Apr 2024 12:03:31 -0700 Subject: [PATCH 01/74] Writing Architecture for api/user --- backend/src/controllers/user.ts | 9 +++++++++ backend/src/routes/user.ts | 3 ++- frontend/src/api/Users.ts | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/src/controllers/user.ts b/backend/src/controllers/user.ts index 1ff74a2..cbabb41 100644 --- a/backend/src/controllers/user.ts +++ b/backend/src/controllers/user.ts @@ -20,3 +20,12 @@ export const getWhoAmI: RequestHandler = async (req: PAPRequest, res, next) => { next(error); } }; + +export const getUsers: RequestHandler = async (req: PAPRequest, res, next) => { + try { + const users = await UserModel.find(); + res.status(200).send(users); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts index efd4171..be41f9f 100644 --- a/backend/src/routes/user.ts +++ b/backend/src/routes/user.ts @@ -1,10 +1,11 @@ import express from "express"; -import { requireSignedIn } from "src/middleware/auth"; +import { requireSignedIn, requireAdmin } from "src/middleware/auth"; import * as UserController from "src/controllers/user"; const router = express.Router(); router.get("/whoami", requireSignedIn, UserController.getWhoAmI); +router.get("/", requireSignedIn, requireAdmin, UserController.getUsers); export default router; diff --git a/frontend/src/api/Users.ts b/frontend/src/api/Users.ts index 64e76b7..f0819f3 100644 --- a/frontend/src/api/Users.ts +++ b/frontend/src/api/Users.ts @@ -19,3 +19,13 @@ export const getWhoAmI = async (firebaseToken: string): Promise> return handleAPIError(error); } }; + +export const getUsers = async (firebaseToken: string): Promise> => { + try { + const response = await get("/api/user", createAuthHeader(firebaseToken)); + const json = (await response.json()) as User[]; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +}; From 760646a32d59b40b7522d6a19a9717d8ba91e589 Mon Sep 17 00:00:00 2001 From: sydneyzhang18 <114766656+sydneyzhang18@users.noreply.github.com> Date: Mon, 8 Apr 2024 12:40:51 -0700 Subject: [PATCH 02/74] profile page frontend progress --- frontend/public/ic_settings.svg | 5 ++ .../src/app/staff/profile/page.module.css | 34 ++++++++++++ frontend/src/app/staff/profile/page.tsx | 53 +++++++++++++++++++ frontend/src/components/UserProfile/index.tsx | 36 +++++++++++++ .../components/UserProfile/styles.module.css | 44 +++++++++++++++ 5 files changed, 172 insertions(+) create mode 100644 frontend/public/ic_settings.svg create mode 100644 frontend/src/app/staff/profile/page.module.css create mode 100644 frontend/src/app/staff/profile/page.tsx create mode 100644 frontend/src/components/UserProfile/index.tsx create mode 100644 frontend/src/components/UserProfile/styles.module.css diff --git a/frontend/public/ic_settings.svg b/frontend/public/ic_settings.svg new file mode 100644 index 0000000..fed6907 --- /dev/null +++ b/frontend/public/ic_settings.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/app/staff/profile/page.module.css b/frontend/src/app/staff/profile/page.module.css new file mode 100644 index 0000000..b2d4309 --- /dev/null +++ b/frontend/src/app/staff/profile/page.module.css @@ -0,0 +1,34 @@ +.main { + padding: 64px; + padding-top: 128px; + background-color: var(--color-background); + display: flex; + flex-direction: column; + color: var(--Accent-Blue-1, #102d5f); + text-align: left; + + /* justify-content: center; */ + margin: 105px 120px 0; + gap: 40px; + + gap: 32px; + /* Desktop/H1 */ + font-family: Lora; + /* font-size: 20px; */ + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.row { + display: flex; + flex-direction: row; +} + +.title { + font-size: 40px; +} + +.subtitle { + font-size: 24px; +} diff --git a/frontend/src/app/staff/profile/page.tsx b/frontend/src/app/staff/profile/page.tsx new file mode 100644 index 0000000..f0c331f --- /dev/null +++ b/frontend/src/app/staff/profile/page.tsx @@ -0,0 +1,53 @@ +"use client"; + +import styles from "@/app/staff/profile/page.module.css"; +import VSRTable from "@/components/VSRTable/VSRTable"; +import SearchKeyword from "@/components/VSRTable/SearchKeyword"; +import PageTitle from "@/components/VSRTable/PageTitle"; +import HeaderBar from "@/components/shared/HeaderBar"; +import Image from "next/image"; +import React, { useContext, useEffect, useState } from "react"; +import { StatusDropdown } from "@/components/shared/StatusDropdown"; +import { useMediaQuery } from "@mui/material"; +import { useRedirectToLoginIfNotSignedIn } from "@/hooks/useRedirection"; +import { UserContext } from "@/contexts/userContext"; +import { DeleteVSRsModal } from "@/components/shared/DeleteVSRsModal"; +import { VSR, getAllVSRs } from "@/api/VSRs"; +import { VSRErrorModal } from "@/components/VSRForm/VSRErrorModal"; +import { useScreenSizes } from "@/hooks/useScreenSizes"; +import { LoadingScreen } from "@/components/shared/LoadingScreen"; +import { Button } from "@/components/shared/Button"; +import { UserProfile } from "@/components/UserProfile"; +import { User } from "firebase/auth"; + +export default function Profile() { + const { isMobile, isTablet } = useScreenSizes(); + + // useRedirectToLoginIfNotSignedIn(); + + return ( +
+ +
+

Account

+

User Profile

+ {/* ACCOUNT INFO */} + +
+

Manage Users

+
+ + +
+
+ ); +} diff --git a/frontend/src/components/UserProfile/index.tsx b/frontend/src/components/UserProfile/index.tsx new file mode 100644 index 0000000..bf0754d --- /dev/null +++ b/frontend/src/components/UserProfile/index.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import styles from "@/components/UserProfile/styles.module.css"; +import Image from "next/image"; +import { User } from "firebase/auth"; + +export interface UserProps { + name: string; + email: string; +} +export function UserProfile({ name, email }: UserProps) { + return ( +
+ {/*
*/} + Internal Error + {/*
*/} +
+

{name}

+

{email}

+
+ Internal Error +
+ ); +} diff --git a/frontend/src/components/UserProfile/styles.module.css b/frontend/src/components/UserProfile/styles.module.css new file mode 100644 index 0000000..4b52fb1 --- /dev/null +++ b/frontend/src/components/UserProfile/styles.module.css @@ -0,0 +1,44 @@ +.user { + display: flex; + flex-direction: row; + border-radius: 6px; + background: var(--Functional-Background, #f9f9f9); + + display: flex; + padding: 26px 34px; + align-items: flex-start; + gap: 10px; + align-self: stretch; + margin-bottom: 32px; +} + +.info { + display: flex; + flex-direction: column; +} + +.name { + color: var(--Accent-Blue-1, #102d5f); + + /* Desktop/H2 */ + font-family: Lora; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.email { + color: var(--Neutral-Gray6, #484848); + + /* Desktop/Body 2 */ + font-family: "Open Sans"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.pfp { + border-radius: 50%; +} From 35dce850c8ce4e9df23625e597ddd8495ec78eaa Mon Sep 17 00:00:00 2001 From: sydneyzhang18 <114766656+sydneyzhang18@users.noreply.github.com> Date: Mon, 8 Apr 2024 23:50:54 -0700 Subject: [PATCH 03/74] admin info progress, will fix alignment issues --- .../src/app/staff/profile/page.module.css | 26 ++++++- frontend/src/app/staff/profile/page.tsx | 6 +- .../components/Profile/AdminProfile/index.tsx | 45 ++++++++++++ .../Profile/AdminProfile/styles.module.css | 68 +++++++++++++++++++ .../{ => Profile}/UserProfile/index.tsx | 2 +- .../UserProfile/styles.module.css | 3 + 6 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/Profile/AdminProfile/index.tsx create mode 100644 frontend/src/components/Profile/AdminProfile/styles.module.css rename frontend/src/components/{ => Profile}/UserProfile/index.tsx (92%) rename frontend/src/components/{ => Profile}/UserProfile/styles.module.css (91%) diff --git a/frontend/src/app/staff/profile/page.module.css b/frontend/src/app/staff/profile/page.module.css index b2d4309..59b63c3 100644 --- a/frontend/src/app/staff/profile/page.module.css +++ b/frontend/src/app/staff/profile/page.module.css @@ -1,6 +1,9 @@ +.page { + background-color: var(--color-background); +} .main { padding: 64px; - padding-top: 128px; + /* padding-top: 105px; */ background-color: var(--color-background); display: flex; flex-direction: column; @@ -11,7 +14,7 @@ margin: 105px 120px 0; gap: 40px; - gap: 32px; + /* gap: 32px; */ /* Desktop/H1 */ font-family: Lora; /* font-size: 20px; */ @@ -20,11 +23,30 @@ line-height: normal; } +.admin { + display: flex; + flex-direction: row; + border-radius: 6px; + background: var(--Functional-Background, #f9f9f9); + + display: flex; + padding: 26px 34px; + align-items: flex-start; + gap: 10px; + align-self: stretch; + margin-bottom: 32px; +} + .row { display: flex; flex-direction: row; } +.column { + display: flex; + flex-direction: column; +} + .title { font-size: 40px; } diff --git a/frontend/src/app/staff/profile/page.tsx b/frontend/src/app/staff/profile/page.tsx index f0c331f..76b40db 100644 --- a/frontend/src/app/staff/profile/page.tsx +++ b/frontend/src/app/staff/profile/page.tsx @@ -17,8 +17,9 @@ import { VSRErrorModal } from "@/components/VSRForm/VSRErrorModal"; import { useScreenSizes } from "@/hooks/useScreenSizes"; import { LoadingScreen } from "@/components/shared/LoadingScreen"; import { Button } from "@/components/shared/Button"; -import { UserProfile } from "@/components/UserProfile"; +import { UserProfile } from "@/components/Profile/UserProfile"; import { User } from "firebase/auth"; +import { AdminProfile } from "@/components/Profile/AdminProfile"; export default function Profile() { const { isMobile, isTablet } = useScreenSizes(); @@ -32,6 +33,7 @@ export default function Profile() {

Account

User Profile

{/* ACCOUNT INFO */} +

Manage Users

@@ -46,7 +48,7 @@ export default function Profile() { />
- + ); diff --git a/frontend/src/components/Profile/AdminProfile/index.tsx b/frontend/src/components/Profile/AdminProfile/index.tsx new file mode 100644 index 0000000..8d0f7e5 --- /dev/null +++ b/frontend/src/components/Profile/AdminProfile/index.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import styles from "@/components/Profile/AdminProfile/styles.module.css"; +import Image from "next/image"; +import { User } from "firebase/auth"; + +export interface AdminProps { + name: string; + email: string; +} +export function AdminProfile({ name, email }: AdminProps) { + return ( +
+
+
+

Account Information

+ Internal Error{" "} +
+
+ Internal Error +
+

Name

+

{name}

+
+
+

Email Account

+

{email}

+
+
+
+
+ ); +} diff --git a/frontend/src/components/Profile/AdminProfile/styles.module.css b/frontend/src/components/Profile/AdminProfile/styles.module.css new file mode 100644 index 0000000..d2ea2e2 --- /dev/null +++ b/frontend/src/components/Profile/AdminProfile/styles.module.css @@ -0,0 +1,68 @@ +.admin { + display: flex; + flex-direction: row; + border-radius: 6px; + background: var(--Functional-Background, #f9f9f9); + + display: flex; + padding: 32px; + align-items: flex-start; + gap: 10px; + align-self: stretch; + margin-bottom: 64px; +} + +.row { + display: flex; + flex-direction: row; +} + +.column { + display: flex; + flex-direction: column; + gap: 32px; +} + +.info_column { + display: flex; + flex-direction: column; + gap: 16px; +} + +.info_column_right { + display: flex; + flex-direction: column; + gap: 16px; + margin-left: 96px; +} + +.pfp { + border-radius: 50%; + margin-right: 64px; +} + +.subtitle { + font-size: 24px; +} + +.info_type { + color: var(--Dark-Gray, #484848); + + /* Desktop/Body 2 */ + font-family: "Open Sans"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.info { + color: var(--Primary-Background-Dark, #232220); + + /* Desktop/Body 1 */ + font-family: "Open Sans"; + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: normal; +} diff --git a/frontend/src/components/UserProfile/index.tsx b/frontend/src/components/Profile/UserProfile/index.tsx similarity index 92% rename from frontend/src/components/UserProfile/index.tsx rename to frontend/src/components/Profile/UserProfile/index.tsx index bf0754d..e0892ad 100644 --- a/frontend/src/components/UserProfile/index.tsx +++ b/frontend/src/components/Profile/UserProfile/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import styles from "@/components/UserProfile/styles.module.css"; +import styles from "@/components/Profile/UserProfile/styles.module.css"; import Image from "next/image"; import { User } from "firebase/auth"; diff --git a/frontend/src/components/UserProfile/styles.module.css b/frontend/src/components/Profile/UserProfile/styles.module.css similarity index 91% rename from frontend/src/components/UserProfile/styles.module.css rename to frontend/src/components/Profile/UserProfile/styles.module.css index 4b52fb1..bb70858 100644 --- a/frontend/src/components/UserProfile/styles.module.css +++ b/frontend/src/components/Profile/UserProfile/styles.module.css @@ -15,6 +15,7 @@ .info { display: flex; flex-direction: column; + margin-left: 41px; } .name { @@ -26,6 +27,7 @@ font-style: normal; font-weight: 700; line-height: normal; + margin-bottom: 14px; } .email { @@ -41,4 +43,5 @@ .pfp { border-radius: 50%; + margin-left: 22px; } From d231e027b10ddf128069539f6a61525d59b0002e Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Tue, 9 Apr 2024 12:36:09 -0700 Subject: [PATCH 04/74] Initial branch --- backend/src/controllers/furnitureItem.ts | 15 +++++++ .../MultipleChoiceDetail/index.tsx | 43 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 frontend/src/components/VSRIndividual/MultipleChoiceDetail/index.tsx diff --git a/backend/src/controllers/furnitureItem.ts b/backend/src/controllers/furnitureItem.ts index 947e330..2c04121 100644 --- a/backend/src/controllers/furnitureItem.ts +++ b/backend/src/controllers/furnitureItem.ts @@ -19,3 +19,18 @@ export const getFurnitureItems: RequestHandler = async (req, res, next) => { next(error); } }; + +export const createFurnitureItem: RequestHandler = async (req, res, next) => { + try{ + const furnitureItem = await FurnitureItemModel.create({ + ...req.body, + + + }); + } + catch(error){ + next(error); + } +} + + diff --git a/frontend/src/components/VSRIndividual/MultipleChoiceDetail/index.tsx b/frontend/src/components/VSRIndividual/MultipleChoiceDetail/index.tsx new file mode 100644 index 0000000..bb33f0b --- /dev/null +++ b/frontend/src/components/VSRIndividual/MultipleChoiceDetail/index.tsx @@ -0,0 +1,43 @@ +import { Chip } from "@mui/material"; +import MultipleChoice from "@/components/shared/input/MultipleChoice"; +import { Controller, FieldErrors, UseFormReturn } from "react-hook-form"; +import { FieldDetail } from "../FieldDetail"; +import { IFormInput } from "@/app/vsr/page"; + +interface MultipleChoiceDetailProps { + title: string; + options: string[]; + name: keyof IFormInput; + placeholder?: string; + formProps: UseFormReturn; +} + +export const TextInputDetail = ({ + title, + name, + placeholder, + formProps, + ...props +}: TextInputDetailProps) => { + return ( + + ( + + )} + /> + + ); +}; From 3ecaf9ff3ac122825fcd5e08576e6217db8e8b8c Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Tue, 9 Apr 2024 12:38:43 -0700 Subject: [PATCH 05/74] Initial branch --- .../MultipleChoiceDetail/index.tsx | 43 ------------------- 1 file changed, 43 deletions(-) delete mode 100644 frontend/src/components/VSRIndividual/MultipleChoiceDetail/index.tsx diff --git a/frontend/src/components/VSRIndividual/MultipleChoiceDetail/index.tsx b/frontend/src/components/VSRIndividual/MultipleChoiceDetail/index.tsx deleted file mode 100644 index bb33f0b..0000000 --- a/frontend/src/components/VSRIndividual/MultipleChoiceDetail/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Chip } from "@mui/material"; -import MultipleChoice from "@/components/shared/input/MultipleChoice"; -import { Controller, FieldErrors, UseFormReturn } from "react-hook-form"; -import { FieldDetail } from "../FieldDetail"; -import { IFormInput } from "@/app/vsr/page"; - -interface MultipleChoiceDetailProps { - title: string; - options: string[]; - name: keyof IFormInput; - placeholder?: string; - formProps: UseFormReturn; -} - -export const TextInputDetail = ({ - title, - name, - placeholder, - formProps, - ...props -}: TextInputDetailProps) => { - return ( - - ( - - )} - /> - - ); -}; From cb4f3835be57a1777b1c4f2d4436d79e3c92e434 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 9 Apr 2024 13:01:52 -0700 Subject: [PATCH 06/74] added delete api route --- backend/src/controllers/furnitureItem.ts | 13 +++++++++++++ backend/src/routes/furnitureItem.ts | 3 +++ 2 files changed, 16 insertions(+) diff --git a/backend/src/controllers/furnitureItem.ts b/backend/src/controllers/furnitureItem.ts index 2c04121..18ae4ec 100644 --- a/backend/src/controllers/furnitureItem.ts +++ b/backend/src/controllers/furnitureItem.ts @@ -1,4 +1,5 @@ import { RequestHandler } from "express"; +import createHttpError from "http-errors"; import FurnitureItemModel from "src/models/furnitureItem"; /** @@ -33,4 +34,16 @@ export const createFurnitureItem: RequestHandler = async (req, res, next) => { } } +export const deleteFurnitureItem: RequestHandler = async (req, res, next) => { + try { + const { id } = req.params; + const deletedFurnitureItem = await FurnitureItemModel.findByIdAndDelete(id); + if (deletedFurnitureItem === null) { + throw createHttpError(404, "FurnitureItem not found at id " + id); + } + return res.status(204).send(); + } catch (error) { + next(error); + } +} diff --git a/backend/src/routes/furnitureItem.ts b/backend/src/routes/furnitureItem.ts index 3af27bf..4ab521e 100644 --- a/backend/src/routes/furnitureItem.ts +++ b/backend/src/routes/furnitureItem.ts @@ -1,8 +1,11 @@ import express from "express"; +import { requireAdmin, requireSignedIn, requireStaffOrAdmin } from "src/middleware/auth"; import * as FurnitureItemController from "src/controllers/furnitureItem"; const router = express.Router(); router.get("/", FurnitureItemController.getFurnitureItems); +router.delete("/:id", requireSignedIn, requireStaffOrAdmin, FurnitureItemController.deleteFurnitureItem); + export default router; From 9f2b367c048d504eec69ca65998749c7798304be Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Tue, 9 Apr 2024 13:29:15 -0700 Subject: [PATCH 07/74] Made POST route to create furniture item and furniture item validator --- backend/src/controllers/furnitureItem.ts | 9 +++--- backend/src/routes/furnitureItem.ts | 2 ++ backend/src/validators/furnitureItem.ts | 38 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 backend/src/validators/furnitureItem.ts diff --git a/backend/src/controllers/furnitureItem.ts b/backend/src/controllers/furnitureItem.ts index 2c04121..51ee89d 100644 --- a/backend/src/controllers/furnitureItem.ts +++ b/backend/src/controllers/furnitureItem.ts @@ -22,11 +22,10 @@ export const getFurnitureItems: RequestHandler = async (req, res, next) => { export const createFurnitureItem: RequestHandler = async (req, res, next) => { try{ - const furnitureItem = await FurnitureItemModel.create({ - ...req.body, - - - }); + const furnitureItem = await FurnitureItemModel.create( + req.body + ); + res.status(201).json(furnitureItem); } catch(error){ next(error); diff --git a/backend/src/routes/furnitureItem.ts b/backend/src/routes/furnitureItem.ts index 3af27bf..f854f44 100644 --- a/backend/src/routes/furnitureItem.ts +++ b/backend/src/routes/furnitureItem.ts @@ -1,8 +1,10 @@ import express from "express"; import * as FurnitureItemController from "src/controllers/furnitureItem"; +import * as FurnitureItemValidator from "src/validators/furnitureItem"; const router = express.Router(); router.get("/", FurnitureItemController.getFurnitureItems); +router.post("/", FurnitureItemValidator.createFurnitureItem, FurnitureItemController.createFurnitureItem); export default router; diff --git a/backend/src/validators/furnitureItem.ts b/backend/src/validators/furnitureItem.ts new file mode 100644 index 0000000..679e6e6 --- /dev/null +++ b/backend/src/validators/furnitureItem.ts @@ -0,0 +1,38 @@ +import { body } from "express-validator"; + + +const makeCategoryValidator = () => + body("category") + .exists({ checkFalsy: true }) + .withMessage("Category is required") + .isString() + .withMessage("Category must be a string"); + +const makeNameValidator = () => + body("name") + .exists({ checkFalsy: true }) + .withMessage("Name is required") + .isString() + .withMessage("Name must be a string"); + +const makeAllowMultipleValidator = () => + body("allowMultiple") + .exists({ checkFalsy: false }) + .withMessage("Must specify if multiple of these items can be requested") + .isBoolean() + .withMessage("allowMultiple must be a boolean"); + +const makeCategoryIndexValidator = () => + body("categoryIndex") + .exists({ checkFalsy: true }) + .withMessage("Category index is required") + .isInt({ min: 0 }) + .withMessage("Category index must be positive and an integer") + +export const createFurnitureItem = [ + makeCategoryValidator(), + makeNameValidator(), + makeAllowMultipleValidator(), + makeCategoryIndexValidator() +] + From a55c805a3723a864ca46c2518a1e9028e6057d2a Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Tue, 9 Apr 2024 13:35:52 -0700 Subject: [PATCH 08/74] Made POST route to create furniture item and furniture item validator --- backend/src/controllers/furnitureItem.ts | 14 +++--- backend/src/routes/furnitureItem.ts | 16 +++++-- backend/src/validators/furnitureItem.ts | 54 ++++++++++++------------ 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/backend/src/controllers/furnitureItem.ts b/backend/src/controllers/furnitureItem.ts index e90ff30..a2965a9 100644 --- a/backend/src/controllers/furnitureItem.ts +++ b/backend/src/controllers/furnitureItem.ts @@ -22,16 +22,13 @@ export const getFurnitureItems: RequestHandler = async (req, res, next) => { }; export const createFurnitureItem: RequestHandler = async (req, res, next) => { - try{ - const furnitureItem = await FurnitureItemModel.create( - req.body - ); + try { + const furnitureItem = await FurnitureItemModel.create(req.body); res.status(201).json(furnitureItem); - } - catch(error){ + } catch (error) { next(error); } -} +}; export const deleteFurnitureItem: RequestHandler = async (req, res, next) => { try { @@ -44,5 +41,4 @@ export const deleteFurnitureItem: RequestHandler = async (req, res, next) => { } catch (error) { next(error); } -} - +}; diff --git a/backend/src/routes/furnitureItem.ts b/backend/src/routes/furnitureItem.ts index 1354064..900f43f 100644 --- a/backend/src/routes/furnitureItem.ts +++ b/backend/src/routes/furnitureItem.ts @@ -1,13 +1,21 @@ import express from "express"; -import { requireAdmin, requireSignedIn, requireStaffOrAdmin } from "src/middleware/auth"; +import { requireSignedIn, requireStaffOrAdmin } from "src/middleware/auth"; import * as FurnitureItemController from "src/controllers/furnitureItem"; import * as FurnitureItemValidator from "src/validators/furnitureItem"; const router = express.Router(); router.get("/", FurnitureItemController.getFurnitureItems); -router.post("/", FurnitureItemValidator.createFurnitureItem, FurnitureItemController.createFurnitureItem); -router.delete("/:id", requireSignedIn, requireStaffOrAdmin, FurnitureItemController.deleteFurnitureItem); - +router.post( + "/", + FurnitureItemValidator.createFurnitureItem, + FurnitureItemController.createFurnitureItem, +); +router.delete( + "/:id", + requireSignedIn, + requireStaffOrAdmin, + FurnitureItemController.deleteFurnitureItem, +); export default router; diff --git a/backend/src/validators/furnitureItem.ts b/backend/src/validators/furnitureItem.ts index 679e6e6..c2360aa 100644 --- a/backend/src/validators/furnitureItem.ts +++ b/backend/src/validators/furnitureItem.ts @@ -1,38 +1,36 @@ import { body } from "express-validator"; - const makeCategoryValidator = () => - body("category") - .exists({ checkFalsy: true }) - .withMessage("Category is required") - .isString() - .withMessage("Category must be a string"); + body("category") + .exists({ checkFalsy: true }) + .withMessage("Category is required") + .isString() + .withMessage("Category must be a string"); -const makeNameValidator = () => - body("name") - .exists({ checkFalsy: true }) - .withMessage("Name is required") - .isString() - .withMessage("Name must be a string"); +const makeNameValidator = () => + body("name") + .exists({ checkFalsy: true }) + .withMessage("Name is required") + .isString() + .withMessage("Name must be a string"); const makeAllowMultipleValidator = () => - body("allowMultiple") - .exists({ checkFalsy: false }) - .withMessage("Must specify if multiple of these items can be requested") - .isBoolean() - .withMessage("allowMultiple must be a boolean"); + body("allowMultiple") + .exists({ checkFalsy: false }) + .withMessage("Must specify if multiple of these items can be requested") + .isBoolean() + .withMessage("allowMultiple must be a boolean"); const makeCategoryIndexValidator = () => - body("categoryIndex") - .exists({ checkFalsy: true }) - .withMessage("Category index is required") - .isInt({ min: 0 }) - .withMessage("Category index must be positive and an integer") + body("categoryIndex") + .exists({ checkFalsy: true }) + .withMessage("Category index is required") + .isInt({ min: 0 }) + .withMessage("Category index must be positive and an integer"); export const createFurnitureItem = [ - makeCategoryValidator(), - makeNameValidator(), - makeAllowMultipleValidator(), - makeCategoryIndexValidator() -] - + makeCategoryValidator(), + makeNameValidator(), + makeAllowMultipleValidator(), + makeCategoryIndexValidator(), +]; From a0251909b3baa5d18ec5962205a1f19c23fd154b Mon Sep 17 00:00:00 2001 From: sydneyzhang18 <114766656+sydneyzhang18@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:24:17 -0700 Subject: [PATCH 09/74] more frontend, waiting on designer clarification to finalize --- .../src/app/staff/profile/page.module.css | 3 ++- frontend/src/app/staff/profile/page.tsx | 2 +- .../components/Profile/AdminProfile/index.tsx | 2 +- .../Profile/AdminProfile/styles.module.css | 11 +++++++-- .../components/Profile/UserProfile/index.tsx | 24 ++++++++++--------- .../Profile/UserProfile/styles.module.css | 10 ++++++-- 6 files changed, 34 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/staff/profile/page.module.css b/frontend/src/app/staff/profile/page.module.css index 59b63c3..13977ff 100644 --- a/frontend/src/app/staff/profile/page.module.css +++ b/frontend/src/app/staff/profile/page.module.css @@ -16,7 +16,7 @@ /* gap: 32px; */ /* Desktop/H1 */ - font-family: Lora; + font-family: var(--font-title); /* font-size: 20px; */ font-style: normal; font-weight: 700; @@ -40,6 +40,7 @@ .row { display: flex; flex-direction: row; + justify-content: space-between; } .column { diff --git a/frontend/src/app/staff/profile/page.tsx b/frontend/src/app/staff/profile/page.tsx index 76b40db..5b85d14 100644 --- a/frontend/src/app/staff/profile/page.tsx +++ b/frontend/src/app/staff/profile/page.tsx @@ -33,7 +33,7 @@ export default function Profile() {

Account

User Profile

{/* ACCOUNT INFO */} - +

Manage Users

diff --git a/frontend/src/components/Profile/AdminProfile/index.tsx b/frontend/src/components/Profile/AdminProfile/index.tsx index 8d0f7e5..013ec8d 100644 --- a/frontend/src/components/Profile/AdminProfile/index.tsx +++ b/frontend/src/components/Profile/AdminProfile/index.tsx @@ -11,7 +11,7 @@ export function AdminProfile({ name, email }: AdminProps) { return (
-
+

Account Information

{/*
*/} -
-

{name}

-

{email}

+
+
+

{name}

+

{email}

+
+ Internal Error
- Internal Error
); } diff --git a/frontend/src/components/Profile/UserProfile/styles.module.css b/frontend/src/components/Profile/UserProfile/styles.module.css index bb70858..a7b2ed2 100644 --- a/frontend/src/components/Profile/UserProfile/styles.module.css +++ b/frontend/src/components/Profile/UserProfile/styles.module.css @@ -22,7 +22,7 @@ color: var(--Accent-Blue-1, #102d5f); /* Desktop/H2 */ - font-family: Lora; + font-family: var(--font-title); font-size: 24px; font-style: normal; font-weight: 700; @@ -34,7 +34,7 @@ color: var(--Neutral-Gray6, #484848); /* Desktop/Body 2 */ - font-family: "Open Sans"; + font-family: var(--font-open-sans); font-size: 16px; font-style: normal; font-weight: 400; @@ -45,3 +45,9 @@ border-radius: 50%; margin-left: 22px; } + +.row_justify { + display: flex; + flex-direction: row; + justify-content: space-between; +} From 29a3df16ce3977d56049991f7cf6643141776591 Mon Sep 17 00:00:00 2001 From: sydneyzhang18 <114766656+sydneyzhang18@users.noreply.github.com> Date: Sat, 13 Apr 2024 17:30:11 -0700 Subject: [PATCH 10/74] fixed frontend alignment issue --- .../src/app/staff/profile/page.module.css | 26 +++---------- frontend/src/app/staff/profile/page.tsx | 2 +- .../components/Profile/UserProfile/index.tsx | 38 +++++++++---------- .../Profile/UserProfile/styles.module.css | 19 +++++++--- 4 files changed, 39 insertions(+), 46 deletions(-) diff --git a/frontend/src/app/staff/profile/page.module.css b/frontend/src/app/staff/profile/page.module.css index 13977ff..4571975 100644 --- a/frontend/src/app/staff/profile/page.module.css +++ b/frontend/src/app/staff/profile/page.module.css @@ -1,42 +1,28 @@ -.page { +/* .page { background-color: var(--color-background); -} + padding-bottom: 67px; + min-height: 100vh; +} */ .main { padding: 64px; - /* padding-top: 105px; */ + padding-top: 105px; background-color: var(--color-background); display: flex; flex-direction: column; color: var(--Accent-Blue-1, #102d5f); text-align: left; - /* justify-content: center; */ - margin: 105px 120px 0; + padding: 105px 120px 0; gap: 40px; /* gap: 32px; */ /* Desktop/H1 */ font-family: var(--font-title); - /* font-size: 20px; */ font-style: normal; font-weight: 700; line-height: normal; } -.admin { - display: flex; - flex-direction: row; - border-radius: 6px; - background: var(--Functional-Background, #f9f9f9); - - display: flex; - padding: 26px 34px; - align-items: flex-start; - gap: 10px; - align-self: stretch; - margin-bottom: 32px; -} - .row { display: flex; flex-direction: row; diff --git a/frontend/src/app/staff/profile/page.tsx b/frontend/src/app/staff/profile/page.tsx index 5b85d14..e8bf85f 100644 --- a/frontend/src/app/staff/profile/page.tsx +++ b/frontend/src/app/staff/profile/page.tsx @@ -27,7 +27,7 @@ export default function Profile() { // useRedirectToLoginIfNotSignedIn(); return ( -
+

Account

diff --git a/frontend/src/components/Profile/UserProfile/index.tsx b/frontend/src/components/Profile/UserProfile/index.tsx index b16e68b..4a0ce41 100644 --- a/frontend/src/components/Profile/UserProfile/index.tsx +++ b/frontend/src/components/Profile/UserProfile/index.tsx @@ -10,29 +10,29 @@ export interface UserProps { export function UserProfile({ name, email }: UserProps) { return (
- {/*
*/} - Internal Error - {/*
*/} -
-
-

{name}

-

{email}

-
+
Internal Error
+
+
+

{name}

+ Internal Error +
+

{email}

+
); } diff --git a/frontend/src/components/Profile/UserProfile/styles.module.css b/frontend/src/components/Profile/UserProfile/styles.module.css index a7b2ed2..332d5fc 100644 --- a/frontend/src/components/Profile/UserProfile/styles.module.css +++ b/frontend/src/components/Profile/UserProfile/styles.module.css @@ -12,12 +12,6 @@ margin-bottom: 32px; } -.info { - display: flex; - flex-direction: column; - margin-left: 41px; -} - .name { color: var(--Accent-Blue-1, #102d5f); @@ -50,4 +44,17 @@ display: flex; flex-direction: row; justify-content: space-between; + width: 100%; +} + +.column { + display: flex; + flex-direction: column; +} + +.column_right { + display: flex; + flex-direction: column; + width: 100%; + margin-left: 41px; } From e76c2785586db691c5fba0d88c4a4874ae8bf429 Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Tue, 16 Apr 2024 12:36:06 -0700 Subject: [PATCH 11/74] Added PUT route for updating furniture items --- backend/src/controllers/furnitureItem.ts | 25 ++++++++++++++++++++++++ backend/src/routes/furnitureItem.ts | 9 +++++++++ backend/src/validators/furnitureItem.ts | 7 +++++++ 3 files changed, 41 insertions(+) diff --git a/backend/src/controllers/furnitureItem.ts b/backend/src/controllers/furnitureItem.ts index a2965a9..a5f1049 100644 --- a/backend/src/controllers/furnitureItem.ts +++ b/backend/src/controllers/furnitureItem.ts @@ -1,6 +1,8 @@ import { RequestHandler } from "express"; +import { validationResult } from "express-validator"; import createHttpError from "http-errors"; import FurnitureItemModel from "src/models/furnitureItem"; +import validationErrorParser from "src/util/validationErrorParser"; /** * Gets all available furniture items in the database. Does not require authentication. @@ -22,7 +24,9 @@ export const getFurnitureItems: RequestHandler = async (req, res, next) => { }; export const createFurnitureItem: RequestHandler = async (req, res, next) => { + const errors = validationResult(req); try { + validationErrorParser(errors); const furnitureItem = await FurnitureItemModel.create(req.body); res.status(201).json(furnitureItem); } catch (error) { @@ -42,3 +46,24 @@ export const deleteFurnitureItem: RequestHandler = async (req, res, next) => { next(error); } }; + +export const updateFurnitureItem: RequestHandler = async (req, res, next) => { + const errors = validationResult(req); + try { + const { id } = req.params; + + validationErrorParser(errors); + + const updatedFurnitureItem = await FurnitureItemModel.findByIdAndUpdate(id, req.body, { + new: true, + }); + + if (updatedFurnitureItem == null) { + throw createHttpError(404, "Furniture Item not found at id " + id); + } + + res.status(200).json(updatedFurnitureItem); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/routes/furnitureItem.ts b/backend/src/routes/furnitureItem.ts index 900f43f..bf15458 100644 --- a/backend/src/routes/furnitureItem.ts +++ b/backend/src/routes/furnitureItem.ts @@ -8,6 +8,8 @@ const router = express.Router(); router.get("/", FurnitureItemController.getFurnitureItems); router.post( "/", + requireSignedIn, + requireStaffOrAdmin, FurnitureItemValidator.createFurnitureItem, FurnitureItemController.createFurnitureItem, ); @@ -17,5 +19,12 @@ router.delete( requireStaffOrAdmin, FurnitureItemController.deleteFurnitureItem, ); +router.put( + "/:id", + requireSignedIn, + requireStaffOrAdmin, + FurnitureItemValidator.updateFurnitureItem, + FurnitureItemController.updateFurnitureItem, +); export default router; diff --git a/backend/src/validators/furnitureItem.ts b/backend/src/validators/furnitureItem.ts index c2360aa..167d460 100644 --- a/backend/src/validators/furnitureItem.ts +++ b/backend/src/validators/furnitureItem.ts @@ -34,3 +34,10 @@ export const createFurnitureItem = [ makeAllowMultipleValidator(), makeCategoryIndexValidator(), ]; + +export const updateFurnitureItem = [ + makeCategoryValidator(), + makeNameValidator(), + makeAllowMultipleValidator(), + makeCategoryIndexValidator(), +]; From 2442c8dce668ae30136cae1977bf7f0eded46ee3 Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Tue, 16 Apr 2024 13:29:05 -0700 Subject: [PATCH 12/74] Initial frontend and components structure --- .../app/staff/furnitureItems/page.module.css | 42 +++++++++++++++++++ .../src/app/staff/furnitureItems/page.tsx | 18 ++++++++ 2 files changed, 60 insertions(+) create mode 100644 frontend/src/app/staff/furnitureItems/page.module.css create mode 100644 frontend/src/app/staff/furnitureItems/page.tsx diff --git a/frontend/src/app/staff/furnitureItems/page.module.css b/frontend/src/app/staff/furnitureItems/page.module.css new file mode 100644 index 0000000..b36ea00 --- /dev/null +++ b/frontend/src/app/staff/furnitureItems/page.module.css @@ -0,0 +1,42 @@ +.title { + font-size: 40px; +} + +.description { + color: #000; + /* Desktop/Body 1 */ + font-family: "Open Sans"; + font-size: 20px; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.main { + padding: 64px; + padding-top: 128px; + background-color: #eceff3; + display: flex; + flex-direction: column; + color: var(--Accent-Blue-1, #102d5f); + text-align: left; + gap: 32px; + /* Desktop/H1 */ + font-family: Lora; + font-size: 20px; + font-style: normal; + font-weight: 700; + line-height: normal; +} + +.formContainer { + display: flex; + background-color: white; + border-radius: 20px; + padding: 64px; + margin-top: 32px; + gap: 10px; + align-items: flex-start; + width: 100%; + height: 100%; +} diff --git a/frontend/src/app/staff/furnitureItems/page.tsx b/frontend/src/app/staff/furnitureItems/page.tsx new file mode 100644 index 0000000..cab39af --- /dev/null +++ b/frontend/src/app/staff/furnitureItems/page.tsx @@ -0,0 +1,18 @@ +"use client"; +import HeaderBar from "@/components/shared/HeaderBar"; +import styles from "src/app/staff/furnitureItems/page.module.css"; + +export default function furnitureItemTemplate() { + return ( +
+ +

Furnishing Request Form Template

+

+ Adding, editing, and removing tags. Remember to save your edits after adding or removing + furnishing options for future VSR forms. +

+ +
+
+ ); +} From 51eb55a6434fe2b75a134b5e2fa14116195e3f2f Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Thu, 18 Apr 2024 19:43:09 -0700 Subject: [PATCH 13/74] Fixed Header bar --- frontend/src/app/staff/furnitureItems/page.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/staff/furnitureItems/page.tsx b/frontend/src/app/staff/furnitureItems/page.tsx index cab39af..67e59e5 100644 --- a/frontend/src/app/staff/furnitureItems/page.tsx +++ b/frontend/src/app/staff/furnitureItems/page.tsx @@ -4,15 +4,17 @@ import styles from "src/app/staff/furnitureItems/page.module.css"; export default function furnitureItemTemplate() { return ( -
+ <> -

Furnishing Request Form Template

-

- Adding, editing, and removing tags. Remember to save your edits after adding or removing - furnishing options for future VSR forms. -

+
+

Furnishing Request Form Template

+

+ Adding, editing, and removing tags. Remember to save your edits after adding or removing + furnishing options for future VSR forms. +

-
-
+
+
+ ); } From b457985a72f81673d371448a371ff12fa63056f9 Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Tue, 23 Apr 2024 13:38:42 -0700 Subject: [PATCH 14/74] Edit template component --- .../app/staff/furnitureItems/page.module.css | 9 +++++ .../src/app/staff/furnitureItems/page.tsx | 35 ++++++++++++++++++- frontend/src/app/vsr/page.tsx | 1 + .../FurnitureRequest/EditTemplate/index.tsx | 33 +++++++++++++++++ .../VSRForm/FurnitureItemSelection/index.tsx | 34 +++++++++++------- .../RequestedFurnishings/index.tsx | 1 + 6 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/FurnitureRequest/EditTemplate/index.tsx diff --git a/frontend/src/app/staff/furnitureItems/page.module.css b/frontend/src/app/staff/furnitureItems/page.module.css index b36ea00..2248fb4 100644 --- a/frontend/src/app/staff/furnitureItems/page.module.css +++ b/frontend/src/app/staff/furnitureItems/page.module.css @@ -40,3 +40,12 @@ width: 100%; height: 100%; } + +.sectionTitle { + color: var(--Accent-Blue-1, #102d5f); + font-family: Lora; + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: normal; +} diff --git a/frontend/src/app/staff/furnitureItems/page.tsx b/frontend/src/app/staff/furnitureItems/page.tsx index 67e59e5..cbe190f 100644 --- a/frontend/src/app/staff/furnitureItems/page.tsx +++ b/frontend/src/app/staff/furnitureItems/page.tsx @@ -1,8 +1,34 @@ "use client"; import HeaderBar from "@/components/shared/HeaderBar"; import styles from "src/app/staff/furnitureItems/page.module.css"; +import { EditTemplate } from "@/components/FurnitureRequest/EditTemplate"; +import { useMemo } from "react"; +import { FurnitureItem, getFurnitureItems } from "@/api/FurnitureItems"; +import React, { useEffect, useState } from "react"; export default function furnitureItemTemplate() { + const [furnitureItems, setFurnitureItems] = useState(); + + useEffect(() => { + getFurnitureItems().then((result) => { + if (result.success) { + setFurnitureItems(result.data); + } + }); + }, []); + + const furnitureCategoriesToItems = useMemo( + () => + furnitureItems?.reduce( + (prevMap: Record, curItem) => ({ + ...prevMap, + [curItem.category]: [...(prevMap[curItem.category] ?? []), curItem], + }), + {}, + ), + [furnitureItems], + ); + return ( <> @@ -13,7 +39,14 @@ export default function furnitureItemTemplate() { furnishing options for future VSR forms.

-
+
+

Furnishings

+ +
); diff --git a/frontend/src/app/vsr/page.tsx b/frontend/src/app/vsr/page.tsx index 4eadbf1..b3fbb53 100644 --- a/frontend/src/app/vsr/page.tsx +++ b/frontend/src/app/vsr/page.tsx @@ -1069,6 +1069,7 @@ const VeteranServiceRequest: React.FC = () => {
{(items ?? []).map((furnitureItem) => ( { + return ( +
+ +
+ {furnitureItems.map((furnitureItem) => ( + + ))} +
+
+
+ ); +}; diff --git a/frontend/src/components/VSRForm/FurnitureItemSelection/index.tsx b/frontend/src/components/VSRForm/FurnitureItemSelection/index.tsx index 42656bf..6234b6d 100644 --- a/frontend/src/components/VSRForm/FurnitureItemSelection/index.tsx +++ b/frontend/src/components/VSRForm/FurnitureItemSelection/index.tsx @@ -5,8 +5,9 @@ import Image from "next/image"; export interface FurnitureItemSelectionProps { furnitureItem: FurnitureItem; - selection: FurnitureInput; - onChangeSelection: (newSelection: FurnitureInput) => unknown; + selection?: FurnitureInput; + onChangeSelection?: (newSelection: FurnitureInput) => unknown; + isActive: boolean; } /** @@ -17,29 +18,36 @@ export const FurnitureItemSelection = ({ furnitureItem, selection, onChangeSelection, + isActive, }: FurnitureItemSelectionProps) => { const handleChipClicked = () => { - if (selection.quantity === 0) { - incrementCount(); - } else if (!furnitureItem.allowMultiple) { - onChangeSelection({ ...selection, quantity: 0 }); + if (isActive) { + if (selection!.quantity === 0) { + incrementCount(); + } else if (!furnitureItem.allowMultiple) { + onChangeSelection!({ ...selection!, quantity: 0 }); + } } }; const incrementCount = () => { - onChangeSelection({ ...selection, quantity: selection.quantity + 1 }); + if (isActive) { + onChangeSelection!({ ...selection!, quantity: selection!.quantity + 1 }); + } }; const decrementCount = () => { - if (selection.quantity > 0) { - onChangeSelection({ ...selection, quantity: selection.quantity - 1 }); + if (isActive) { + if (selection!.quantity > 0) { + onChangeSelection!({ ...selection!, quantity: selection!.quantity - 1 }); + } } }; return (
0 ? styles.chipSelected : styles.chipUnselected + selection && selection.quantity > 0 ? styles.chipSelected : styles.chipUnselected }`} onClick={handleChipClicked} > @@ -57,7 +65,7 @@ export const FurnitureItemSelection = ({ > 0 ? styles.decSelected : styles.dec + selection && selection.quantity > 0 ? styles.decSelected : styles.dec }`} src="/icon_minus.svg" width={22} @@ -65,7 +73,7 @@ export const FurnitureItemSelection = ({ alt="dropdown" /> - {selection.quantity} + {selection?.quantity ?? 0}
); }; + +// The button is currently being styled by styles.chip, although its not completed +// and I'm not sure that's where we want to style the button. and it doesn't do anything \ No newline at end of file diff --git a/frontend/src/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css b/frontend/src/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css index ae72e2e..f4979d6 100644 --- a/frontend/src/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css +++ b/frontend/src/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css @@ -14,3 +14,21 @@ flex-wrap: wrap; gap: 16px; } + +.chip { + white-space: nowrap; + text-align: center; + font-family: "OpenSans"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + border-radius: 64px; + border: 1px solid var(--Secondary-1, #102d5f); + background-color:#102d5f; +} + +.chip:hover { + cursor: pointer; + background: #c7def1; +} \ No newline at end of file From d709394ddba9251defa3297d5c8263012291e3d3 Mon Sep 17 00:00:00 2001 From: 2s2e Date: Tue, 30 Apr 2024 22:43:12 -0700 Subject: [PATCH 20/74] temp commit --- backend/src/controllers/vsr.ts | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts index 305a3c5..5c401db 100644 --- a/backend/src/controllers/vsr.ts +++ b/backend/src/controllers/vsr.ts @@ -13,14 +13,35 @@ import validationErrorParser from "src/util/validationErrorParser"; * staff or admin permission. */ export const getAllVSRS: RequestHandler = async (req, res, next) => { + const pipeline = [ + { + $addFields: { + sort_order: { + $switch: { + branches: [ + { case: { $eq: ["$status", "Received"] }, then: 1 }, + { case: { $eq: ["$status", "Approved"] }, then: 2 }, + { case: { $eq: ["$status", "Appointment Scheduled"] }, then: 3 }, + { case: { $eq: ["$status", "Complete"] }, then: 4 }, + { case: { $eq: ["$status", "No-show / Incomplete"] }, then: 5 }, + { case: { $eq: ["$status", "Archived"] }, then: 6 }, + ], + default: 7, + }, + }, + }, + }, + { + $sort: { sort_order: 1, date_received: -1 }, + }, + { + $project: { sort_order: 0 }, + }, + ]; + try { if (req.query.search) { - const vsrs = await VSRModel.find({ $text: { $search: req.query.search as string } }).sort({ - //by status in a particular order - status: 1, - //and then by name - name: 1, - }); + const vsrs = await VSRModel.find({ $text: { $search: req.query.search as string } }); res.status(200).json({ vsrs }); } else { const vsrs = await VSRModel.find().sort({ From 365f6bf93512797d261ccb00bb665cbbca8e5f07 Mon Sep 17 00:00:00 2001 From: 2s2e Date: Wed, 1 May 2024 10:33:09 -0700 Subject: [PATCH 21/74] Revert "added searchability on frontend; bug: can only search by name, and needs to be entire name for a result to hit" This reverts commit c891e663cf078eee3967e748cd0c750a98d29e0e. --- frontend/src/app/staff/vsr/page.tsx | 29 +------- .../VSRTable/SearchKeyword/index.tsx | 67 ++----------------- 2 files changed, 7 insertions(+), 89 deletions(-) diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 49d019a..bd5c5e8 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -2,7 +2,7 @@ import styles from "@/app/staff/vsr/page.module.css"; import VSRTable from "@/components/VSRTable/VSRTable"; -import { SearchKeyword } from "@/components/VSRTable/SearchKeyword"; +import SearchKeyword from "@/components/VSRTable/SearchKeyword"; import PageTitle from "@/components/VSRTable/PageTitle"; import HeaderBar from "@/components/shared/HeaderBar"; import Image from "next/image"; @@ -74,29 +74,6 @@ export default function VSRTableView() { fetchVSRs(); }, [firebaseUser]); - const fetchSearchedVSRs = (input: string) => { - if (!firebaseUser) { - return; - } - - setLoadingVsrs(true); - firebaseUser?.getIdToken().then((firebaseToken) => { - getAllVSRs(firebaseToken, input).then((result) => { - if (result.success) { - setVsrs(result.data); - } else { - if (result.error === "Failed to fetch") { - setTableError(VSRTableError.CANNOT_FETCH_VSRS_NO_INTERNET); - } else { - console.error(`Error retrieving VSRs: ${result.error}`); - setTableError(VSRTableError.CANNOT_FETCH_VSRS_INTERNAL); - } - } - setLoadingVsrs(false); - }); - }); - }; - /** * Renders an error modal corresponding to the page's error state, or renders * nothing if there is no error. @@ -163,7 +140,7 @@ export default function VSRTableView() {
- {searchOnOwnRow ? null : } + {searchOnOwnRow ? null : }

Status:

@@ -210,7 +187,7 @@ export default function VSRTableView() { />
- {searchOnOwnRow ? : null} + {searchOnOwnRow ? : null}
{loadingVsrs ? ( diff --git a/frontend/src/components/VSRTable/SearchKeyword/index.tsx b/frontend/src/components/VSRTable/SearchKeyword/index.tsx index ca791b1..cf92950 100644 --- a/frontend/src/components/VSRTable/SearchKeyword/index.tsx +++ b/frontend/src/components/VSRTable/SearchKeyword/index.tsx @@ -1,75 +1,16 @@ import styles from "@/components/VSRTable/SearchKeyword/styles.module.css"; import Image from "next/image"; -import { UserContext } from "@/contexts/userContext"; -import React, { useContext, useState } from "react"; -import { getAllVSRs } from "@/api/VSRs"; +import * as React from "react"; /** * A component for the Search input above the VSR table. */ - -interface SearchProps { - // eslint-disable-next-line @typescript-eslint/ban-types - fetchFunction: Function; -} - -export const SearchKeyword = ({ fetchFunction }: SearchProps) => { - const [searchInput, setSearchInput] = useState(""); - - const handleInputChange = (event: { target: { value: string } }) => { - const input = event.target.value; - setSearchInput(input); - fetchFunction(input); - }; - +export default function SearchKeyword() { return (
{/* image */} Search - +
); -}; - -// export default function SearchKeyword(fetchFunction: (input: string) => void) { -// const { firebaseUser } = useContext(UserContext); -// const [searchInput, setSearchInput] = useState(""); - -// const handleInputChange = (event: { target: { value: string } }) => { -// const input = event.target.value; -// setSearchInput(input); -// fetchFunction(input); -// }; - -// // const fetchSearchedVSRs = (input) => { -// // if (!firebaseUser) { -// // return; -// // } - -// // firebaseUser?.getIdToken().then((firebaseToken) => { -// // getAllVSRs(firebaseToken, input).then((result) => { -// // if (result.success) { - -// // } -// // }); -// // }); -// // }; - -// return ( -//
-// {/* image */} -// Search -// -//
-// ); -// } +} From 8b39a020e06a500af7657a5cd64e2d4857567b09 Mon Sep 17 00:00:00 2001 From: 2s2e Date: Wed, 1 May 2024 10:44:33 -0700 Subject: [PATCH 22/74] Revert "Revert "added searchability on frontend; bug: can only search by name, and needs to be entire name for a result to hit"" This reverts commit 365f6bf93512797d261ccb00bb665cbbca8e5f07. --- frontend/src/app/staff/vsr/page.tsx | 29 +++++++- .../VSRTable/SearchKeyword/index.tsx | 67 +++++++++++++++++-- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index bd5c5e8..49d019a 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -2,7 +2,7 @@ import styles from "@/app/staff/vsr/page.module.css"; import VSRTable from "@/components/VSRTable/VSRTable"; -import SearchKeyword from "@/components/VSRTable/SearchKeyword"; +import { SearchKeyword } from "@/components/VSRTable/SearchKeyword"; import PageTitle from "@/components/VSRTable/PageTitle"; import HeaderBar from "@/components/shared/HeaderBar"; import Image from "next/image"; @@ -74,6 +74,29 @@ export default function VSRTableView() { fetchVSRs(); }, [firebaseUser]); + const fetchSearchedVSRs = (input: string) => { + if (!firebaseUser) { + return; + } + + setLoadingVsrs(true); + firebaseUser?.getIdToken().then((firebaseToken) => { + getAllVSRs(firebaseToken, input).then((result) => { + if (result.success) { + setVsrs(result.data); + } else { + if (result.error === "Failed to fetch") { + setTableError(VSRTableError.CANNOT_FETCH_VSRS_NO_INTERNET); + } else { + console.error(`Error retrieving VSRs: ${result.error}`); + setTableError(VSRTableError.CANNOT_FETCH_VSRS_INTERNAL); + } + } + setLoadingVsrs(false); + }); + }); + }; + /** * Renders an error modal corresponding to the page's error state, or renders * nothing if there is no error. @@ -140,7 +163,7 @@ export default function VSRTableView() {
- {searchOnOwnRow ? null : } + {searchOnOwnRow ? null : }

Status:

@@ -187,7 +210,7 @@ export default function VSRTableView() { />
- {searchOnOwnRow ? : null} + {searchOnOwnRow ? : null}
{loadingVsrs ? ( diff --git a/frontend/src/components/VSRTable/SearchKeyword/index.tsx b/frontend/src/components/VSRTable/SearchKeyword/index.tsx index cf92950..ca791b1 100644 --- a/frontend/src/components/VSRTable/SearchKeyword/index.tsx +++ b/frontend/src/components/VSRTable/SearchKeyword/index.tsx @@ -1,16 +1,75 @@ import styles from "@/components/VSRTable/SearchKeyword/styles.module.css"; import Image from "next/image"; -import * as React from "react"; +import { UserContext } from "@/contexts/userContext"; +import React, { useContext, useState } from "react"; +import { getAllVSRs } from "@/api/VSRs"; /** * A component for the Search input above the VSR table. */ -export default function SearchKeyword() { + +interface SearchProps { + // eslint-disable-next-line @typescript-eslint/ban-types + fetchFunction: Function; +} + +export const SearchKeyword = ({ fetchFunction }: SearchProps) => { + const [searchInput, setSearchInput] = useState(""); + + const handleInputChange = (event: { target: { value: string } }) => { + const input = event.target.value; + setSearchInput(input); + fetchFunction(input); + }; + return (
{/* image */} Search - +
); -} +}; + +// export default function SearchKeyword(fetchFunction: (input: string) => void) { +// const { firebaseUser } = useContext(UserContext); +// const [searchInput, setSearchInput] = useState(""); + +// const handleInputChange = (event: { target: { value: string } }) => { +// const input = event.target.value; +// setSearchInput(input); +// fetchFunction(input); +// }; + +// // const fetchSearchedVSRs = (input) => { +// // if (!firebaseUser) { +// // return; +// // } + +// // firebaseUser?.getIdToken().then((firebaseToken) => { +// // getAllVSRs(firebaseToken, input).then((result) => { +// // if (result.success) { + +// // } +// // }); +// // }); +// // }; + +// return ( +//
+// {/* image */} +// Search +// +//
+// ); +// } From b167ec725df751e9bc5565b07fb6514147abbde3 Mon Sep 17 00:00:00 2001 From: 2s2e Date: Wed, 1 May 2024 11:09:24 -0700 Subject: [PATCH 23/74] pasted harsh's original vsr.ts in, hopefully will solve merge issues --- backend/src/controllers/vsr.ts | 56 ++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts index 636e3ef..ac20dba 100644 --- a/backend/src/controllers/vsr.ts +++ b/backend/src/controllers/vsr.ts @@ -13,35 +13,39 @@ import validationErrorParser from "src/util/validationErrorParser"; * staff or admin permission. */ export const getAllVSRS: RequestHandler = async (req, res, next) => { - const pipeline = [ - { - $addFields: { - sort_order: { - $switch: { - branches: [ - { case: { $eq: ["$status", "Received"] }, then: 1 }, - { case: { $eq: ["$status", "Approved"] }, then: 2 }, - { case: { $eq: ["$status", "Appointment Scheduled"] }, then: 3 }, - { case: { $eq: ["$status", "Complete"] }, then: 4 }, - { case: { $eq: ["$status", "No-show / Incomplete"] }, then: 5 }, - { case: { $eq: ["$status", "Archived"] }, then: 6 }, - ], - default: 7, + try { + if (req.query.search) { + // const vsrs = await VSRModel.find({ $text: { $search: req.query.search as string } }).sort({ + // //by status in a particular order + // status: 1, + // //and then by name + // name: 1, + // }); + + const vsrs = await VSRModel.aggregate([ + { + $match: { $text: { $search: req.query.search as string } }, + }, + { + $addFields: { + statusOrder: { + $switch: { + branches: [ + { case: { $eq: ["$status", "Received"] }, then: 1 }, + { case: { $eq: ["$status", "Approved"] }, then: 2 }, + { case: { $eq: ["$status", "Appointment Scheduled"] }, then: 3 }, + { case: { $eq: ["$status", "Complete"] }, then: 4 }, + { case: { $eq: ["$status", "No-show / Incomplete"] }, then: 5 }, + { case: { $eq: ["$status", "Archived"] }, then: 6 }, + ], + default: 99, + }, + }, }, }, - }, - }, - { - $sort: { sort_order: 1, date_received: -1 }, - }, - { - $project: { sort_order: 0 }, - }, - ]; + { $sort: { statusOrder: 1, dateReceived: -1 } }, + ]); - try { - if (req.query.search) { - const vsrs = await VSRModel.find({ $text: { $search: req.query.search as string } }); res.status(200).json({ vsrs }); } else { // const vsrs = await VSRModel.find().sort({ From f540b60c1441c7842b16488c8be778c414d13a7c Mon Sep 17 00:00:00 2001 From: 2s2e Date: Wed, 1 May 2024 12:31:41 -0700 Subject: [PATCH 24/74] Added draft of backend code for testing --- backend/src/controllers/vsr.ts | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts index ac20dba..9eeb37b 100644 --- a/backend/src/controllers/vsr.ts +++ b/backend/src/controllers/vsr.ts @@ -14,6 +14,7 @@ import validationErrorParser from "src/util/validationErrorParser"; */ export const getAllVSRS: RequestHandler = async (req, res, next) => { try { + let vsrs; if (req.query.search) { // const vsrs = await VSRModel.find({ $text: { $search: req.query.search as string } }).sort({ // //by status in a particular order @@ -22,7 +23,7 @@ export const getAllVSRS: RequestHandler = async (req, res, next) => { // name: 1, // }); - const vsrs = await VSRModel.aggregate([ + vsrs = await VSRModel.aggregate([ { $match: { $text: { $search: req.query.search as string } }, }, @@ -45,8 +46,6 @@ export const getAllVSRS: RequestHandler = async (req, res, next) => { }, { $sort: { statusOrder: 1, dateReceived: -1 } }, ]); - - res.status(200).json({ vsrs }); } else { // const vsrs = await VSRModel.find().sort({ // //by status in a particular order @@ -55,7 +54,7 @@ export const getAllVSRS: RequestHandler = async (req, res, next) => { // dateReceived: -1, // }); - const vsrs = await VSRModel.aggregate([ + vsrs = await VSRModel.aggregate([ { $addFields: { statusOrder: { @@ -75,9 +74,34 @@ export const getAllVSRS: RequestHandler = async (req, res, next) => { }, { $sort: { statusOrder: 1, dateReceived: -1 } }, ]); + } + + if (req.query.status) { + vsrs = vsrs.filter((vsr) => vsr.status === req.query.status); + } - res.status(200).json({ vsrs }); + if ( + req.query.incomeLevel && + typeof req.query.incomeLevel === "string" && + req.query.incomeLevel in ["50000", "25000", "12500", "0"] + ) { + const incomeMap: { [key: string]: string } = { + "50000": "$50,000 and above", + "25000": "$25,000 - $50,000", + "12500": "$12,500 - $25,000", + "0": "Below $12,500", + }; + + vsrs = vsrs.filter((vsr) => vsr.incomeLevel === incomeMap[req.query.incomeLevel as string]); } + + if (req.query.zipCode) { + //we expect a list of zipcodes + const zipCodes = req.query.zipCode as string[]; + vsrs = vsrs.filter((vsr) => zipCodes.includes(vsr.zipCode.toString())); + } + + res.status(200).json({ vsrs }); } catch (error) { next(error); } From 471cc831bbd01c7fcb5a48d4a2fa1b1cc39b9db8 Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Thu, 2 May 2024 13:26:03 -0700 Subject: [PATCH 25/74] Added props for editing ability for EditTemplate and added comments for rendering series of EditTemplates --- .../app/staff/furnitureItems/page.module.css | 11 +++- .../src/app/staff/furnitureItems/page.tsx | 66 +++++++++++++++---- .../FurnitureRequest/EditTemplate/index.tsx | 51 ++++++++++++-- .../RequestedFurnishings/styles.module.css | 4 +- 4 files changed, 113 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/staff/furnitureItems/page.module.css b/frontend/src/app/staff/furnitureItems/page.module.css index 2248fb4..a68cdc1 100644 --- a/frontend/src/app/staff/furnitureItems/page.module.css +++ b/frontend/src/app/staff/furnitureItems/page.module.css @@ -31,11 +31,11 @@ .formContainer { display: flex; + flex-direction: column; background-color: white; border-radius: 20px; padding: 64px; margin-top: 32px; - gap: 10px; align-items: flex-start; width: 100%; height: 100%; @@ -48,4 +48,13 @@ font-style: normal; font-weight: 700; line-height: normal; + padding-bottom: 32px; +} + +.furnishings { + display: flex; + width: 100%; + flex-direction: column; + gap: 64px; + align-self: flex-start; } diff --git a/frontend/src/app/staff/furnitureItems/page.tsx b/frontend/src/app/staff/furnitureItems/page.tsx index 135a4ff..5955d95 100644 --- a/frontend/src/app/staff/furnitureItems/page.tsx +++ b/frontend/src/app/staff/furnitureItems/page.tsx @@ -8,11 +8,13 @@ import React, { useEffect, useState } from "react"; export default function furnitureItemTemplate() { const [furnitureItems, setFurnitureItems] = useState(); - + const [editingCategory, setEditingCategory] = useState(); useEffect(() => { getFurnitureItems().then((result) => { if (result.success) { setFurnitureItems(result.data); + } else { + setFurnitureItems([]); } }); }, []); @@ -29,6 +31,14 @@ export default function furnitureItemTemplate() { [furnitureItems], ); + const handleBeginEditing = (category: string) => { + setEditingCategory(category); + }; + + const handleFinishEditing = () => { + setEditingCategory(undefined); + }; + return ( <> @@ -41,20 +51,52 @@ export default function furnitureItemTemplate() {

Furnishings

- - +
+ {/* Possible way to render a series of EditTemplate components + {Object.entries(furnitureCategoriesToItems).map(([category, items]) => ( + handleBeginEditing(category)} + onFinishEditing={handleFinishEditing} + /> + ))} + */} + + + + + + +
); } -// add more furniture items, such as bedroom \ No newline at end of file +// add more furniture items, such as bedroom diff --git a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx index a753cd5..c1653a2 100644 --- a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx +++ b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx @@ -1,5 +1,6 @@ import styles from "@/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css"; import { FurnitureItem } from "@/api/FurnitureItems"; +import { useState } from "react"; import { useMemo } from "react"; import { FurnitureItemSelection } from "@/components/VSRForm/FurnitureItemSelection"; import { FieldDetail } from "@/components/VSRIndividual/FieldDetails/FieldDetail"; @@ -8,13 +9,39 @@ export interface EditTemplateProps { furnitureItems: FurnitureItem[]; categoryTitle: string; categoryName: string; + isEditing: boolean; + isDisabled: boolean; + onBeginEditing: () => void; + onFinishEditing: () => void; } export const EditTemplate = ({ furnitureItems, categoryTitle, categoryName, + isEditing, + isDisabled, + onBeginEditing, + onFinishEditing, }: EditTemplateProps) => { + const [isAddingNewItem, setIsAddingNewItem] = useState(false); + const [editingItemId, setEditingItemId] = useState(null); + const handleStartEditItem = (itemId: string) => { + setEditingItemId(itemId); + }; + + const handleStopEditItem = () => { + setEditingItemId(null); + }; + + const handleAddNewItem = () => { + setIsAddingNewItem(true); + }; + + const handleFinishAddNewItem = () => { + setIsAddingNewItem(false); + }; + return (
@@ -26,9 +53,20 @@ export const EditTemplate = ({ furnitureItem={furnitureItem} /> ))} - + +
@@ -36,4 +74,9 @@ export const EditTemplate = ({ }; // The button is currently being styled by styles.chip, although its not completed -// and I'm not sure that's where we want to style the button. and it doesn't do anything \ No newline at end of file +// and I'm not sure that's where we want to style the button. and it doesn't do anything + +//const[isEditing, setIsEditing] = useState(false); +//const handleEditClick = () => { +// setIsEditing(current => !current); +// console.log('Button clicked for furniture item:', categoryTitle); }}>Edit Section diff --git a/frontend/src/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css b/frontend/src/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css index f4979d6..65a9aa4 100644 --- a/frontend/src/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css +++ b/frontend/src/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css @@ -25,10 +25,10 @@ line-height: normal; border-radius: 64px; border: 1px solid var(--Secondary-1, #102d5f); - background-color:#102d5f; + background-color: #102d5f; } .chip:hover { cursor: pointer; background: #c7def1; -} \ No newline at end of file +} From 2c66a2af06074c942a526684db440ba28cf404c9 Mon Sep 17 00:00:00 2001 From: 2s2e Date: Thu, 2 May 2024 22:53:25 -0700 Subject: [PATCH 26/74] Added a popup, blank so far --- frontend/src/app/staff/vsr/page.tsx | 10 +++++--- .../components/VSRTable/FilterModal/index.tsx | 25 +++++++++++++++++++ .../VSRTable/FilterModal/styles.module.css | 24 ++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/VSRTable/FilterModal/index.tsx create mode 100644 frontend/src/components/VSRTable/FilterModal/styles.module.css diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 49d019a..bf00575 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -2,6 +2,7 @@ import styles from "@/app/staff/vsr/page.module.css"; import VSRTable from "@/components/VSRTable/VSRTable"; +import FilterModal from "@/components/VSRTable/FilterModal"; import { SearchKeyword } from "@/components/VSRTable/SearchKeyword"; import PageTitle from "@/components/VSRTable/PageTitle"; import HeaderBar from "@/components/shared/HeaderBar"; @@ -38,6 +39,7 @@ export default function VSRTableView() { const [selectedVsrIds, setSelectedVsrIds] = useState([]); const [deleteVsrModalOpen, setDeleteVsrModalOpen] = useState(false); + const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); useRedirectToLoginIfNotSignedIn(); @@ -192,11 +194,13 @@ export default function VSRTableView() { iconAlt="Filter" text="Filter" hideTextOnMobile - onClick={() => { - // TODO: implement filtering - }} + onClick={() => setIsFilterModalOpen(true)} /> )} + setIsFilterModalOpen(false)} + > +
+
, + document.body, + ); +}; + +export default FilterModal; diff --git a/frontend/src/components/VSRTable/FilterModal/styles.module.css b/frontend/src/components/VSRTable/FilterModal/styles.module.css new file mode 100644 index 0000000..37a726a --- /dev/null +++ b/frontend/src/components/VSRTable/FilterModal/styles.module.css @@ -0,0 +1,24 @@ +.overlay { + position: "fixed"; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: "rgba(0, 0, 0, 0.7)"; + display: "flex"; + align-items: "center"; + justify-content: "center"; +} +.modal { + background-color: "#fff"; + padding: "20px"; + border-radius: "5px"; + width: "300px"; + box-shadow: "0 4px 6px rgba(0, 0, 0, 0.1)"; +} +.closeButton { + float: "right"; + border: "none"; + background: "none"; + cursor: "pointer"; +} From e09f0b5ce5fff3bbabec714c9a333bad25bb16f6 Mon Sep 17 00:00:00 2001 From: HarshGurnani Date: Fri, 3 May 2024 09:19:22 -0700 Subject: [PATCH 27/74] modified FilterModal and zip code filtering --- frontend/src/api/VSRs.ts | 5 ++ frontend/src/app/staff/vsr/page.tsx | 60 ++++++++++++------ .../components/VSRTable/FilterModal/index.tsx | 63 ++++++++++++++++--- .../VSRTable/FilterModal/styles.module.css | 13 ++++ 4 files changed, 113 insertions(+), 28 deletions(-) diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts index 916acad..7682064 100644 --- a/frontend/src/api/VSRs.ts +++ b/frontend/src/api/VSRs.ts @@ -159,12 +159,17 @@ export async function createVSR(vsr: CreateVSRRequest): Promise> export async function getAllVSRs( firebaseToken: string, search?: string, + zipCodes?: string[], ): Promise> { try { if (search) { const response = await get(`/api/vsr?search=${search}`, createAuthHeader(firebaseToken)); const json = (await response.json()) as VSRListJson; return { success: true, data: json.vsrs.map(parseVSR) }; + } else if (zipCodes) { + const response = await get(`/api/vsr?zipCode=${zipCodes}`, createAuthHeader(firebaseToken)); + const json = (await response.json()) as VSRListJson; + return { success: true, data: json.vsrs.map(parseVSR) }; } else { const response = await get("/api/vsr", createAuthHeader(firebaseToken)); const json = (await response.json()) as VSRListJson; diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index bf00575..862fd4d 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -39,7 +39,9 @@ export default function VSRTableView() { const [selectedVsrIds, setSelectedVsrIds] = useState([]); const [deleteVsrModalOpen, setDeleteVsrModalOpen] = useState(false); + const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); + const [filteredZipCodes, setFilteredZipCodes] = useState([]); useRedirectToLoginIfNotSignedIn(); @@ -48,27 +50,46 @@ export default function VSRTableView() { /** * Fetches the list of all VSRs from the backend and updates our vsrs state. */ - const fetchVSRs = () => { + const fetchVSRs = (zipCodes = null) => { if (!firebaseUser) { return; } - setLoadingVsrs(true); - firebaseUser?.getIdToken().then((firebaseToken) => { - getAllVSRs(firebaseToken).then((result) => { - if (result.success) { - setVsrs(result.data); - } else { - if (result.error === "Failed to fetch") { - setTableError(VSRTableError.CANNOT_FETCH_VSRS_NO_INTERNET); + if (zipCodes !== null) { + setLoadingVsrs(true); + firebaseUser?.getIdToken().then((firebaseToken) => { + getAllVSRs(firebaseToken, zipCodes).then((result) => { + if (result.success) { + setVsrs(result.data); } else { - console.error(`Error retrieving VSRs: ${result.error}`); - setTableError(VSRTableError.CANNOT_FETCH_VSRS_INTERNAL); + if (result.error === "Failed to fetch") { + setTableError(VSRTableError.CANNOT_FETCH_VSRS_NO_INTERNET); + } else { + console.error(`Error retrieving VSRs: ${result.error}`); + setTableError(VSRTableError.CANNOT_FETCH_VSRS_INTERNAL); + } } - } - setLoadingVsrs(false); + setLoadingVsrs(false); + }); }); - }); + } else { + setLoadingVsrs(true); + firebaseUser?.getIdToken().then((firebaseToken) => { + getAllVSRs(firebaseToken).then((result) => { + if (result.success) { + setVsrs(result.data); + } else { + if (result.error === "Failed to fetch") { + setTableError(VSRTableError.CANNOT_FETCH_VSRS_NO_INTERNET); + } else { + console.error(`Error retrieving VSRs: ${result.error}`); + setTableError(VSRTableError.CANNOT_FETCH_VSRS_INTERNAL); + } + } + setLoadingVsrs(false); + }); + }); + } }; // Fetch the VSRs from the backend once the Firebase user loads. @@ -197,10 +218,6 @@ export default function VSRTableView() { onClick={() => setIsFilterModalOpen(true)} /> )} - setIsFilterModalOpen(false)} - >
); } diff --git a/frontend/src/components/VSRTable/FilterModal/index.tsx b/frontend/src/components/VSRTable/FilterModal/index.tsx index 2c44731..f3359a6 100644 --- a/frontend/src/components/VSRTable/FilterModal/index.tsx +++ b/frontend/src/components/VSRTable/FilterModal/index.tsx @@ -1,24 +1,67 @@ import styles from "@/components/VSRTable/FilterModal/styles.module.css"; +import { useState } from "react"; import Image from "next/image"; import ReactDOM from "react-dom"; +import { BaseModal } from "@/components/shared/BaseModal"; +import { Button } from "@/components/shared/Button"; +import TextField from "@/components/shared/input/TextField"; interface FilterModalProps { isOpen: boolean; onClose: () => void; + onZipCodesEntered: (zipCodes: string[]) => void; } -const FilterModal = ({ isOpen, onClose }: FilterModalProps) => { +const FilterModal = ({ isOpen, onClose, onZipCodesEntered }: FilterModalProps) => { if (!isOpen) return null; - return ReactDOM.createPortal( -
-
- -
-
, - document.body, + const [zipCodes, setZipCodes] = useState([]); + + const handleZipCodeChange = (e: React.ChangeEvent) => { + setZipCodes(e.target.value.split(",").map((zip) => zip.trim())); // Assuming zip codes are comma-separated + }; + + const handleApplyFilter = () => { + // Pass the entered zip codes to the parent component when the user applies the filter + onZipCodesEntered(zipCodes); + onClose(); // Close the modal + }; + + return ( + <> + + } + bottomRow={ +
+
+ } + /> + ); }; diff --git a/frontend/src/components/VSRTable/FilterModal/styles.module.css b/frontend/src/components/VSRTable/FilterModal/styles.module.css index 37a726a..8efa2a9 100644 --- a/frontend/src/components/VSRTable/FilterModal/styles.module.css +++ b/frontend/src/components/VSRTable/FilterModal/styles.module.css @@ -22,3 +22,16 @@ background: "none"; cursor: "pointer"; } +.buttonContainer { + display: flex; + flex-direction: row; + gap: 32px; +} +.button { + width: 100%; + padding: 12px 24px; + text-align: center; + font-family: "Lora"; + font-size: 24px; + font-weight: 700; +} From 6b80568b40b4fbb539ce9c9ffa5fb30b815c4bab Mon Sep 17 00:00:00 2001 From: 2s2e Date: Fri, 3 May 2024 11:28:33 -0700 Subject: [PATCH 28/74] Fixed word-only issue with search --- backend/src/controllers/vsr.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts index 9eeb37b..52dd5f3 100644 --- a/backend/src/controllers/vsr.ts +++ b/backend/src/controllers/vsr.ts @@ -22,10 +22,12 @@ export const getAllVSRS: RequestHandler = async (req, res, next) => { // //and then by name // name: 1, // }); + const searchTerm = req.query.search as string; + const regex = new RegExp(searchTerm, "i"); vsrs = await VSRModel.aggregate([ { - $match: { $text: { $search: req.query.search as string } }, + $match: { name: { $regex: regex } }, }, { $addFields: { From db289d51ca850cd007876e2b6d7a5cf01a1f77f9 Mon Sep 17 00:00:00 2001 From: SB1019 Date: Fri, 3 May 2024 19:02:20 -0700 Subject: [PATCH 29/74] Adding Profile Picture and Firebase Profile WIP --- backend/src/controllers/user.ts | 28 +++++++++- backend/src/models/user.ts | 9 ++++ backend/src/routes/user.ts | 2 +- frontend/src/api/Users.ts | 12 ++++- frontend/src/app/staff/profile/page.tsx | 51 ++++++++++++------- .../components/Profile/UserProfile/index.tsx | 11 ++-- 6 files changed, 83 insertions(+), 30 deletions(-) diff --git a/backend/src/controllers/user.ts b/backend/src/controllers/user.ts index cbabb41..13930c9 100644 --- a/backend/src/controllers/user.ts +++ b/backend/src/controllers/user.ts @@ -1,6 +1,7 @@ import { RequestHandler } from "express"; import { PAPRequest } from "src/middleware/auth"; -import UserModel from "src/models/user"; +import { firebaseAuth } from "src/services/firebase"; +import UserModel, { DisplayUser } from "src/models/user"; /** * Retrieves data about the current user (their MongoDB ID, Firebase UID, and role). @@ -24,7 +25,30 @@ export const getWhoAmI: RequestHandler = async (req: PAPRequest, res, next) => { export const getUsers: RequestHandler = async (req: PAPRequest, res, next) => { try { const users = await UserModel.find(); - res.status(200).send(users); + const displayUsers = []; + for (const user of users) { + const { uid, _id } = user; + + try { + // const userRecord = await firebaseAuth.getUser(uid); + + await firebaseAuth.updateUser(uid, { + displayName: "Samvrit Srinath", + photoURL: + "https://www.google.com/url?sa=i&url=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F42893664%2Ffirebase-photourl-from-a-google-auth-provider-returns-a-jpg-with-colors-inverted&psig=AOvVaw1rsKyabxOup86UrqGbfpsp&ust=1714873347675000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCIDx4Jjv8oUDFQAAAAAdAAAAABAD", + }); + + const newUser = await firebaseAuth.getUser(uid); + const { displayName, email, photoURL } = newUser!; + + const displayUser = { _id, uid, displayName, email, photoURL }; + displayUsers.push(displayUser); + } catch (error) { + next(error); + } + } + + res.status(200).json(displayUsers as DisplayUser[]); } catch (error) { next(error); } diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 566df40..c31de5a 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -1,3 +1,4 @@ +import { ObjectId } from "mongodb"; import { InferSchemaType, Schema, model } from "mongoose"; /** @@ -28,6 +29,14 @@ export enum UserRole { ADMIN = "admin", } +export interface DisplayUser { + _id: ObjectId; + uid: string; + email: string; + displayName: string; + photoURL: string; +} + type User = InferSchemaType; export default model("User", userSchema); diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts index be41f9f..adcc607 100644 --- a/backend/src/routes/user.ts +++ b/backend/src/routes/user.ts @@ -1,6 +1,6 @@ import express from "express"; -import { requireSignedIn, requireAdmin } from "src/middleware/auth"; +import { requireAdmin, requireSignedIn } from "src/middleware/auth"; import * as UserController from "src/controllers/user"; const router = express.Router(); diff --git a/frontend/src/api/Users.ts b/frontend/src/api/Users.ts index f0819f3..5d0ce73 100644 --- a/frontend/src/api/Users.ts +++ b/frontend/src/api/Users.ts @@ -6,6 +6,14 @@ export interface User { role: string; } +export interface DisplayUser { + _id: string; + uid: string; + email: string; + displayName: string; + photoURL: string; +} + export const createAuthHeader = (firebaseToken: string) => ({ Authorization: `Bearer ${firebaseToken}`, }); @@ -20,10 +28,10 @@ export const getWhoAmI = async (firebaseToken: string): Promise> } }; -export const getUsers = async (firebaseToken: string): Promise> => { +export const getAllUsers = async (firebaseToken: string): Promise> => { try { const response = await get("/api/user", createAuthHeader(firebaseToken)); - const json = (await response.json()) as User[]; + const json = (await response.json()) as DisplayUser[]; return { success: true, data: json }; } catch (error) { return handleAPIError(error); diff --git a/frontend/src/app/staff/profile/page.tsx b/frontend/src/app/staff/profile/page.tsx index e8bf85f..5ff7d16 100644 --- a/frontend/src/app/staff/profile/page.tsx +++ b/frontend/src/app/staff/profile/page.tsx @@ -1,28 +1,37 @@ "use client"; import styles from "@/app/staff/profile/page.module.css"; -import VSRTable from "@/components/VSRTable/VSRTable"; -import SearchKeyword from "@/components/VSRTable/SearchKeyword"; -import PageTitle from "@/components/VSRTable/PageTitle"; import HeaderBar from "@/components/shared/HeaderBar"; -import Image from "next/image"; -import React, { useContext, useEffect, useState } from "react"; -import { StatusDropdown } from "@/components/shared/StatusDropdown"; -import { useMediaQuery } from "@mui/material"; -import { useRedirectToLoginIfNotSignedIn } from "@/hooks/useRedirection"; -import { UserContext } from "@/contexts/userContext"; -import { DeleteVSRsModal } from "@/components/shared/DeleteVSRsModal"; -import { VSR, getAllVSRs } from "@/api/VSRs"; -import { VSRErrorModal } from "@/components/VSRForm/VSRErrorModal"; +import React, { useContext, useState } from "react"; import { useScreenSizes } from "@/hooks/useScreenSizes"; -import { LoadingScreen } from "@/components/shared/LoadingScreen"; import { Button } from "@/components/shared/Button"; import { UserProfile } from "@/components/Profile/UserProfile"; -import { User } from "firebase/auth"; import { AdminProfile } from "@/components/Profile/AdminProfile"; +import { DisplayUser, getAllUsers } from "@/api/Users"; +import { UserContext } from "@/contexts/userContext"; +enum AllUsersError { + CANNOT_FETCH_USERS_NO_INTERNET, + CANNOT_FETCH_USERS_INTERNAL, + NONE, +} export default function Profile() { const { isMobile, isTablet } = useScreenSizes(); + const { firebaseUser, papUser } = useContext(UserContext); + const [users, setUsers] = useState([]); + // const [userError, setUserError] = useState(AllUsersError.NONE); + + firebaseUser?.getIdToken().then((firebaseToken) => { + getAllUsers(firebaseToken).then((result) => { + if (result.success) { + setUsers(result.data); + console.log(result.data); + } else { + // setUserError(AllUsersError.CANNOT_FETCH_USERS_INTERNAL); + // console.log(userError); + } + }); + }); // useRedirectToLoginIfNotSignedIn(); @@ -34,7 +43,6 @@ export default function Profile() {

User Profile

{/* ACCOUNT INFO */} -

Manage Users

- - + {/* */} + {users.map((user, index) => + user.uid != firebaseUser?.uid ? ( + + ) : null, + )}
); diff --git a/frontend/src/components/Profile/UserProfile/index.tsx b/frontend/src/components/Profile/UserProfile/index.tsx index 4a0ce41..5794f26 100644 --- a/frontend/src/components/Profile/UserProfile/index.tsx +++ b/frontend/src/components/Profile/UserProfile/index.tsx @@ -6,18 +6,13 @@ import { User } from "firebase/auth"; export interface UserProps { name: string; email: string; + photoURL: string; } -export function UserProfile({ name, email }: UserProps) { +export function UserProfile({ name, email, photoURL }: UserProps) { return (
- Internal Error + Internal Error
From c106889a4a3f0fd46c17605390055c7bfe5fb0b4 Mon Sep 17 00:00:00 2001 From: 2s2e Date: Mon, 6 May 2024 23:03:30 -0700 Subject: [PATCH 30/74] Added multiple choice to filter modal, began work for frontend sending to backend --- frontend/src/api/VSRs.ts | 8 +++++ .../components/VSRTable/FilterModal/index.tsx | 29 ++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts index 7682064..28a321b 100644 --- a/frontend/src/api/VSRs.ts +++ b/frontend/src/api/VSRs.ts @@ -160,7 +160,15 @@ export async function getAllVSRs( firebaseToken: string, search?: string, zipCodes?: string[], + income?: string, ): Promise> { + const incomeMap: { [key: string]: string } = { + "$50,001 and over": "50000", + "$25,001 - $50,000": "25000", + "$12,501 - $25,000": "12500", + "$12,500 and under": "0", + }; + try { if (search) { const response = await get(`/api/vsr?search=${search}`, createAuthHeader(firebaseToken)); diff --git a/frontend/src/components/VSRTable/FilterModal/index.tsx b/frontend/src/components/VSRTable/FilterModal/index.tsx index f3359a6..bbbbd47 100644 --- a/frontend/src/components/VSRTable/FilterModal/index.tsx +++ b/frontend/src/components/VSRTable/FilterModal/index.tsx @@ -5,6 +5,8 @@ import ReactDOM from "react-dom"; import { BaseModal } from "@/components/shared/BaseModal"; import { Button } from "@/components/shared/Button"; import TextField from "@/components/shared/input/TextField"; +import MultipleChoice from "@/components/shared/input/MultipleChoice"; +import { incomeOptions } from "@/constants/fieldOptions"; interface FilterModalProps { isOpen: boolean; @@ -16,6 +18,7 @@ const FilterModal = ({ isOpen, onClose, onZipCodesEntered }: FilterModalProps) = if (!isOpen) return null; const [zipCodes, setZipCodes] = useState([]); + const [income, setIncome] = useState(""); const handleZipCodeChange = (e: React.ChangeEvent) => { setZipCodes(e.target.value.split(",").map((zip) => zip.trim())); // Assuming zip codes are comma-separated @@ -34,13 +37,25 @@ const FilterModal = ({ isOpen, onClose, onZipCodesEntered }: FilterModalProps) = onClose={onClose} title="" content={ - +
+ + setIncome(Array.isArray(newValue) ? newValue[0] : newValue) + } + required + > +
+ +
} bottomRow={
From ca2276af0cf4720b3e9d9730d983c72fdf720415 Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Tue, 7 May 2024 13:31:17 -0700 Subject: [PATCH 31/74] Added frontend api functions to send requests to backend, fixed bug with EditTemplate --- frontend/src/api/FurnitureItems.ts | 44 ++++++++++++++++++- .../src/app/staff/furnitureItems/page.tsx | 21 +++++---- .../FurnitureRequest/EditTemplate/index.tsx | 5 ++- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/frontend/src/api/FurnitureItems.ts b/frontend/src/api/FurnitureItems.ts index e20f904..08cfdbe 100644 --- a/frontend/src/api/FurnitureItems.ts +++ b/frontend/src/api/FurnitureItems.ts @@ -1,4 +1,5 @@ -import { APIResult, handleAPIError, get } from "@/api/requests"; +import { APIResult, handleAPIError, get, post, put, httpDelete } from "@/api/requests"; +import { createAuthHeader } from "@/api/Users"; export interface FurnitureItem { _id: string; @@ -17,3 +18,44 @@ export async function getFurnitureItems(): Promise> { return handleAPIError(error); } } + +export async function addFurnitureItem( + furnitureItem: FurnitureItem, + firebaseToken: string, +): Promise> { + try { + const response = await post(`/api/furnitureItems`, furnitureItem, createAuthHeader(firebaseToken)); + const json = (await response.json()) as FurnitureItem; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} + +export async function updateFurnitureItem( + id: string, + furnitureItem: FurnitureItem, + firebaseToken: string +) : Promise>{ + try{ + const response = await put(`/api/furnitureItems/${id}`, furnitureItem, createAuthHeader(firebaseToken)); + const json = (await response.json()) as FurnitureItem; + return { success: true, data: json }; + } + catch(error){ + return handleAPIError(error); + } +} + +export async function deleteFurnitureItem( + id: string, + firebaseToken: string +): Promise>{ + try{ + await httpDelete(`/api/furnitureItems/${id}`, createAuthHeader(firebaseToken)); + return { success: true, data: null}; + } catch (error) { + return handleAPIError(error); + } + +} diff --git a/frontend/src/app/staff/furnitureItems/page.tsx b/frontend/src/app/staff/furnitureItems/page.tsx index 5955d95..e969671 100644 --- a/frontend/src/app/staff/furnitureItems/page.tsx +++ b/frontend/src/app/staff/furnitureItems/page.tsx @@ -3,8 +3,10 @@ import HeaderBar from "@/components/shared/HeaderBar"; import styles from "src/app/staff/furnitureItems/page.module.css"; import { EditTemplate } from "@/components/FurnitureRequest/EditTemplate"; import { useMemo } from "react"; -import { FurnitureItem, getFurnitureItems } from "@/api/FurnitureItems"; +import { FurnitureItem, getFurnitureItems, addFurnitureItem, updateFurnitureItem, deleteFurnitureItem } from "@/api/FurnitureItems"; import React, { useEffect, useState } from "react"; +import { ICreateVSRFormInput } from "@/components/VSRForm/VSRFormTypes"; + export default function furnitureItemTemplate() { const [furnitureItems, setFurnitureItems] = useState(); @@ -33,12 +35,14 @@ export default function furnitureItemTemplate() { const handleBeginEditing = (category: string) => { setEditingCategory(category); + console.log(category); }; const handleFinishEditing = () => { setEditingCategory(undefined); }; + return ( <> @@ -52,22 +56,23 @@ export default function furnitureItemTemplate() {

Furnishings

- {/* Possible way to render a series of EditTemplate components - {Object.entries(furnitureCategoriesToItems).map(([category, items]) => ( + + + {furnitureCategoriesToItems ? Object.entries(furnitureCategoriesToItems!).map(([category, items]) => ( handleBeginEditing(category)} onFinishEditing={handleFinishEditing} /> - ))} - */} + )): null} + - + /> */}
diff --git a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx index c1653a2..b0e9b82 100644 --- a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx +++ b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx @@ -1,7 +1,6 @@ import styles from "@/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css"; -import { FurnitureItem } from "@/api/FurnitureItems"; import { useState } from "react"; -import { useMemo } from "react"; +import { FurnitureItem, getFurnitureItems, addFurnitureItem, updateFurnitureItem, deleteFurnitureItem } from "@/api/FurnitureItems"; import { FurnitureItemSelection } from "@/components/VSRForm/FurnitureItemSelection"; import { FieldDetail } from "@/components/VSRIndividual/FieldDetails/FieldDetail"; @@ -15,6 +14,8 @@ export interface EditTemplateProps { onFinishEditing: () => void; } +//Display components + export const EditTemplate = ({ furnitureItems, categoryTitle, From 4f01503465519487cf8f6dca91457f378b25ac2f Mon Sep 17 00:00:00 2001 From: HarshGurnani Date: Thu, 9 May 2024 10:08:35 -0700 Subject: [PATCH 32/74] added connection to backend for filtering by zipcode and income; need to test if it is working correctly --- frontend/src/api/VSRs.ts | 32 +++++--- frontend/src/app/staff/vsr/page.tsx | 75 ++++++------------- .../components/VSRTable/FilterModal/index.tsx | 8 +- .../VSRTable/SearchKeyword/index.tsx | 49 +----------- 4 files changed, 51 insertions(+), 113 deletions(-) diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts index 28a321b..8152a59 100644 --- a/frontend/src/api/VSRs.ts +++ b/frontend/src/api/VSRs.ts @@ -170,19 +170,29 @@ export async function getAllVSRs( }; try { + let url_string = ``; + url_string = `/api/vsr`; if (search) { - const response = await get(`/api/vsr?search=${search}`, createAuthHeader(firebaseToken)); - const json = (await response.json()) as VSRListJson; - return { success: true, data: json.vsrs.map(parseVSR) }; - } else if (zipCodes) { - const response = await get(`/api/vsr?zipCode=${zipCodes}`, createAuthHeader(firebaseToken)); - const json = (await response.json()) as VSRListJson; - return { success: true, data: json.vsrs.map(parseVSR) }; - } else { - const response = await get("/api/vsr", createAuthHeader(firebaseToken)); - const json = (await response.json()) as VSRListJson; - return { success: true, data: json.vsrs.map(parseVSR) }; + url_string += `?search=${search}`; } + if (zipCodes) { + if (search) { + url_string += `&zipCode=${zipCodes}`; + } else { + url_string += `?zipCode=${zipCodes}`; + } + } + if (income) { + if (search || zipCodes) { + url_string += `&incomeLevel=${incomeMap[income]}`; + } else { + url_string += `?incomeLevel=${incomeMap[income]}`; + } + } + + const response = await get(url_string, createAuthHeader(firebaseToken)); + const json = (await response.json()) as VSRListJson; + return { success: true, data: json.vsrs.map(parseVSR) }; } catch (error) { return handleAPIError(error); } diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 862fd4d..92ea2c4 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -41,7 +41,9 @@ export default function VSRTableView() { const [deleteVsrModalOpen, setDeleteVsrModalOpen] = useState(false); const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); - const [filteredZipCodes, setFilteredZipCodes] = useState([]); + const [filteredZipCodes, setFilteredZipCodes] = useState(undefined); + const [filteredIncome, setFilteredIncome] = useState(undefined); + const [search, setSearch] = useState(undefined); useRedirectToLoginIfNotSignedIn(); @@ -50,61 +52,16 @@ export default function VSRTableView() { /** * Fetches the list of all VSRs from the backend and updates our vsrs state. */ - const fetchVSRs = (zipCodes = null) => { + const fetchVSRs = (search?: string, zipCodes?: string[], income?: string) => { if (!firebaseUser) { return; } - if (zipCodes !== null) { - setLoadingVsrs(true); - firebaseUser?.getIdToken().then((firebaseToken) => { - getAllVSRs(firebaseToken, zipCodes).then((result) => { - if (result.success) { - setVsrs(result.data); - } else { - if (result.error === "Failed to fetch") { - setTableError(VSRTableError.CANNOT_FETCH_VSRS_NO_INTERNET); - } else { - console.error(`Error retrieving VSRs: ${result.error}`); - setTableError(VSRTableError.CANNOT_FETCH_VSRS_INTERNAL); - } - } - setLoadingVsrs(false); - }); - }); - } else { - setLoadingVsrs(true); - firebaseUser?.getIdToken().then((firebaseToken) => { - getAllVSRs(firebaseToken).then((result) => { - if (result.success) { - setVsrs(result.data); - } else { - if (result.error === "Failed to fetch") { - setTableError(VSRTableError.CANNOT_FETCH_VSRS_NO_INTERNET); - } else { - console.error(`Error retrieving VSRs: ${result.error}`); - setTableError(VSRTableError.CANNOT_FETCH_VSRS_INTERNAL); - } - } - setLoadingVsrs(false); - }); - }); - } - }; - - // Fetch the VSRs from the backend once the Firebase user loads. - useEffect(() => { - fetchVSRs(); - }, [firebaseUser]); - - const fetchSearchedVSRs = (input: string) => { - if (!firebaseUser) { - return; - } + console.log(filteredIncome); setLoadingVsrs(true); firebaseUser?.getIdToken().then((firebaseToken) => { - getAllVSRs(firebaseToken, input).then((result) => { + getAllVSRs(firebaseToken, search, zipCodes, income).then((result) => { if (result.success) { setVsrs(result.data); } else { @@ -120,6 +77,11 @@ export default function VSRTableView() { }); }; + // Fetch the VSRs from the backend once the Firebase user loads. + useEffect(() => { + fetchVSRs(); + }, [firebaseUser]); + /** * Renders an error modal corresponding to the page's error state, or renders * nothing if there is no error. @@ -186,7 +148,14 @@ export default function VSRTableView() {
- {searchOnOwnRow ? null : } + {searchOnOwnRow ? null : ( + { + setSearch(search); + fetchVSRs(search, filteredZipCodes, filteredIncome); + }} + /> + )}

Status:

@@ -231,7 +200,7 @@ export default function VSRTableView() { />
- {searchOnOwnRow ? : null} + {/* {searchOnOwnRow ? : null} */}
{loadingVsrs ? ( @@ -259,8 +228,10 @@ export default function VSRTableView() { setIsFilterModalOpen(false)} - onZipCodesEntered={(zipCodes: string[]) => { + onInputEntered={(zipCodes: string[] | undefined, incomeLevel: string | undefined) => { setFilteredZipCodes(zipCodes); + setFilteredIncome(incomeLevel); + fetchVSRs(search, zipCodes, incomeLevel); }} />
diff --git a/frontend/src/components/VSRTable/FilterModal/index.tsx b/frontend/src/components/VSRTable/FilterModal/index.tsx index bbbbd47..71a1776 100644 --- a/frontend/src/components/VSRTable/FilterModal/index.tsx +++ b/frontend/src/components/VSRTable/FilterModal/index.tsx @@ -1,7 +1,5 @@ import styles from "@/components/VSRTable/FilterModal/styles.module.css"; import { useState } from "react"; -import Image from "next/image"; -import ReactDOM from "react-dom"; import { BaseModal } from "@/components/shared/BaseModal"; import { Button } from "@/components/shared/Button"; import TextField from "@/components/shared/input/TextField"; @@ -11,10 +9,10 @@ import { incomeOptions } from "@/constants/fieldOptions"; interface FilterModalProps { isOpen: boolean; onClose: () => void; - onZipCodesEntered: (zipCodes: string[]) => void; + onInputEntered: (zipCodes: string[] | undefined, incomeLevel: string | undefined) => void; } -const FilterModal = ({ isOpen, onClose, onZipCodesEntered }: FilterModalProps) => { +const FilterModal = ({ isOpen, onClose, onInputEntered }: FilterModalProps) => { if (!isOpen) return null; const [zipCodes, setZipCodes] = useState([]); @@ -26,7 +24,7 @@ const FilterModal = ({ isOpen, onClose, onZipCodesEntered }: FilterModalProps) = const handleApplyFilter = () => { // Pass the entered zip codes to the parent component when the user applies the filter - onZipCodesEntered(zipCodes); + onInputEntered(zipCodes, income); onClose(); // Close the modal }; diff --git a/frontend/src/components/VSRTable/SearchKeyword/index.tsx b/frontend/src/components/VSRTable/SearchKeyword/index.tsx index ca791b1..6309671 100644 --- a/frontend/src/components/VSRTable/SearchKeyword/index.tsx +++ b/frontend/src/components/VSRTable/SearchKeyword/index.tsx @@ -1,25 +1,22 @@ import styles from "@/components/VSRTable/SearchKeyword/styles.module.css"; import Image from "next/image"; -import { UserContext } from "@/contexts/userContext"; -import React, { useContext, useState } from "react"; -import { getAllVSRs } from "@/api/VSRs"; +import React, { useState } from "react"; /** * A component for the Search input above the VSR table. */ interface SearchProps { - // eslint-disable-next-line @typescript-eslint/ban-types - fetchFunction: Function; + onUpdate: (search: string) => void; } -export const SearchKeyword = ({ fetchFunction }: SearchProps) => { +export const SearchKeyword = ({ onUpdate }: SearchProps) => { const [searchInput, setSearchInput] = useState(""); const handleInputChange = (event: { target: { value: string } }) => { const input = event.target.value; setSearchInput(input); - fetchFunction(input); + onUpdate(input); }; return ( @@ -35,41 +32,3 @@ export const SearchKeyword = ({ fetchFunction }: SearchProps) => {
); }; - -// export default function SearchKeyword(fetchFunction: (input: string) => void) { -// const { firebaseUser } = useContext(UserContext); -// const [searchInput, setSearchInput] = useState(""); - -// const handleInputChange = (event: { target: { value: string } }) => { -// const input = event.target.value; -// setSearchInput(input); -// fetchFunction(input); -// }; - -// // const fetchSearchedVSRs = (input) => { -// // if (!firebaseUser) { -// // return; -// // } - -// // firebaseUser?.getIdToken().then((firebaseToken) => { -// // getAllVSRs(firebaseToken, input).then((result) => { -// // if (result.success) { - -// // } -// // }); -// // }); -// // }; - -// return ( -//
-// {/* image */} -// Search -// -//
-// ); -// } From 00ed973b2732e9dbb85d28eae75202fceacb2923 Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Thu, 9 May 2024 14:43:56 -0700 Subject: [PATCH 33/74] Rough first start to edit section --- frontend/src/api/FurnitureItems.ts | 30 +++-- .../src/app/staff/furnitureItems/page.tsx | 39 ++++--- .../FurnitureRequest/EditTemplate/index.tsx | 104 ++++++++++++------ 3 files changed, 111 insertions(+), 62 deletions(-) diff --git a/frontend/src/api/FurnitureItems.ts b/frontend/src/api/FurnitureItems.ts index 08cfdbe..991e021 100644 --- a/frontend/src/api/FurnitureItems.ts +++ b/frontend/src/api/FurnitureItems.ts @@ -24,7 +24,11 @@ export async function addFurnitureItem( firebaseToken: string, ): Promise> { try { - const response = await post(`/api/furnitureItems`, furnitureItem, createAuthHeader(firebaseToken)); + const response = await post( + `/api/furnitureItems`, + furnitureItem, + createAuthHeader(firebaseToken), + ); const json = (await response.json()) as FurnitureItem; return { success: true, data: json }; } catch (error) { @@ -35,27 +39,29 @@ export async function addFurnitureItem( export async function updateFurnitureItem( id: string, furnitureItem: FurnitureItem, - firebaseToken: string -) : Promise>{ - try{ - const response = await put(`/api/furnitureItems/${id}`, furnitureItem, createAuthHeader(firebaseToken)); + firebaseToken: string, +): Promise> { + try { + const response = await put( + `/api/furnitureItems/${id}`, + furnitureItem, + createAuthHeader(firebaseToken), + ); const json = (await response.json()) as FurnitureItem; return { success: true, data: json }; - } - catch(error){ + } catch (error) { return handleAPIError(error); } } export async function deleteFurnitureItem( id: string, - firebaseToken: string -): Promise>{ - try{ + firebaseToken: string, +): Promise> { + try { await httpDelete(`/api/furnitureItems/${id}`, createAuthHeader(firebaseToken)); - return { success: true, data: null}; + return { success: true, data: null }; } catch (error) { return handleAPIError(error); } - } diff --git a/frontend/src/app/staff/furnitureItems/page.tsx b/frontend/src/app/staff/furnitureItems/page.tsx index e969671..18aa7a1 100644 --- a/frontend/src/app/staff/furnitureItems/page.tsx +++ b/frontend/src/app/staff/furnitureItems/page.tsx @@ -3,11 +3,16 @@ import HeaderBar from "@/components/shared/HeaderBar"; import styles from "src/app/staff/furnitureItems/page.module.css"; import { EditTemplate } from "@/components/FurnitureRequest/EditTemplate"; import { useMemo } from "react"; -import { FurnitureItem, getFurnitureItems, addFurnitureItem, updateFurnitureItem, deleteFurnitureItem } from "@/api/FurnitureItems"; +import { + FurnitureItem, + getFurnitureItems, + addFurnitureItem, + updateFurnitureItem, + deleteFurnitureItem, +} from "@/api/FurnitureItems"; import React, { useEffect, useState } from "react"; import { ICreateVSRFormInput } from "@/components/VSRForm/VSRFormTypes"; - export default function furnitureItemTemplate() { const [furnitureItems, setFurnitureItems] = useState(); const [editingCategory, setEditingCategory] = useState(); @@ -42,7 +47,6 @@ export default function furnitureItemTemplate() { setEditingCategory(undefined); }; - return ( <> @@ -56,21 +60,20 @@ export default function furnitureItemTemplate() {

Furnishings

- - - {furnitureCategoriesToItems ? Object.entries(furnitureCategoriesToItems!).map(([category, items]) => ( - handleBeginEditing(category)} - onFinishEditing={handleFinishEditing} - /> - )): null} - + {furnitureCategoriesToItems + ? Object.entries(furnitureCategoriesToItems!).map(([category, items]) => ( + handleBeginEditing(category)} + onFinishEditing={handleFinishEditing} + /> + )) + : null} {/* -
- {furnitureItems.map((furnitureItem) => ( - - ))} - - -
+ {isEditing ? ( + <> +
+ {furnitureItems.map((furnitureItem) => ( + + ))} +
+ +

Please Select a Tag to Delete or Start Editing

+ + + + + + + + + ) : ( + <> +
+ {furnitureItems.map((furnitureItem) => ( + + ))} +
+ + + )}
); }; - -// The button is currently being styled by styles.chip, although its not completed -// and I'm not sure that's where we want to style the button. and it doesn't do anything - -//const[isEditing, setIsEditing] = useState(false); -//const handleEditClick = () => { -// setIsEditing(current => !current); -// console.log('Button clicked for furniture item:', categoryTitle); }}>Edit Section From f3afafcf7bf9a2a56630375dac08d5ecf04a4d87 Mon Sep 17 00:00:00 2001 From: 2s2e Date: Thu, 9 May 2024 16:20:55 -0700 Subject: [PATCH 34/74] When no VSRs are found, a message is now displayed --- frontend/src/app/staff/vsr/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 92ea2c4..2f92f89 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -204,12 +204,16 @@ export default function VSRTableView() {
{loadingVsrs ? ( - ) : ( + ) : vsrs?.length !== 0 ? ( + ) : ( +
+

No VSRs found.

+
)}
From 75bca6915c4d94336940dda3c929ce676391ab9c Mon Sep 17 00:00:00 2001 From: 2s2e Date: Thu, 9 May 2024 22:54:17 -0700 Subject: [PATCH 35/74] Income filtering works --- backend/src/controllers/vsr.ts | 25 +++++++++++++++++-------- frontend/src/api/VSRs.ts | 2 ++ frontend/src/app/staff/vsr/page.tsx | 2 -- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts index 52dd5f3..9318199 100644 --- a/backend/src/controllers/vsr.ts +++ b/backend/src/controllers/vsr.ts @@ -83,18 +83,27 @@ export const getAllVSRS: RequestHandler = async (req, res, next) => { } if ( - req.query.incomeLevel && - typeof req.query.incomeLevel === "string" && - req.query.incomeLevel in ["50000", "25000", "12500", "0"] + req.query.incomeLevel + // && + // typeof req.query.incomeLevel === "string" && + // req.query.incomeLevel in ["50000", "25000", "12500", "0"] ) { + /* + "$12,500 and under", + "$12,501 - $25,000", + "$25,001 - $50,000", + "$50,001 and over", + */ const incomeMap: { [key: string]: string } = { - "50000": "$50,000 and above", - "25000": "$25,000 - $50,000", - "12500": "$12,500 - $25,000", - "0": "Below $12,500", + "50000": "$50,001 and over", + "25000": "$25,001 - $50,000", + "12500": "$12,501 - $25,000", + "0": "$12,500 and under", }; - vsrs = vsrs.filter((vsr) => vsr.incomeLevel === incomeMap[req.query.incomeLevel as string]); + vsrs = vsrs.filter((vsr) => { + return vsr.incomeLevel === incomeMap[req.query.incomeLevel as string]; + }); } if (req.query.zipCode) { diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts index 8152a59..6ba7e44 100644 --- a/frontend/src/api/VSRs.ts +++ b/frontend/src/api/VSRs.ts @@ -189,9 +189,11 @@ export async function getAllVSRs( url_string += `?incomeLevel=${incomeMap[income]}`; } } + console.log(url_string); const response = await get(url_string, createAuthHeader(firebaseToken)); const json = (await response.json()) as VSRListJson; + console.log(url_string); return { success: true, data: json.vsrs.map(parseVSR) }; } catch (error) { return handleAPIError(error); diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 2f92f89..2f3f079 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -57,8 +57,6 @@ export default function VSRTableView() { return; } - console.log(filteredIncome); - setLoadingVsrs(true); firebaseUser?.getIdToken().then((firebaseToken) => { getAllVSRs(firebaseToken, search, zipCodes, income).then((result) => { From 8c574701a5fd6a39d3e04ca421bd0efb56fa1213 Mon Sep 17 00:00:00 2001 From: 2s2e Date: Thu, 9 May 2024 23:07:43 -0700 Subject: [PATCH 36/74] Filtering by zip code works --- backend/src/controllers/vsr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts index 9318199..7f166ef 100644 --- a/backend/src/controllers/vsr.ts +++ b/backend/src/controllers/vsr.ts @@ -108,7 +108,7 @@ export const getAllVSRS: RequestHandler = async (req, res, next) => { if (req.query.zipCode) { //we expect a list of zipcodes - const zipCodes = req.query.zipCode as string[]; + const zipCodes = (req.query.zipCode as string).split(",").map((zip) => zip.trim()); vsrs = vsrs.filter((vsr) => zipCodes.includes(vsr.zipCode.toString())); } From 7c422b4559e00b3c6b53060fe4f7355e1f5b809b Mon Sep 17 00:00:00 2001 From: HarshGurnani Date: Fri, 10 May 2024 06:55:51 -0700 Subject: [PATCH 37/74] filter by status added --- frontend/src/api/VSRs.ts | 9 ++++++++- frontend/src/app/staff/vsr/page.tsx | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts index 6ba7e44..43dbccb 100644 --- a/frontend/src/api/VSRs.ts +++ b/frontend/src/api/VSRs.ts @@ -161,6 +161,7 @@ export async function getAllVSRs( search?: string, zipCodes?: string[], income?: string, + status?: string, ): Promise> { const incomeMap: { [key: string]: string } = { "$50,001 and over": "50000", @@ -189,7 +190,13 @@ export async function getAllVSRs( url_string += `?incomeLevel=${incomeMap[income]}`; } } - console.log(url_string); + if (status) { + if (search || zipCodes || income) { + url_string += `&status=${status}`; + } else { + url_string += `?status=${status}`; + } + } const response = await get(url_string, createAuthHeader(firebaseToken)); const json = (await response.json()) as VSRListJson; diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 2f3f079..a695fb6 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -44,6 +44,7 @@ export default function VSRTableView() { const [filteredZipCodes, setFilteredZipCodes] = useState(undefined); const [filteredIncome, setFilteredIncome] = useState(undefined); const [search, setSearch] = useState(undefined); + const [status, setStatus] = useState(undefined); useRedirectToLoginIfNotSignedIn(); @@ -52,14 +53,14 @@ export default function VSRTableView() { /** * Fetches the list of all VSRs from the backend and updates our vsrs state. */ - const fetchVSRs = (search?: string, zipCodes?: string[], income?: string) => { + const fetchVSRs = (search?: string, zipCodes?: string[], income?: string, status?: string) => { if (!firebaseUser) { return; } setLoadingVsrs(true); firebaseUser?.getIdToken().then((firebaseToken) => { - getAllVSRs(firebaseToken, search, zipCodes, income).then((result) => { + getAllVSRs(firebaseToken, search, zipCodes, income, status).then((result) => { if (result.success) { setVsrs(result.data); } else { @@ -150,7 +151,7 @@ export default function VSRTableView() { { setSearch(search); - fetchVSRs(search, filteredZipCodes, filteredIncome); + fetchVSRs(search, filteredZipCodes, filteredIncome, status); }} /> )} @@ -158,7 +159,13 @@ export default function VSRTableView() {

Status:

- + { + setStatus(value); + fetchVSRs(search, filteredZipCodes, filteredIncome, value); + }} + />
@@ -233,7 +240,7 @@ export default function VSRTableView() { onInputEntered={(zipCodes: string[] | undefined, incomeLevel: string | undefined) => { setFilteredZipCodes(zipCodes); setFilteredIncome(incomeLevel); - fetchVSRs(search, zipCodes, incomeLevel); + fetchVSRs(search, zipCodes, incomeLevel, status); }} />
From 9de10c4cfe47000ac69291ae5f20bef2ff8ec434 Mon Sep 17 00:00:00 2001 From: Daniel Shao Date: Sun, 12 May 2024 21:52:33 -0700 Subject: [PATCH 38/74] merged yoto changes and added nonfunctional elements --- frontend/.env.frontend.development | 10 ++++++ .../FurnitureRequest/EditTemplate/index.tsx | 30 +++++++++++----- .../EditTemplate/styles.module.css | 35 +++++++++++++++++++ package.json | 5 +++ 4 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 frontend/.env.frontend.development create mode 100644 frontend/src/components/FurnitureRequest/EditTemplate/styles.module.css create mode 100644 package.json diff --git a/frontend/.env.frontend.development b/frontend/.env.frontend.development new file mode 100644 index 0000000..3a9e20b --- /dev/null +++ b/frontend/.env.frontend.development @@ -0,0 +1,10 @@ +NEXT_PUBLIC_BACKEND_URL="http://localhost:3001" +NEXT_PUBLIC_FIREBASE_SETTINGS='{ + "apiKey": "AIzaSyAN8IA2AQnmX-tL8x2oQKUcz1TG9KNxiSA", + "authDomain": "tse-pap-dev.firebaseapp.com", + "projectId": "tse-pap-dev", + "storageBucket": "tse-pap-dev.appspot.com", + "messagingSenderId": "769700838421", + "appId": "1:769700838421:web:3049e07fd98a27c77ee21c", + "measurementId": "G-EEL71R3Q0F" +}' \ No newline at end of file diff --git a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx index b0e9b82..9a4d913 100644 --- a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx +++ b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx @@ -1,4 +1,4 @@ -import styles from "@/components/VSRIndividual/PageSections/RequestedFurnishings/styles.module.css"; +import styles from "@/components/FurnitureRequest/EditTemplate/styles.module.css"; import { useState } from "react"; import { FurnitureItem, getFurnitureItems, addFurnitureItem, updateFurnitureItem, deleteFurnitureItem } from "@/api/FurnitureItems"; import { FurnitureItemSelection } from "@/components/VSRForm/FurnitureItemSelection"; @@ -37,6 +37,7 @@ export const EditTemplate = ({ const handleAddNewItem = () => { setIsAddingNewItem(true); + }; const handleFinishAddNewItem = () => { @@ -68,16 +69,27 @@ export const EditTemplate = ({ > Add New Item + {isAddingNewItem && ( +
+ +
+ +
+ + + +
+ )}
); }; - -// The button is currently being styled by styles.chip, although its not completed -// and I'm not sure that's where we want to style the button. and it doesn't do anything - -//const[isEditing, setIsEditing] = useState(false); -//const handleEditClick = () => { -// setIsEditing(current => !current); -// console.log('Button clicked for furniture item:', categoryTitle); }}>Edit Section +// Create a conditional such that if isAddingNewItem is true, render the text field, mutliple quantities, etc diff --git a/frontend/src/components/FurnitureRequest/EditTemplate/styles.module.css b/frontend/src/components/FurnitureRequest/EditTemplate/styles.module.css new file mode 100644 index 0000000..855eca3 --- /dev/null +++ b/frontend/src/components/FurnitureRequest/EditTemplate/styles.module.css @@ -0,0 +1,35 @@ +.row { + display: flex; + flex-direction: row; + justify-content: space-between; + } + + .row:not(:last-child) { + padding-bottom: 32px; + } + + .chipContainer { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 16px; + } + + .chip { + white-space: nowrap; + text-align: center; + font-family: "OpenSans"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; + border-radius: 64px; + border: 1px solid var(--Secondary-1, #102d5f); + background-color: #102d5f; + } + + .chip:hover { + cursor: pointer; + background: #c7def1; + } + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..a802f87 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "email-validator": "^2.0.4" + } +} From 19c845618d77e8cadd8a6807af52e6b4bb68c23c Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Tue, 14 May 2024 13:45:57 -0700 Subject: [PATCH 39/74] Full functionality for add, edit, and delete furniture items. Updated DeleteModal component to be more general --- frontend/src/api/FurnitureItems.ts | 9 +- .../src/app/staff/furnitureItems/page.tsx | 7 +- frontend/src/app/staff/vsr/page.tsx | 63 +++++++- .../FurnitureRequest/EditTemplate/index.tsx | 141 ++++++++++++++++-- .../EditTemplate/styles.module.css | 2 +- .../VSRForm/FurnitureItemSelection/index.tsx | 11 ++ .../VSRIndividual/VSRIndividualPage/index.tsx | 87 +++++++++-- .../shared/ConfirmDeleteModal/index.tsx | 56 +++++++ .../styles.module.css | 0 .../shared/DeleteVSRsModal/index.tsx | 116 -------------- 10 files changed, 339 insertions(+), 153 deletions(-) create mode 100644 frontend/src/components/shared/ConfirmDeleteModal/index.tsx rename frontend/src/components/shared/{DeleteVSRsModal => ConfirmDeleteModal}/styles.module.css (100%) delete mode 100644 frontend/src/components/shared/DeleteVSRsModal/index.tsx diff --git a/frontend/src/api/FurnitureItems.ts b/frontend/src/api/FurnitureItems.ts index 991e021..7ad4096 100644 --- a/frontend/src/api/FurnitureItems.ts +++ b/frontend/src/api/FurnitureItems.ts @@ -9,6 +9,13 @@ export interface FurnitureItem { categoryIndex: number; } +export interface CreateFurnitureItem { + category: string; + name: string; + allowMultiple: boolean; + categoryIndex: number; +} + export async function getFurnitureItems(): Promise> { try { const response = await get(`/api/furnitureItems`); @@ -20,7 +27,7 @@ export async function getFurnitureItems(): Promise> { } export async function addFurnitureItem( - furnitureItem: FurnitureItem, + furnitureItem: CreateFurnitureItem, firebaseToken: string, ): Promise> { try { diff --git a/frontend/src/app/staff/furnitureItems/page.tsx b/frontend/src/app/staff/furnitureItems/page.tsx index 18aa7a1..0d05392 100644 --- a/frontend/src/app/staff/furnitureItems/page.tsx +++ b/frontend/src/app/staff/furnitureItems/page.tsx @@ -17,6 +17,10 @@ export default function furnitureItemTemplate() { const [furnitureItems, setFurnitureItems] = useState(); const [editingCategory, setEditingCategory] = useState(); useEffect(() => { + fetchFurnitureItems(); + }, []); + + const fetchFurnitureItems = () => { getFurnitureItems().then((result) => { if (result.success) { setFurnitureItems(result.data); @@ -24,7 +28,7 @@ export default function furnitureItemTemplate() { setFurnitureItems([]); } }); - }, []); + } const furnitureCategoriesToItems = useMemo( () => @@ -45,6 +49,7 @@ export default function furnitureItemTemplate() { const handleFinishEditing = () => { setEditingCategory(undefined); + fetchFurnitureItems(); }; return ( diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 1adbde3..3ea1f89 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -11,8 +11,8 @@ import { StatusDropdown } from "@/components/shared/StatusDropdown"; import { useMediaQuery } from "@mui/material"; import { useRedirectToLoginIfNotSignedIn } from "@/hooks/useRedirection"; import { UserContext } from "@/contexts/userContext"; -import { DeleteVSRsModal } from "@/components/shared/DeleteVSRsModal"; -import { VSR, getAllVSRs, bulkExportVSRS } from "@/api/VSRs"; +import { ConfirmDeleteModal } from "@/components/shared/ConfirmDeleteModal"; +import { VSR, getAllVSRs, bulkExportVSRS, deleteVSR } from "@/api/VSRs"; import { VSRErrorModal } from "@/components/VSRForm/VSRErrorModal"; import { useScreenSizes } from "@/hooks/useScreenSizes"; import { LoadingScreen } from "@/components/shared/LoadingScreen"; @@ -48,6 +48,9 @@ export default function VSRTableView() { const [selectedVsrIds, setSelectedVsrIds] = useState([]); const [deleteVsrModalOpen, setDeleteVsrModalOpen] = useState(false); + const [loadingDelete, setLoadingDelete] = useState(false); + const [successNotificationOpen, setSuccessNotificationOpen] = useState(false); + const [errorNotificationOpen, setErrorNotificationOpen] = useState(false); useRedirectToLoginIfNotSignedIn(); @@ -84,6 +87,41 @@ export default function VSRTableView() { fetchVSRs(); }, [firebaseUser]); + const onDelete = async () => { + if (loadingDelete || !firebaseUser) { + return; + } + + setSuccessNotificationOpen(false); + setErrorNotificationOpen(false); + setLoadingDelete(true); + + try { + const firebaseToken = await firebaseUser.getIdToken(); + if (!firebaseToken) { + setLoadingDelete(false); + return; + } + + await Promise.all(selectedVsrIds.map(vsrId => deleteVSR(vsrId, firebaseToken).then((res) => { + if (res.success) { + return Promise.resolve(); + } else { + return Promise.reject(res.error); + } + }))) + setSuccessNotificationOpen(true); + setSelectedVsrIds([]); + fetchVSRs(); + } catch (error) { + console.error(`Error deleting VSR(s): ${error}`); + setErrorNotificationOpen(true); + } finally { + setLoadingDelete(false); + setDeleteVsrModalOpen(false); + } + }; + /** * Renders an error modal corresponding to the page's error state, or renders * nothing if there is no error. @@ -304,14 +342,23 @@ export default function VSRTableView() { /> {renderErrorModal()} {renderExportErrorModal()} - setDeleteVsrModalOpen(false)} - afterDelete={() => { - setSelectedVsrIds([]); - fetchVSRs(); - }} - vsrIds={selectedVsrIds} + title="Delete VSR(s)" + content={ + <> + {"Deleted VSR’s "} + cannot + {" be recovered. Are you sure you’d like to delete the selected VSR forms ("} + {1} + {")?"} + + } + cancelText="Cancel" + confirmText="Delete VSR(s)" + onConfirm={onDelete} + buttonLoading={loadingDelete} />
); diff --git a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx index a13c421..1cdc059 100644 --- a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx +++ b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx @@ -1,14 +1,19 @@ import styles from "@/components/FurnitureRequest/EditTemplate/styles.module.css"; -import { useState } from "react"; +import { useContext, useState } from "react"; import { FurnitureItem, getFurnitureItems, addFurnitureItem, updateFurnitureItem, deleteFurnitureItem, + CreateFurnitureItem, } from "@/api/FurnitureItems"; import { FurnitureItemSelection } from "@/components/VSRForm/FurnitureItemSelection"; import { FieldDetail } from "@/components/VSRIndividual/FieldDetails/FieldDetail"; +import TextField from "@/components/shared/input/TextField"; +import { UserContext } from "@/contexts/userContext"; +import { Checkbox, FormControlLabel } from "@mui/material"; +import { ConfirmDeleteModal } from "@/components/shared/ConfirmDeleteModal"; export interface EditTemplateProps { furnitureItems: FurnitureItem[]; @@ -33,25 +38,101 @@ export const EditTemplate = ({ }: EditTemplateProps) => { const [isAddingNewItem, setIsAddingNewItem] = useState(false); const [editingItemId, setEditingItemId] = useState(null); + const [itemName, setItemName] = useState(""); + const { firebaseUser, papUser } = useContext(UserContext); + const [allowMultiple, setAllowMultiple] = useState(false); + const [confirmDeleteModal, setConfirmDeleteModal] = useState(false); + + const getFurnitureItemById = (itemId: string) => { + for (const furnitureItem of furnitureItems) { + if (furnitureItem._id === itemId) { + return furnitureItem; + } + } + return null; + }; + const handleStartEditItem = (itemId: string) => { + const furnitureItem = getFurnitureItemById(itemId); setEditingItemId(itemId); + setItemName(furnitureItem?.name ?? ""); + setAllowMultiple(furnitureItem?.allowMultiple ?? false); }; const handleStopEditItem = () => { setEditingItemId(null); }; + const onDelete = async() => { + const firebaseToken = await firebaseUser?.getIdToken(); + if (!firebaseToken || editingItemId===null) { + return; + } + const response = await deleteFurnitureItem(editingItemId, firebaseToken); + if (response.success) { + onFinishEditing(); + } else { + console.error(`Cannot delete Furniture Item. Error: ${response.error}`); + } + setConfirmDeleteModal(false); + } + const handleAddNewItem = () => { setIsAddingNewItem(true); - }; - const handleFinishAddNewItem = () => { + const handleSaveChanges = async () => { + if (isAddingNewItem) { + const createFurnitureItem: CreateFurnitureItem = { + category: categoryName, + name: itemName, + allowMultiple: allowMultiple, + categoryIndex: furnitureItems.length + 1, + }; + const firebaseToken = await firebaseUser?.getIdToken(); + if (!firebaseToken) { + return; + } + const response = await addFurnitureItem(createFurnitureItem, firebaseToken); + if (response.success) { + onFinishEditing(); + } else { + console.error(`Cannot create Furniture Item. Error: ${response.error}`); + } + } + + else if (editingItemId !== null){ + const furnitureItem = getFurnitureItemById(editingItemId); + if(furnitureItem===null){ + return; //Put error here instead + } + const editFurnitureItem: FurnitureItem = { + _id: furnitureItem._id, + category: categoryName, + name: itemName, + allowMultiple: allowMultiple, + categoryIndex: furnitureItem.categoryIndex + } + const firebaseToken = await firebaseUser?.getIdToken(); + if (!firebaseToken) { + return; + } + const response = await updateFurnitureItem(furnitureItem._id, editFurnitureItem, firebaseToken); + if (response.success) { + onFinishEditing(); + } else { + console.error(`Cannot edit Furniture Item. Error: ${response.error}`); + } + } + + + setEditingItemId(null); setIsAddingNewItem(false); }; return (
+ {isEditing ?

Please Select a Tag to Delete or Start Editing

: null} {isEditing ? ( <> @@ -61,32 +142,49 @@ export const EditTemplate = ({ isActive={false} key={furnitureItem._id} furnitureItem={furnitureItem} + onChipClicked={() => { + handleStartEditItem(furnitureItem._id); + }} /> ))}
-

Please Select a Tag to Delete or Start Editing

- - @@ -119,6 +217,17 @@ export const EditTemplate = ({ )} + + setConfirmDeleteModal(false)} + title={"Delete Furniture Item"} + content={"Are you sure you want to delete the selected Furniture Item?"} + cancelText={"Continue Editing"} + confirmText={"Delete Item"} + buttonLoading={false} + onConfirm={onDelete} + />
); }; diff --git a/frontend/src/components/FurnitureRequest/EditTemplate/styles.module.css b/frontend/src/components/FurnitureRequest/EditTemplate/styles.module.css index 855eca3..941135c 100644 --- a/frontend/src/components/FurnitureRequest/EditTemplate/styles.module.css +++ b/frontend/src/components/FurnitureRequest/EditTemplate/styles.module.css @@ -1,6 +1,6 @@ .row { display: flex; - flex-direction: row; + flex-direction: column; justify-content: space-between; } diff --git a/frontend/src/components/VSRForm/FurnitureItemSelection/index.tsx b/frontend/src/components/VSRForm/FurnitureItemSelection/index.tsx index 6234b6d..67fe015 100644 --- a/frontend/src/components/VSRForm/FurnitureItemSelection/index.tsx +++ b/frontend/src/components/VSRForm/FurnitureItemSelection/index.tsx @@ -8,6 +8,7 @@ export interface FurnitureItemSelectionProps { selection?: FurnitureInput; onChangeSelection?: (newSelection: FurnitureInput) => unknown; isActive: boolean; + onChipClicked?: () => unknown; } /** @@ -19,6 +20,7 @@ export const FurnitureItemSelection = ({ selection, onChangeSelection, isActive, + onChipClicked }: FurnitureItemSelectionProps) => { const handleChipClicked = () => { if (isActive) { @@ -28,12 +30,18 @@ export const FurnitureItemSelection = ({ onChangeSelection!({ ...selection!, quantity: 0 }); } } + else{ + onChipClicked?.() + } }; const incrementCount = () => { if (isActive) { onChangeSelection!({ ...selection!, quantity: selection!.quantity + 1 }); } + else{ + onChipClicked?.() + } }; const decrementCount = () => { @@ -42,6 +50,9 @@ export const FurnitureItemSelection = ({ onChangeSelection!({ ...selection!, quantity: selection!.quantity - 1 }); } } + else{ + onChipClicked?.() + } }; return ( diff --git a/frontend/src/components/VSRIndividual/VSRIndividualPage/index.tsx b/frontend/src/components/VSRIndividual/VSRIndividualPage/index.tsx index fbcdd8e..40363ce 100644 --- a/frontend/src/components/VSRIndividual/VSRIndividualPage/index.tsx +++ b/frontend/src/components/VSRIndividual/VSRIndividualPage/index.tsx @@ -10,7 +10,14 @@ import { } from "@/components/VSRIndividual"; import styles from "@/components/VSRIndividual/VSRIndividualPage/styles.module.css"; import Image from "next/image"; -import { type VSR, getVSR, updateVSRStatus, UpdateVSRRequest, updateVSR } from "@/api/VSRs"; +import { + type VSR, + getVSR, + updateVSRStatus, + UpdateVSRRequest, + updateVSR, + deleteVSR, +} from "@/api/VSRs"; import { useParams, useRouter } from "next/navigation"; import { FurnitureItem, getFurnitureItems } from "@/api/FurnitureItems"; import { useScreenSizes } from "@/hooks/useScreenSizes"; @@ -20,7 +27,7 @@ import { ErrorNotification } from "@/components/Errors/ErrorNotification"; import { UserContext } from "@/contexts/userContext"; import { VSRErrorModal } from "@/components/VSRForm/VSRErrorModal"; import { LoadingScreen } from "@/components/shared/LoadingScreen"; -import { DeleteVSRsModal } from "@/components/shared/DeleteVSRsModal"; +import { ConfirmDeleteModal } from "@/components/shared/ConfirmDeleteModal"; import { SubmitHandler, useForm } from "react-hook-form"; import { IEditVSRFormInput } from "@/components/VSRForm/VSRFormTypes"; import { BaseModal } from "@/components/shared/BaseModal"; @@ -66,6 +73,9 @@ export const VSRIndividualPage = () => { const [loadingEdit, setLoadingEdit] = useState(false); const [deleteVsrModalOpen, setDeleteVsrModalOpen] = useState(false); + const [loadingDelete, setLoadingDelete] = useState(false); + const [successNotificationOpen, setSuccessNotificationOpen] = useState(false); + const [errorNotificationOpen, setErrorNotificationOpen] = useState(false); const { isMobile, isTablet } = useScreenSizes(); @@ -173,6 +183,43 @@ export const VSRIndividualPage = () => { setLoadingUpdateStatus(false); }; + const onDelete = async () => { + if (loadingDelete || !firebaseUser) { + return; + } + + setSuccessNotificationOpen(false); + setErrorNotificationOpen(false); + setLoadingDelete(true); + + try { + const firebaseToken = await firebaseUser.getIdToken(); + if (!firebaseToken) { + setLoadingDelete(false); + return; + } + + await deleteVSR(vsr._id, firebaseToken).then((res) => { + if (res.success) { + return Promise.resolve(); + } else { + return Promise.reject(res.error); + } + }), + setSuccessNotificationOpen(true); + // Redirect user to dashboard after deleting VSR, but give them some time to see the success message first + setTimeout(() => { + router.push("/staff/vsr"); + }, 1000); + } catch (error) { + console.error(`Error deleting VSR(s): ${error}`); + setErrorNotificationOpen(true); + } finally { + setLoadingDelete(false); + setDeleteVsrModalOpen(false); + } + }; + /** * Callback triggered when the user clicks "Undo" on the success notification * after updating the VSR's status @@ -580,16 +627,23 @@ export const VSRIndividualPage = () => { actionText="Dismiss" onActionClicked={() => setUpdateStatusErrorNotificationOpen(false)} /> - setDeleteVsrModalOpen(false)} - afterDelete={() => { - // Redirect user to dashboard after deleting VSR, but give them some time to see the success message first - setTimeout(() => { - router.push("/staff/vsr"); - }, 1000); - }} - vsrIds={[vsr._id]} + title="Delete VSR(s)" + content={ + <> + {"Deleted VSR’s "} + cannot + {" be recovered. Are you sure you’d like to delete the selected VSR forms ("} + {1} + {")?"} + + } + cancelText="Cancel" + confirmText="Delete VSR(s)" + onConfirm={onDelete} + buttonLoading={loadingDelete} /> { actionText="Dismiss" onActionClicked={() => setEditErrorNotificationOpen(false)} /> + + setSuccessNotificationOpen(false) }]} + /> + setErrorNotificationOpen(false)} + /> ); }; diff --git a/frontend/src/components/shared/ConfirmDeleteModal/index.tsx b/frontend/src/components/shared/ConfirmDeleteModal/index.tsx new file mode 100644 index 0000000..100f656 --- /dev/null +++ b/frontend/src/components/shared/ConfirmDeleteModal/index.tsx @@ -0,0 +1,56 @@ +import { deleteVSR } from "@/api/VSRs"; +import styles from "@/components/shared/ConfirmDeleteModal/styles.module.css"; +import { UserContext } from "@/contexts/userContext"; +import { ReactElement, ReactNode, useContext, useState } from "react"; +import { SuccessNotification } from "@/components/shared/SuccessNotification"; +import { ErrorNotification } from "@/components/Errors/ErrorNotification"; +import { BaseModal } from "@/components/shared/BaseModal"; +import { Button } from "@/components/shared/Button"; + +interface ConfirmDeleteModalProps { + isOpen: boolean; + onClose: () => unknown; + title: string; + content: string | ReactElement; + cancelText: string; + confirmText: string; + buttonLoading: boolean; + onConfirm: () => unknown; +} + +/** + * A modal that asks the user to confirm whether they want to delete one or more + * VSRs, and then deletes them if they confirm that they want to. + */ +export const ConfirmDeleteModal = ({ isOpen, onClose, title, content, cancelText, confirmText, buttonLoading, onConfirm}: ConfirmDeleteModalProps) => { + return ( + <> + +
+ } + /> + + + ); +}; diff --git a/frontend/src/components/shared/DeleteVSRsModal/styles.module.css b/frontend/src/components/shared/ConfirmDeleteModal/styles.module.css similarity index 100% rename from frontend/src/components/shared/DeleteVSRsModal/styles.module.css rename to frontend/src/components/shared/ConfirmDeleteModal/styles.module.css diff --git a/frontend/src/components/shared/DeleteVSRsModal/index.tsx b/frontend/src/components/shared/DeleteVSRsModal/index.tsx deleted file mode 100644 index c595773..0000000 --- a/frontend/src/components/shared/DeleteVSRsModal/index.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { deleteVSR } from "@/api/VSRs"; -import styles from "@/components/shared/DeleteVSRsModal/styles.module.css"; -import { UserContext } from "@/contexts/userContext"; -import { useContext, useState } from "react"; -import { SuccessNotification } from "@/components/shared/SuccessNotification"; -import { ErrorNotification } from "@/components/Errors/ErrorNotification"; -import { BaseModal } from "@/components/shared/BaseModal"; -import { Button } from "@/components/shared/Button"; - -interface DeleteVSRsModalProps { - isOpen: boolean; - onClose: () => unknown; - afterDelete: () => unknown; - vsrIds: string[]; -} - -/** - * A modal that asks the user to confirm whether they want to delete one or more - * VSRs, and then deletes them if they confirm that they want to. - */ -export const DeleteVSRsModal = ({ isOpen, onClose, afterDelete, vsrIds }: DeleteVSRsModalProps) => { - const { firebaseUser } = useContext(UserContext); - - const [loadingDelete, setLoadingDelete] = useState(false); - const [successNotificationOpen, setSuccessNotificationOpen] = useState(false); - const [errorNotificationOpen, setErrorNotificationOpen] = useState(false); - - const onDelete = async () => { - if (loadingDelete || !firebaseUser) { - return; - } - - setSuccessNotificationOpen(false); - setErrorNotificationOpen(false); - setLoadingDelete(true); - - try { - const firebaseToken = await firebaseUser.getIdToken(); - if (!firebaseToken) { - setLoadingDelete(false); - return; - } - - await Promise.all( - vsrIds.map((vsrId) => - deleteVSR(vsrId, firebaseToken).then((res) => { - if (res.success) { - return Promise.resolve(); - } else { - return Promise.reject(res.error); - } - }), - ), - ); - - setSuccessNotificationOpen(true); - afterDelete(); - } catch (error) { - console.error(`Error deleting VSR(s): ${error}`); - setErrorNotificationOpen(true); - } finally { - setLoadingDelete(false); - onClose(); - } - }; - - return ( - <> - - {"Deleted VSR’s "} - cannot - {" be recovered. Are you sure you’d like to delete the selected VSR forms ("} - {vsrIds.length} - {")?"} - - } - bottomRow={ -
-
- } - /> - setSuccessNotificationOpen(false) }]} - /> - setErrorNotificationOpen(false)} - /> - - ); -}; From 17b9d31ae18caea54b9050820bc83454cb5aefc6 Mon Sep 17 00:00:00 2001 From: 2s2e Date: Thu, 16 May 2024 14:54:59 -0700 Subject: [PATCH 40/74] reset filters button works --- frontend/src/app/staff/vsr/page.tsx | 9 ++++++++- .../src/components/VSRTable/FilterModal/index.tsx | 11 +++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index a695fb6..b9927f6 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -236,12 +236,19 @@ export default function VSRTableView() { /> setIsFilterModalOpen(false)} + onClose={() => { + setIsFilterModalOpen(false); + }} onInputEntered={(zipCodes: string[] | undefined, incomeLevel: string | undefined) => { setFilteredZipCodes(zipCodes); setFilteredIncome(incomeLevel); fetchVSRs(search, zipCodes, incomeLevel, status); }} + onResetFilters={() => { + setFilteredZipCodes(undefined); + setFilteredIncome(undefined); + fetchVSRs(search, undefined, undefined, status); + }} /> ); diff --git a/frontend/src/components/VSRTable/FilterModal/index.tsx b/frontend/src/components/VSRTable/FilterModal/index.tsx index 71a1776..4ce6ea8 100644 --- a/frontend/src/components/VSRTable/FilterModal/index.tsx +++ b/frontend/src/components/VSRTable/FilterModal/index.tsx @@ -5,14 +5,16 @@ import { Button } from "@/components/shared/Button"; import TextField from "@/components/shared/input/TextField"; import MultipleChoice from "@/components/shared/input/MultipleChoice"; import { incomeOptions } from "@/constants/fieldOptions"; +import { on } from "events"; interface FilterModalProps { isOpen: boolean; onClose: () => void; onInputEntered: (zipCodes: string[] | undefined, incomeLevel: string | undefined) => void; + onResetFilters: () => void; } -const FilterModal = ({ isOpen, onClose, onInputEntered }: FilterModalProps) => { +const FilterModal = ({ isOpen, onClose, onInputEntered, onResetFilters }: FilterModalProps) => { if (!isOpen) return null; const [zipCodes, setZipCodes] = useState([]); @@ -28,6 +30,11 @@ const FilterModal = ({ isOpen, onClose, onInputEntered }: FilterModalProps) => { onClose(); // Close the modal }; + const handleReset = () => { + onResetFilters(); + onClose(); // Close the modal + }; + return ( <> { variant="error" outlined text="Reset Selection" - onClick={onClose} + onClick={handleApplyFilter} className={styles.button} /> + + ); +}; + +export default FilterChip; diff --git a/frontend/src/components/VSRTable/FilterChip/styles.module.css b/frontend/src/components/VSRTable/FilterChip/styles.module.css new file mode 100644 index 0000000..2b1ca5a --- /dev/null +++ b/frontend/src/components/VSRTable/FilterChip/styles.module.css @@ -0,0 +1,11 @@ +.filterChip { + border-radius: 64px; + border: 1px solid var(--Secondary-1, #102d5f); + background: #102d5f; + padding: 8px 16px; + justify-content: center; + align-items: center; + gap: 8px; + flex: 1 0 0; + align-self: stretch; +} diff --git a/frontend/src/components/VSRTable/FilterModal/index.tsx b/frontend/src/components/VSRTable/FilterModal/index.tsx index 4ce6ea8..2898f6f 100644 --- a/frontend/src/components/VSRTable/FilterModal/index.tsx +++ b/frontend/src/components/VSRTable/FilterModal/index.tsx @@ -68,7 +68,7 @@ const FilterModal = ({ isOpen, onClose, onInputEntered, onResetFilters }: Filter variant="error" outlined text="Reset Selection" - onClick={handleApplyFilter} + onClick={handleReset} className={styles.button} /> +

{label}

+ ); }; diff --git a/frontend/src/components/VSRTable/FilterChip/styles.module.css b/frontend/src/components/VSRTable/FilterChip/styles.module.css index 2b1ca5a..13bf256 100644 --- a/frontend/src/components/VSRTable/FilterChip/styles.module.css +++ b/frontend/src/components/VSRTable/FilterChip/styles.module.css @@ -1,11 +1,29 @@ .filterChip { - border-radius: 64px; - border: 1px solid var(--Secondary-1, #102d5f); - background: #102d5f; - padding: 8px 16px; + display: flex; + height: 40px; + /* flex-direction: column; */ justify-content: center; align-items: center; gap: 8px; - flex: 1 0 0; + border-radius: 64px; + border: 1px solid var(--Secondary-1, #102d5f); + background: var(--Secondary-1, #102d5f); align-self: stretch; + padding: 8px 16px; +} + +.filterText { + color: #fff; + text-align: center; + font-family: "Open Sans"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.deleteButton { + cursor: pointer; + border: none !important; + background: transparent; } diff --git a/frontend/src/components/shared/StatusDropdown/index.tsx b/frontend/src/components/shared/StatusDropdown/index.tsx index 4cd1589..739068d 100644 --- a/frontend/src/components/shared/StatusDropdown/index.tsx +++ b/frontend/src/components/shared/StatusDropdown/index.tsx @@ -15,6 +15,10 @@ export interface StatusOption { * All available statuses that can be set using the status dropdown */ export const STATUS_OPTIONS: StatusOption[] = [ + { + value: "Any", + color: "ffffff", + }, { value: "Received", color: "#e6e6e6", From a584520b0e59e1528ea26f4bc69d94efc04ac5fb Mon Sep 17 00:00:00 2001 From: Yoto Kim Date: Wed, 22 May 2024 10:31:11 -0700 Subject: [PATCH 43/74] Added styling to box shadows and buttons --- frontend/public/ic_edit_light.svg | 5 + .../src/app/staff/furnitureItems/page.tsx | 2 +- frontend/src/app/staff/vsr/page.tsx | 24 ++-- .../FurnitureRequest/EditTemplate/index.tsx | 132 ++++++++++-------- .../EditTemplate/styles.module.css | 101 +++++++++----- .../VSRForm/FurnitureItemSelection/index.tsx | 17 +-- .../shared/ConfirmDeleteModal/index.tsx | 12 +- 7 files changed, 176 insertions(+), 117 deletions(-) create mode 100644 frontend/public/ic_edit_light.svg diff --git a/frontend/public/ic_edit_light.svg b/frontend/public/ic_edit_light.svg new file mode 100644 index 0000000..5b1de93 --- /dev/null +++ b/frontend/public/ic_edit_light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/app/staff/furnitureItems/page.tsx b/frontend/src/app/staff/furnitureItems/page.tsx index 0d05392..2b9081a 100644 --- a/frontend/src/app/staff/furnitureItems/page.tsx +++ b/frontend/src/app/staff/furnitureItems/page.tsx @@ -28,7 +28,7 @@ export default function furnitureItemTemplate() { setFurnitureItems([]); } }); - } + }; const furnitureCategoriesToItems = useMemo( () => diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 3ea1f89..3726897 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -103,16 +103,20 @@ export default function VSRTableView() { return; } - await Promise.all(selectedVsrIds.map(vsrId => deleteVSR(vsrId, firebaseToken).then((res) => { - if (res.success) { - return Promise.resolve(); - } else { - return Promise.reject(res.error); - } - }))) - setSuccessNotificationOpen(true); - setSelectedVsrIds([]); - fetchVSRs(); + await Promise.all( + selectedVsrIds.map((vsrId) => + deleteVSR(vsrId, firebaseToken).then((res) => { + if (res.success) { + return Promise.resolve(); + } else { + return Promise.reject(res.error); + } + }), + ), + ); + setSuccessNotificationOpen(true); + setSelectedVsrIds([]); + fetchVSRs(); } catch (error) { console.error(`Error deleting VSR(s): ${error}`); setErrorNotificationOpen(true); diff --git a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx index 1cdc059..c0a8bb2 100644 --- a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx +++ b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx @@ -12,8 +12,10 @@ import { FurnitureItemSelection } from "@/components/VSRForm/FurnitureItemSelect import { FieldDetail } from "@/components/VSRIndividual/FieldDetails/FieldDetail"; import TextField from "@/components/shared/input/TextField"; import { UserContext } from "@/contexts/userContext"; +3; import { Checkbox, FormControlLabel } from "@mui/material"; import { ConfirmDeleteModal } from "@/components/shared/ConfirmDeleteModal"; +import { Button } from "@/components/shared/Button"; export interface EditTemplateProps { furnitureItems: FurnitureItem[]; @@ -63,9 +65,9 @@ export const EditTemplate = ({ setEditingItemId(null); }; - const onDelete = async() => { + const onDelete = async () => { const firebaseToken = await firebaseUser?.getIdToken(); - if (!firebaseToken || editingItemId===null) { + if (!firebaseToken || editingItemId === null) { return; } const response = await deleteFurnitureItem(editingItemId, firebaseToken); @@ -75,7 +77,7 @@ export const EditTemplate = ({ console.error(`Cannot delete Furniture Item. Error: ${response.error}`); } setConfirmDeleteModal(false); - } + }; const handleAddNewItem = () => { setIsAddingNewItem(true); @@ -99,11 +101,9 @@ export const EditTemplate = ({ } else { console.error(`Cannot create Furniture Item. Error: ${response.error}`); } - } - - else if (editingItemId !== null){ + } else if (editingItemId !== null) { const furnitureItem = getFurnitureItemById(editingItemId); - if(furnitureItem===null){ + if (furnitureItem === null) { return; //Put error here instead } const editFurnitureItem: FurnitureItem = { @@ -111,13 +111,17 @@ export const EditTemplate = ({ category: categoryName, name: itemName, allowMultiple: allowMultiple, - categoryIndex: furnitureItem.categoryIndex - } + categoryIndex: furnitureItem.categoryIndex, + }; const firebaseToken = await firebaseUser?.getIdToken(); if (!firebaseToken) { return; } - const response = await updateFurnitureItem(furnitureItem._id, editFurnitureItem, firebaseToken); + const response = await updateFurnitureItem( + furnitureItem._id, + editFurnitureItem, + firebaseToken, + ); if (response.success) { onFinishEditing(); } else { @@ -125,32 +129,36 @@ export const EditTemplate = ({ } } - setEditingItemId(null); setIsAddingNewItem(false); }; return ( -
- {isEditing ?

Please Select a Tag to Delete or Start Editing

: null} +
+ {isEditing ? ( +

Select a Tag to edit or add new option

+ ) : null} +
+ {furnitureItems.map((furnitureItem) => ( + { + if (isEditing) { + handleStartEditItem(furnitureItem._id); + } + }} + /> + ))} +
+ {isEditing ? ( <> -
- {furnitureItems.map((furnitureItem) => ( - { - handleStartEditItem(furnitureItem._id); - }} - /> - ))} -
- {isAddingNewItem || editingItemId !== null ? ( <> +
) : null} - - - - - - - +
+ + + + + + + +
) : ( <> -
- {furnitureItems.map((furnitureItem) => ( - - ))} -
- + */} +
+
+
+ {zipCodes?.map((zipCode) => ( + z !== zipCode)); + }} + /> + ))} +
} bottomRow={ diff --git a/frontend/src/components/VSRTable/FilterModal/styles.module.css b/frontend/src/components/VSRTable/FilterModal/styles.module.css index 8efa2a9..174e5c6 100644 --- a/frontend/src/components/VSRTable/FilterModal/styles.module.css +++ b/frontend/src/components/VSRTable/FilterModal/styles.module.css @@ -35,3 +35,26 @@ font-size: 24px; font-weight: 700; } + +.filterChips { + display: flex; + flex-direction: row; + gap: 16px; + margin: 0 0 16px; +} + +.zipCodeContainer { + display: flex; + flex-direction: row; + gap: 32px; + align-items: flex-end; +} + +.enterButton { + width: 20%; + padding: 12px 24px; + text-align: center; + font-family: "Lora"; + font-size: 18px; + font-weight: 700; +} From 4c4494430ca09afddec7b16e23a0f471008f59b5 Mon Sep 17 00:00:00 2001 From: 2s2e Date: Thu, 23 May 2024 11:21:51 -0700 Subject: [PATCH 46/74] Added error for when zipcode isn't formatted properly --- frontend/src/app/staff/vsr/page.tsx | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index d9aa6ea..496c4e0 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -24,6 +24,7 @@ import { SuccessNotification } from "@/components/shared/SuccessNotification"; enum VSRTableError { CANNOT_FETCH_VSRS_NO_INTERNET, CANNOT_FETCH_VSRS_INTERNAL, + ZIPCODE_INVALID, NONE, } @@ -146,6 +147,30 @@ export default function VSRTableView() { }} /> ); + case VSRTableError.ZIPCODE_INVALID: + return ( + { + setTableError(VSRTableError.NONE); + }} + imageComponent={ + Internal Error + } + title="Zip Code Formatting Error" + content="Each zip code must be exactly 5 digits long. Please correct the zip code and try again." + buttonText="Try Again" + onButtonClicked={() => { + setTableError(VSRTableError.NONE); + fetchVSRs(); + }} + /> + ); default: return null; } @@ -376,6 +401,15 @@ export default function VSRTableView() { setIsFilterModalOpen(false); }} onInputEntered={(zipCodes: string[] | undefined, incomeLevel: string | undefined) => { + console.log(zipCodes); + if (zipCodes) { + for (let i = 0; i < zipCodes.length; i++) { + if (zipCodes[i].length != 5) { + setTableError(VSRTableError.ZIPCODE_INVALID); + return; + } + } + } setFilteredZipCodes(zipCodes); setFilteredIncome(incomeLevel); fetchVSRs(search, zipCodes, incomeLevel, status); From ad0a2b59d2a466aff05f208ec8e525ecb332ecb6 Mon Sep 17 00:00:00 2001 From: HarshGurnani Date: Fri, 24 May 2024 11:42:56 -0700 Subject: [PATCH 47/74] minor changes --- frontend/src/app/staff/vsr/page.tsx | 4 ++-- frontend/src/components/VSRTable/FilterModal/index.tsx | 2 +- frontend/src/components/shared/StatusDropdown/index.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index d9aa6ea..9e9729a 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -252,9 +252,9 @@ export default function VSRTableView() {

Status:

{ - if (value === "Any") { + if (value === "All Statuses") { setStatus(undefined); fetchVSRs(search, filteredZipCodes, filteredIncome, undefined); } else { diff --git a/frontend/src/components/VSRTable/FilterModal/index.tsx b/frontend/src/components/VSRTable/FilterModal/index.tsx index dbb4bcf..b0cb9ba 100644 --- a/frontend/src/components/VSRTable/FilterModal/index.tsx +++ b/frontend/src/components/VSRTable/FilterModal/index.tsx @@ -6,7 +6,6 @@ import TextField from "@/components/shared/input/TextField"; import MultipleChoice from "@/components/shared/input/MultipleChoice"; import { incomeOptions } from "@/constants/fieldOptions"; import FilterChip from "@/components/VSRTable/FilterChip"; -import { on } from "events"; interface FilterModalProps { isOpen: boolean; @@ -85,6 +84,7 @@ const FilterModal = ({ isOpen, onClose, onInputEntered, onResetFilters }: Filter Date: Fri, 24 May 2024 11:44:52 -0700 Subject: [PATCH 48/74] Zip code filtering appends zip codes instead of overwritting what is there --- frontend/src/app/staff/vsr/page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 496c4e0..27017d4 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -410,7 +410,10 @@ export default function VSRTableView() { } } } - setFilteredZipCodes(zipCodes); + //add zipCodes to filteredZipCodes and zipcodes + const combinedZipCodes = [...(filteredZipCodes ?? []), ...(zipCodes ?? [])]; + + setFilteredZipCodes(combinedZipCodes); setFilteredIncome(incomeLevel); fetchVSRs(search, zipCodes, incomeLevel, status); }} From 1cd5ee777151acf9af3843cc00d7b308d0f5de96 Mon Sep 17 00:00:00 2001 From: HarshGurnani Date: Fri, 24 May 2024 12:54:50 -0700 Subject: [PATCH 49/74] changes discussed in meeting; need to change popup to not be full screen --- frontend/src/app/staff/vsr/page.tsx | 4 +- .../components/VSRTable/FilterModal/index.tsx | 40 ++++++------------- .../VSRTable/FilterModal/styles.module.css | 16 +------- 3 files changed, 18 insertions(+), 42 deletions(-) diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 9e9729a..79deeaf 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -304,7 +304,9 @@ export default function VSRTableView() { {/* {searchOnOwnRow ? : null} */} -

Applied Filters:

+ {(filteredZipCodes && filteredZipCodes.length > 0) || filteredIncome ? ( +

Applied Filters:

+ ) : null} {filteredZipCodes?.map((zipCode) => ( { - if (currentZipCode.trim()) { - setZipCodes((prevZipCodes) => [...prevZipCodes, currentZipCode.trim()]); - setCurrentZipCode(""); // Clear the text field after adding the zipcode - } - }; - return ( <>
-
- -
+
{zipCodes?.map((zipCode) => ( @@ -127,7 +111,9 @@ const FilterModal = ({ isOpen, onClose, onInputEntered, onResetFilters }: Filter outlined={false} text="Apply Filters" onClick={handleApplyFilter} - className={styles.button} + className={`${styles.button} ${ + zipCodes.length > 0 || currentZipCode !== "" || income !== "" ? "" : styles.disabled + }`} />
} diff --git a/frontend/src/components/VSRTable/FilterModal/styles.module.css b/frontend/src/components/VSRTable/FilterModal/styles.module.css index 174e5c6..70e224d 100644 --- a/frontend/src/components/VSRTable/FilterModal/styles.module.css +++ b/frontend/src/components/VSRTable/FilterModal/styles.module.css @@ -43,18 +43,6 @@ margin: 0 0 16px; } -.zipCodeContainer { - display: flex; - flex-direction: row; - gap: 32px; - align-items: flex-end; -} - -.enterButton { - width: 20%; - padding: 12px 24px; - text-align: center; - font-family: "Lora"; - font-size: 18px; - font-weight: 700; +.disabled { + background-color: grey !important; } From 8059c1ba604466b4227f6a70093fc75d73f359b2 Mon Sep 17 00:00:00 2001 From: benjaminjohnson2204 Date: Mon, 27 May 2024 16:00:17 -0700 Subject: [PATCH 50/74] Remove commented-out code & touch up styles --- backend/src/controllers/vsr.ts | 95 +++------ backend/src/middleware/auth.ts | 2 - frontend/src/api/VSRs.ts | 47 ++--- frontend/src/app/staff/vsr/page.module.css | 35 +++- frontend/src/app/staff/vsr/page.tsx | 115 +++++------ .../PageSections/CaseDetails/index.tsx | 1 + .../components/VSRTable/FilterChip/index.tsx | 6 +- .../VSRTable/FilterChip/styles.module.css | 24 ++- .../components/VSRTable/FilterModal/index.tsx | 189 ++++++++++-------- .../VSRTable/FilterModal/styles.module.css | 56 ++++-- .../VSRTable/SearchKeyword/index.tsx | 14 +- .../shared/StatusDropdown/index.tsx | 27 ++- 12 files changed, 306 insertions(+), 305 deletions(-) diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts index 7798471..f9ed160 100644 --- a/backend/src/controllers/vsr.ts +++ b/backend/src/controllers/vsr.ts @@ -19,86 +19,39 @@ type FurnitureItemEntry = FurnitureItem & { _id: ObjectId }; */ export const getAllVSRS: RequestHandler = async (req, res, next) => { try { - let vsrs; - if (req.query.search) { - // const vsrs = await VSRModel.find({ $text: { $search: req.query.search as string } }).sort({ - // //by status in a particular order - // status: 1, - // //and then by name - // name: 1, - // }); - const searchTerm = req.query.search as string; - const regex = new RegExp(searchTerm, "i"); - - vsrs = await VSRModel.aggregate([ - { - $match: { name: { $regex: regex } }, - }, - { - $addFields: { - statusOrder: { - $switch: { - branches: [ - { case: { $eq: ["$status", "Received"] }, then: 1 }, - { case: { $eq: ["$status", "Approved"] }, then: 2 }, - { case: { $eq: ["$status", "Appointment Scheduled"] }, then: 3 }, - { case: { $eq: ["$status", "Complete"] }, then: 4 }, - { case: { $eq: ["$status", "No-show / Incomplete"] }, then: 5 }, - { case: { $eq: ["$status", "Archived"] }, then: 6 }, - ], - default: 99, - }, + let vsrs = await VSRModel.aggregate([ + ...(req.query.search + ? [ + { + $match: { name: { $regex: new RegExp(req.query.search as string) } }, }, - }, - }, - { $sort: { statusOrder: 1, dateReceived: -1 } }, - ]); - } else { - // const vsrs = await VSRModel.find().sort({ - // //by status in a particular order - // status: 1, - // //and then by name - // dateReceived: -1, - // }); - - vsrs = await VSRModel.aggregate([ - { - $addFields: { - statusOrder: { - $switch: { - branches: [ - { case: { $eq: ["$status", "Received"] }, then: 1 }, - { case: { $eq: ["$status", "Approved"] }, then: 2 }, - { case: { $eq: ["$status", "Appointment Scheduled"] }, then: 3 }, - { case: { $eq: ["$status", "Complete"] }, then: 4 }, - { case: { $eq: ["$status", "No-show / Incomplete"] }, then: 5 }, - { case: { $eq: ["$status", "Archived"] }, then: 6 }, - ], - default: 99, - }, + ] + : []), + { + $addFields: { + statusOrder: { + $switch: { + branches: [ + { case: { $eq: ["$status", "Received"] }, then: 1 }, + { case: { $eq: ["$status", "Approved"] }, then: 2 }, + { case: { $eq: ["$status", "Appointment Scheduled"] }, then: 3 }, + { case: { $eq: ["$status", "Complete"] }, then: 4 }, + { case: { $eq: ["$status", "No-show / Incomplete"] }, then: 5 }, + { case: { $eq: ["$status", "Archived"] }, then: 6 }, + ], + default: 99, }, }, }, - { $sort: { statusOrder: 1, dateReceived: -1 } }, - ]); - } + }, + { $sort: { statusOrder: 1, dateReceived: -1 } }, + ]); if (req.query.status) { vsrs = vsrs.filter((vsr) => vsr.status === req.query.status); } - if ( - req.query.incomeLevel - // && - // typeof req.query.incomeLevel === "string" && - // req.query.incomeLevel in ["50000", "25000", "12500", "0"] - ) { - /* - "$12,500 and under", - "$12,501 - $25,000", - "$25,001 - $50,000", - "$50,001 and over", - */ + if (req.query.incomeLevel) { const incomeMap: { [key: string]: string } = { "50000": "$50,001 and over", "25000": "$25,001 - $50,000", diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index fefec49..4836fd6 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -19,7 +19,6 @@ const requireSignedIn = async (req: PAPRequest, res: Response, next: NextFunctio const authHeader = req.headers.authorization; // Token shoud be "Bearer: " const token = authHeader?.split("Bearer ")[1]; - if (!token) { return res .status(AuthError.TOKEN_NOT_IN_HEADER.status) @@ -72,7 +71,6 @@ const requireAdmin = async (req: PAPRequest, res: Response, next: NextFunction) if (!user || user.role !== UserRole.ADMIN) { return res.status(AuthError.NOT_ADMIN.status).send(AuthError.NOT_ADMIN.displayMessage(true)); } - return next(); }; diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts index 81d68c9..ccb130a 100644 --- a/frontend/src/api/VSRs.ts +++ b/frontend/src/api/VSRs.ts @@ -170,37 +170,26 @@ export async function getAllVSRs( "$12,500 and under": "0", }; - try { - let url_string = ``; - url_string = `/api/vsr`; - if (search) { - url_string += `?search=${search}`; - } - if (zipCodes) { - if (search) { - url_string += `&zipCode=${zipCodes}`; - } else { - url_string += `?zipCode=${zipCodes}`; - } - } - if (income) { - if (search || zipCodes) { - url_string += `&incomeLevel=${incomeMap[income]}`; - } else { - url_string += `?incomeLevel=${incomeMap[income]}`; - } - } - if (status) { - if (search || zipCodes || income) { - url_string += `&status=${status}`; - } else { - url_string += `?status=${status}`; - } - } + const searchParams = new URLSearchParams(); + if (search) { + searchParams.set("search", search); + } + if (zipCodes) { + searchParams.set("zipCode", zipCodes.join(", ")); + } + if (income) { + searchParams.set("incomeLevel", incomeMap[income]); + } + if (status) { + searchParams.set("status", status); + } + + const searchParamsString = searchParams.toString(); + const urlString = `/api/vsr${searchParamsString ? "?" + searchParamsString : ""}`; - const response = await get(url_string, createAuthHeader(firebaseToken)); + try { + const response = await get(urlString, createAuthHeader(firebaseToken)); const json = (await response.json()) as VSRListJson; - console.log(url_string); return { success: true, data: json.vsrs.map(parseVSR) }; } catch (error) { return handleAPIError(error); diff --git a/frontend/src/app/staff/vsr/page.module.css b/frontend/src/app/staff/vsr/page.module.css index 03b1eaf..1224996 100644 --- a/frontend/src/app/staff/vsr/page.module.css +++ b/frontend/src/app/staff/vsr/page.module.css @@ -72,25 +72,28 @@ width: 100%; } +.appliedFiltersContainer { + display: flex; + flex-direction: row; + gap: 16px; + align-items: center; + flex-wrap: wrap; +} + .filterChips { display: flex; flex-direction: row; gap: 16px; - margin: 0 0 16px; + align-items: center; + flex-wrap: wrap; } .appliedText { - display: flex; - width: 138px; - flex-direction: column; - justify-content: center; - align-self: stretch; color: #222; font-family: "Open Sans"; font-size: 20px; - font-style: normal; font-weight: 400; - line-height: normal; + text-wrap: nowrap; } /* shrink the margins at a screen size larger than tablet to avoid overflow */ @@ -100,6 +103,22 @@ } } +/* tablet version */ +@media screen and (max-width: 850px) { + .appliedFiltersContainer { + flex-direction: column; + align-items: start; + } + + .filterChips { + flex-wrap: wrap; + } + + .appliedText { + font-size: 14px; + } +} + @media screen and (max-width: 600px) { .row_right { gap: 4px; diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 9c4e09c..09e4d82 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -52,11 +52,13 @@ export default function VSRTableView() { const [selectedVsrIds, setSelectedVsrIds] = useState([]); const [deleteVsrModalOpen, setDeleteVsrModalOpen] = useState(false); - const [isFilterModalOpen, setIsFilterModalOpen] = useState(false); - const [filteredZipCodes, setFilteredZipCodes] = useState(undefined); - const [filteredIncome, setFilteredIncome] = useState(undefined); - const [search, setSearch] = useState(undefined); - const [status, setStatus] = useState(undefined); + const [filterModalAnchorElement, setFilterModalAnchorElement] = useState( + null, + ); + const [filteredZipCodes, setFilteredZipCodes] = useState(); + const [filteredIncome, setFilteredIncome] = useState(); + const [search, setSearch] = useState(); + const [status, setStatus] = useState(); useRedirectToLoginIfNotSignedIn(); @@ -65,14 +67,14 @@ export default function VSRTableView() { /** * Fetches the list of all VSRs from the backend and updates our vsrs state. */ - const fetchVSRs = (search?: string, zipCodes?: string[], income?: string, status?: string) => { + const fetchVSRs = () => { if (!firebaseUser) { return; } setLoadingVsrs(true); firebaseUser?.getIdToken().then((firebaseToken) => { - getAllVSRs(firebaseToken, search, zipCodes, income, status).then((result) => { + getAllVSRs(firebaseToken, search, filteredZipCodes, filteredIncome, status).then((result) => { if (result.success) { setVsrs(result.data); } else { @@ -91,7 +93,7 @@ export default function VSRTableView() { // Fetch the VSRs from the backend once the Firebase user loads. useEffect(() => { fetchVSRs(); - }, [firebaseUser]); + }, [firebaseUser, search, filteredZipCodes, filteredIncome, status]); /** * Renders an error modal corresponding to the page's error state, or renders @@ -257,6 +259,8 @@ export default function VSRTableView() { }); }; + const renderSearchBar = () => ; + return (
@@ -264,14 +268,7 @@ export default function VSRTableView() {
- {searchOnOwnRow ? null : ( - { - setSearch(search); - fetchVSRs(search, filteredZipCodes, filteredIncome, status); - }} - /> - )} + {searchOnOwnRow ? null : renderSearchBar()}

Status:

@@ -279,14 +276,9 @@ export default function VSRTableView() { { - if (value === "All Statuses") { - setStatus(undefined); - fetchVSRs(search, filteredZipCodes, filteredIncome, undefined); - } else { - setStatus(value); - fetchVSRs(search, filteredZipCodes, filteredIncome, value); - } + setStatus(value === "All Statuses" ? undefined : value); }} + includeAllStatuses />
@@ -311,7 +303,7 @@ export default function VSRTableView() { iconAlt="Filter" text="Filter" hideTextOnMobile - onClick={() => setIsFilterModalOpen(true)} + onClick={(e) => setFilterModalAnchorElement(e.target as HTMLElement)} /> )}
- {/* {searchOnOwnRow ? : null} */} + {searchOnOwnRow ? renderSearchBar() : null} - - {(filteredZipCodes && filteredZipCodes.length > 0) || filteredIncome ? ( + {(filteredZipCodes && filteredZipCodes.length > 0) || filteredIncome ? ( +

Applied Filters:

- ) : null} - {filteredZipCodes?.map((zipCode) => ( - z !== zipCode)); - fetchVSRs( - search, - filteredZipCodes?.filter((z) => z !== zipCode), - filteredIncome, - status, - ); - }} - /> - ))} - {filteredIncome ? ( - - ) : null} - + + {filteredZipCodes?.map((zipCode) => ( + { + setFilteredZipCodes(filteredZipCodes?.filter((z) => z !== zipCode)); + }} + /> + ))} + {filteredIncome ? ( + { + setFilteredIncome(undefined); + }} + /> + ) : null} + +
+ ) : null}
{loadingVsrs ? ( @@ -396,31 +383,19 @@ export default function VSRTableView() { vsrIds={selectedVsrIds} /> { - setIsFilterModalOpen(false); + setFilterModalAnchorElement(null); }} + initialZipCodes={filteredZipCodes ?? []} + initialIncomeLevel={filteredIncome ?? ""} onInputEntered={(zipCodes: string[] | undefined, incomeLevel: string | undefined) => { - console.log(zipCodes); - if (zipCodes) { - for (let i = 0; i < zipCodes.length; i++) { - if (zipCodes[i].length != 5) { - setTableError(VSRTableError.ZIPCODE_INVALID); - return; - } - } - } - //add zipCodes to filteredZipCodes and zipcodes - const combinedZipCodes = [...(filteredZipCodes ?? []), ...(zipCodes ?? [])]; - - setFilteredZipCodes(combinedZipCodes); + setFilteredZipCodes(zipCodes); setFilteredIncome(incomeLevel); - fetchVSRs(search, zipCodes, incomeLevel, status); }} onResetFilters={() => { setFilteredZipCodes(undefined); setFilteredIncome(undefined); - fetchVSRs(search, undefined, undefined, status); }} />
diff --git a/frontend/src/components/VSRIndividual/PageSections/CaseDetails/index.tsx b/frontend/src/components/VSRIndividual/PageSections/CaseDetails/index.tsx index 61f9b43..ab97db6 100644 --- a/frontend/src/components/VSRIndividual/PageSections/CaseDetails/index.tsx +++ b/frontend/src/components/VSRIndividual/PageSections/CaseDetails/index.tsx @@ -55,6 +55,7 @@ export const CaseDetails = ({ ); }; diff --git a/frontend/src/components/VSRTable/FilterChip/index.tsx b/frontend/src/components/VSRTable/FilterChip/index.tsx index 5daa69a..ea3571c 100644 --- a/frontend/src/components/VSRTable/FilterChip/index.tsx +++ b/frontend/src/components/VSRTable/FilterChip/index.tsx @@ -1,4 +1,5 @@ import styles from "@/components/VSRTable/FilterChip/styles.module.css"; +import { useScreenSizes } from "@/hooks/useScreenSizes"; import Image from "next/image"; interface FilterChipProps { @@ -7,11 +8,14 @@ interface FilterChipProps { } const FilterChip = ({ label, onDelete }: FilterChipProps) => { + const { isTablet } = useScreenSizes(); + const iconSize = isTablet ? 18 : 24; + return (

{label}

); diff --git a/frontend/src/components/VSRTable/FilterChip/styles.module.css b/frontend/src/components/VSRTable/FilterChip/styles.module.css index 13bf256..24df3d6 100644 --- a/frontend/src/components/VSRTable/FilterChip/styles.module.css +++ b/frontend/src/components/VSRTable/FilterChip/styles.module.css @@ -1,7 +1,6 @@ .filterChip { display: flex; height: 40px; - /* flex-direction: column; */ justify-content: center; align-items: center; gap: 8px; @@ -23,7 +22,30 @@ } .deleteButton { + display: flex; cursor: pointer; border: none !important; background: transparent; } + +/* tablet version */ +@media screen and (max-width: 850px) { + .filterChip { + height: 36px; + } + + .filterText { + font-size: 14px; + } +} + +/* mobile version */ +@media screen and (max-width: 550px) { + .filterChip { + height: 30px; + } + + .filterText { + font-size: 12px; + } +} diff --git a/frontend/src/components/VSRTable/FilterModal/index.tsx b/frontend/src/components/VSRTable/FilterModal/index.tsx index 6d1f13e..f35b082 100644 --- a/frontend/src/components/VSRTable/FilterModal/index.tsx +++ b/frontend/src/components/VSRTable/FilterModal/index.tsx @@ -1,124 +1,147 @@ import styles from "@/components/VSRTable/FilterModal/styles.module.css"; -import { useState } from "react"; -import { BaseModal } from "@/components/shared/BaseModal"; +import { useEffect, useState } from "react"; import { Button } from "@/components/shared/Button"; import TextField from "@/components/shared/input/TextField"; import MultipleChoice from "@/components/shared/input/MultipleChoice"; import { incomeOptions } from "@/constants/fieldOptions"; import FilterChip from "@/components/VSRTable/FilterChip"; +import { ClickAwayListener, Popper } from "@mui/material"; interface FilterModalProps { - isOpen: boolean; - onClose: () => void; + anchorElement: HTMLElement | null; + initialZipCodes: string[]; + initialIncomeLevel: string; + onClose: () => unknown; onInputEntered: (zipCodes: string[] | undefined, incomeLevel: string | undefined) => void; onResetFilters: () => void; } -const FilterModal = ({ isOpen, onClose, onInputEntered, onResetFilters }: FilterModalProps) => { - if (!isOpen) return null; - - const finalZipCodes: string[] = []; - const [zipCodes, setZipCodes] = useState([]); +const FilterModal = ({ + anchorElement, + initialZipCodes, + initialIncomeLevel, + onClose, + onInputEntered, + onResetFilters, +}: FilterModalProps) => { + const [zipCodes, setZipCodes] = useState(initialZipCodes); const [currentZipCode, setCurrentZipCode] = useState(""); + const [zipCodeError, setZipCodeError] = useState(false); + + const [income, setIncome] = useState(initialIncomeLevel); + + useEffect(() => { + setZipCodes(initialZipCodes); + setIncome(initialIncomeLevel); + }, [initialZipCodes, initialIncomeLevel]); - const [income, setIncome] = useState(""); + const applyButtonEnabled = zipCodes.length > 0 || currentZipCode !== "" || income !== ""; const handleZipCodeChange = (e: React.ChangeEvent) => { - // setZipCodes(e.target.value.split(",").map((zip) => zip.trim())); // Assuming zip codes are comma-separated setCurrentZipCode(e.target.value); + setZipCodeError(false); }; const handleApplyFilter = () => { - // Pass the entered zip codes to the parent component when the user applies the filter - zipCodes.forEach((zipCode) => { - finalZipCodes.push(zipCode); - }); - if (currentZipCode.trim() !== "") { - finalZipCodes.push(currentZipCode); + if (currentZipCode.trim() !== "" && currentZipCode.length !== 5) { + setZipCodeError(true); + return; } - onInputEntered(finalZipCodes, income); + // Pass the entered zip codes to the parent component when the user applies the filter + onInputEntered( + [...zipCodes, ...(currentZipCode.trim() === "" ? [] : [currentZipCode.trim()])], + income, + ); + setCurrentZipCode(""); + setZipCodeError(false); onClose(); // Close the modal }; const handleReset = () => { onResetFilters(); + setCurrentZipCode(""); + setZipCodeError(false); onClose(); // Close the modal }; const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter") { if (currentZipCode.trim()) { - setZipCodes((prevZipCodes) => [...prevZipCodes, currentZipCode.trim()]); - setCurrentZipCode(""); // Clear the text field after adding the zipcode + if (currentZipCode.length === 5) { + setZipCodeError(false); + setZipCodes((prevZipCodes) => [...prevZipCodes, currentZipCode.trim()]); + setCurrentZipCode(""); // Clear the text field after adding the zipcode + } else { + setZipCodeError(true); + } } event.preventDefault(); // Prevent the default action of the Enter key } }; - return ( - <> - - - setIncome(Array.isArray(newValue) ? newValue[0] : newValue) - } - required - > -
- -
-
- {zipCodes?.map((zipCode) => ( - z !== zipCode)); - }} - /> - ))} -
+ const renderPopper = () => ( + +
+ + setIncome(Array.isArray(newValue) ? newValue[0] : newValue) + } + required={false} + /> + + {zipCodes && zipCodes.length > 0 ? ( +
+ {zipCodes?.map((zipCode) => ( + { + setZipCodes(zipCodes?.filter((z) => z !== zipCode)); + }} + /> + ))}
- } - bottomRow={ -
-
- } - /> - + ) : null} + +
+
+
+
+ ); + + return anchorElement === null ? ( + renderPopper() + ) : ( + {renderPopper()} ); }; diff --git a/frontend/src/components/VSRTable/FilterModal/styles.module.css b/frontend/src/components/VSRTable/FilterModal/styles.module.css index 70e224d..9185642 100644 --- a/frontend/src/components/VSRTable/FilterModal/styles.module.css +++ b/frontend/src/components/VSRTable/FilterModal/styles.module.css @@ -1,32 +1,28 @@ -.overlay { - position: "fixed"; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: "rgba(0, 0, 0, 0.7)"; - display: "flex"; - align-items: "center"; - justify-content: "center"; -} -.modal { - background-color: "#fff"; - padding: "20px"; - border-radius: "5px"; - width: "300px"; - box-shadow: "0 4px 6px rgba(0, 0, 0, 0.1)"; +.root { + display: flex; + flex-direction: column; + background-color: white; + border-radius: 8px; + box-shadow: 0px 0px 24px 0px rgba(0, 0, 0, 0.2); + padding: 16px; + gap: 32px; + max-width: 410px; + margin-top: 12px; } + .closeButton { float: "right"; border: "none"; background: "none"; cursor: "pointer"; } + .buttonContainer { display: flex; flex-direction: row; - gap: 32px; + gap: 16px; } + .button { width: 100%; padding: 12px 24px; @@ -40,9 +36,31 @@ display: flex; flex-direction: row; gap: 16px; - margin: 0 0 16px; + align-items: center; + flex-wrap: wrap; } .disabled { background-color: grey !important; } + +/* tablet version */ +@media screen and (max-width: 850px) { + .root { + max-width: calc(100vw - 96px); + margin-left: 48px; + margin-top: 8px; + } + + .buttonContainer { + gap: 32px; + } +} + +/* mobile version */ +@media screen and (max-width: 550px) { + .root { + max-width: calc(100vw - 48px); + margin-left: 24px; + } +} diff --git a/frontend/src/components/VSRTable/SearchKeyword/index.tsx b/frontend/src/components/VSRTable/SearchKeyword/index.tsx index 6309671..7833cf6 100644 --- a/frontend/src/components/VSRTable/SearchKeyword/index.tsx +++ b/frontend/src/components/VSRTable/SearchKeyword/index.tsx @@ -1,6 +1,7 @@ import styles from "@/components/VSRTable/SearchKeyword/styles.module.css"; import Image from "next/image"; -import React, { useState } from "react"; +import React from "react"; +import { debounce } from "@mui/material"; /** * A component for the Search input above the VSR table. @@ -11,14 +12,6 @@ interface SearchProps { } export const SearchKeyword = ({ onUpdate }: SearchProps) => { - const [searchInput, setSearchInput] = useState(""); - - const handleInputChange = (event: { target: { value: string } }) => { - const input = event.target.value; - setSearchInput(input); - onUpdate(input); - }; - return (
{/* image */} @@ -26,8 +19,7 @@ export const SearchKeyword = ({ onUpdate }: SearchProps) => { onUpdate(e.target.value), 250)} />
); diff --git a/frontend/src/components/shared/StatusDropdown/index.tsx b/frontend/src/components/shared/StatusDropdown/index.tsx index 01a45be..321eefc 100644 --- a/frontend/src/components/shared/StatusDropdown/index.tsx +++ b/frontend/src/components/shared/StatusDropdown/index.tsx @@ -15,10 +15,6 @@ export interface StatusOption { * All available statuses that can be set using the status dropdown */ export const STATUS_OPTIONS: StatusOption[] = [ - { - value: "All Statuses", - color: "#ffffff", - }, { value: "Received", color: "#e6e6e6", @@ -45,6 +41,14 @@ export const STATUS_OPTIONS: StatusOption[] = [ }, ]; +/** + * A special status-like option for all statuses + */ +export const ALL_STATUSES_OPTION: StatusOption = { + value: "All Statuses", + color: "transparent", +}; + /** * An input component that displays a dropdown menu with all available status * options and enables the user to select a status. @@ -52,9 +56,10 @@ export const STATUS_OPTIONS: StatusOption[] = [ export interface StatusDropdownProps { value: string; onChanged?: (value: string) => void; + includeAllStatuses: boolean; } -export function StatusDropdown({ value, onChanged }: StatusDropdownProps) { +export function StatusDropdown({ value, onChanged, includeAllStatuses }: StatusDropdownProps) { const [selectedValue, setSelectedValue] = useState(value); const [isOpen, setIsOpen] = useState(false); @@ -136,11 +141,13 @@ export function StatusDropdown({ value, onChanged }: StatusDropdownProps) { }} IconComponent={DropdownIcon} > - {...STATUS_OPTIONS.map((status) => ( - - - - ))} + {...(includeAllStatuses ? [ALL_STATUSES_OPTION] : []) + .concat(STATUS_OPTIONS) + .map((status) => ( + + + + ))}
From 0c767f19165e1252f9d2fc1e9588b1419411a021 Mon Sep 17 00:00:00 2001 From: benjaminjohnson2204 Date: Mon, 27 May 2024 16:16:15 -0700 Subject: [PATCH 51/74] Apply filters to VSR bulk exporting --- backend/src/controllers/vsr.ts | 86 ++++++----------------------- backend/src/services/vsrs.ts | 65 ++++++++++++++++++++++ frontend/src/api/VSRs.ts | 44 +++++++++++---- frontend/src/app/staff/vsr/page.tsx | 9 ++- 4 files changed, 123 insertions(+), 81 deletions(-) create mode 100644 backend/src/services/vsrs.ts diff --git a/backend/src/controllers/vsr.ts b/backend/src/controllers/vsr.ts index f9ed160..36e6ab3 100644 --- a/backend/src/controllers/vsr.ts +++ b/backend/src/controllers/vsr.ts @@ -7,6 +7,7 @@ import { sendVSRConfirmationEmailToVeteran, sendVSRNotificationEmailToStaff, } from "src/services/emails"; +import { retrieveVSRs } from "src/services/vsrs"; import validationErrorParser from "src/util/validationErrorParser"; import ExcelJS from "exceljs"; import { ObjectId } from "mongodb"; @@ -19,56 +20,13 @@ type FurnitureItemEntry = FurnitureItem & { _id: ObjectId }; */ export const getAllVSRS: RequestHandler = async (req, res, next) => { try { - let vsrs = await VSRModel.aggregate([ - ...(req.query.search - ? [ - { - $match: { name: { $regex: new RegExp(req.query.search as string) } }, - }, - ] - : []), - { - $addFields: { - statusOrder: { - $switch: { - branches: [ - { case: { $eq: ["$status", "Received"] }, then: 1 }, - { case: { $eq: ["$status", "Approved"] }, then: 2 }, - { case: { $eq: ["$status", "Appointment Scheduled"] }, then: 3 }, - { case: { $eq: ["$status", "Complete"] }, then: 4 }, - { case: { $eq: ["$status", "No-show / Incomplete"] }, then: 5 }, - { case: { $eq: ["$status", "Archived"] }, then: 6 }, - ], - default: 99, - }, - }, - }, - }, - { $sort: { statusOrder: 1, dateReceived: -1 } }, - ]); - - if (req.query.status) { - vsrs = vsrs.filter((vsr) => vsr.status === req.query.status); - } - - if (req.query.incomeLevel) { - const incomeMap: { [key: string]: string } = { - "50000": "$50,001 and over", - "25000": "$25,001 - $50,000", - "12500": "$12,501 - $25,000", - "0": "$12,500 and under", - }; - - vsrs = vsrs.filter((vsr) => { - return vsr.incomeLevel === incomeMap[req.query.incomeLevel as string]; - }); - } - - if (req.query.zipCode) { - //we expect a list of zipcodes - const zipCodes = (req.query.zipCode as string).split(",").map((zip) => zip.trim()); - vsrs = vsrs.filter((vsr) => zipCodes.includes(vsr.zipCode.toString())); - } + const vsrs = await retrieveVSRs( + req.query.search as string | undefined, + req.query.status as string | undefined, + req.query.incomeLevel as string | undefined, + req.query.zipCode ? (req.query.zipCode as string).split(",") : undefined, + undefined, + ); res.status(200).json({ vsrs }); } catch (error) { @@ -338,32 +296,20 @@ const writeSpreadsheet = async (plainVsrs: VSR[], res: Response) => { export const bulkExportVSRS: RequestHandler = async (req, res, next) => { try { - const filename = "vsrs.xlsx"; + const filename = `vsrs_${new Date().toISOString()}.xlsx`; // Set some headers on the response so the client knows that a file is attached res.set({ "Content-Disposition": `attachment; filename="${filename}"`, "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); - let vsrs: VSR[]; - - if (req.query.vsrIds && ((req.query.vsrIds.length ?? 0) as number) > 0) { - // If the "vsrIds" query parameter exists and is non-empty, then find & export all VSRs - // with an _id in the vsrIds list - - // Need to convert each ID string to an ObjectId object - const vsrObjectIds = (req.query.vsrIds as string)?.split(",").map((_id) => new ObjectId(_id)); - vsrs = ( - await VSRModel.find({ - _id: { - $in: vsrObjectIds, - }, - }) - ).map((doc) => doc.toObject()); - } else { - // If the "vsrIds" query parameter is not provided or is empty, export all VSRs in the database - vsrs = (await VSRModel.find()).map((doc) => doc.toObject()); - } + const vsrs = await retrieveVSRs( + req.query.search as string | undefined, + req.query.status as string | undefined, + req.query.incomeLevel as string | undefined, + req.query.zipCode ? (req.query.zipCode as string).split(",") : undefined, + req.query.vsrIds ? (req.query.vsrIds as string).split(",") : undefined, + ); await writeSpreadsheet(vsrs, res); } catch (error) { diff --git a/backend/src/services/vsrs.ts b/backend/src/services/vsrs.ts new file mode 100644 index 0000000..583a4d5 --- /dev/null +++ b/backend/src/services/vsrs.ts @@ -0,0 +1,65 @@ +import VSRModel from "src/models/vsr"; + +export const retrieveVSRs = async ( + search: string | undefined, + status: string | undefined, + incomeLevel: string | undefined, + zipCodes: string[] | undefined, + vsrIds: string[] | undefined, +) => { + let vsrs = await VSRModel.aggregate([ + ...(search + ? [ + { + $match: { name: { $regex: new RegExp(search) } }, + }, + ] + : []), + { + $addFields: { + statusOrder: { + $switch: { + branches: [ + { case: { $eq: ["$status", "Received"] }, then: 1 }, + { case: { $eq: ["$status", "Approved"] }, then: 2 }, + { case: { $eq: ["$status", "Appointment Scheduled"] }, then: 3 }, + { case: { $eq: ["$status", "Complete"] }, then: 4 }, + { case: { $eq: ["$status", "No-show / Incomplete"] }, then: 5 }, + { case: { $eq: ["$status", "Archived"] }, then: 6 }, + ], + default: 99, + }, + }, + }, + }, + { $sort: { statusOrder: 1, dateReceived: -1 } }, + ]); + + if (vsrIds && vsrIds.length > 0) { + const vsrIdsSet = new Set(vsrIds); + vsrs = vsrs.filter((vsr) => vsrIdsSet.has(vsr._id.toString())); + } + + if (status) { + vsrs = vsrs.filter((vsr) => vsr.status === status); + } + + if (incomeLevel) { + const incomeMap: { [key: string]: string } = { + "50000": "$50,001 and over", + "25000": "$25,001 - $50,000", + "12500": "$12,501 - $25,000", + "0": "$12,500 and under", + }; + + vsrs = vsrs.filter((vsr) => { + return vsr.incomeLevel === incomeMap[incomeLevel]; + }); + } + + if (zipCodes && zipCodes.length > 0) { + vsrs = vsrs.filter((vsr) => zipCodes.includes(vsr.zipCode.toString())); + } + + return vsrs; +}; diff --git a/frontend/src/api/VSRs.ts b/frontend/src/api/VSRs.ts index ccb130a..b2c9cdc 100644 --- a/frontend/src/api/VSRs.ts +++ b/frontend/src/api/VSRs.ts @@ -156,6 +156,13 @@ export async function createVSR(vsr: CreateVSRRequest): Promise> } } +const incomeMap: { [key: string]: string } = { + "$50,001 and over": "50000", + "$25,001 - $50,000": "25000", + "$12,501 - $25,000": "12500", + "$12,500 and under": "0", +}; + export async function getAllVSRs( firebaseToken: string, search?: string, @@ -163,13 +170,6 @@ export async function getAllVSRs( income?: string, status?: string, ): Promise> { - const incomeMap: { [key: string]: string } = { - "$50,001 and over": "50000", - "$25,001 - $50,000": "25000", - "$12,501 - $25,000": "12500", - "$12,500 and under": "0", - }; - const searchParams = new URLSearchParams(); if (search) { searchParams.set("search", search); @@ -250,17 +250,41 @@ export async function updateVSR( export async function bulkExportVSRS( firebaseToken: string, vsrIds: string[], + search?: string, + zipCodes?: string[], + income?: string, + status?: string, ): Promise> { + const searchParams = new URLSearchParams(); + if (search) { + searchParams.set("search", search); + } + if (zipCodes) { + searchParams.set("zipCode", zipCodes.join(", ")); + } + if (income) { + searchParams.set("incomeLevel", incomeMap[income]); + } + if (status) { + searchParams.set("status", status); + } + + if (vsrIds && vsrIds.length > 0) { + searchParams.set("vsrIds", vsrIds.join(",")); + } + + const searchParamsString = searchParams.toString(); + const urlString = `/api/vsr/bulk_export${searchParamsString ? "?" + searchParamsString : ""}`; + try { - const query = vsrIds.length === 0 ? "" : `?vsrIds=${vsrIds.join(",")}`; - const response = await get(`/api/vsr/bulk_export${query}`, createAuthHeader(firebaseToken)); + const response = await get(urlString, createAuthHeader(firebaseToken)); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; - link.setAttribute("download", "vsrs.xlsx"); + link.setAttribute("download", `vsrs_${new Date().toISOString()}.xlsx`); document.body.appendChild(link); link.click(); diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index 09e4d82..e5126f9 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -243,7 +243,14 @@ export default function VSRTableView() { setExportError(VSRExportError.NONE); setLoadingExport(true); firebaseUser?.getIdToken().then((firebaseToken) => { - bulkExportVSRS(firebaseToken, selectedVsrIds).then((result) => { + bulkExportVSRS( + firebaseToken, + selectedVsrIds, + search, + filteredZipCodes, + filteredIncome, + status, + ).then((result) => { if (result.success) { setExportSuccess(true); } else { From 795ef9455301ace2fd0153c5f5eb5249181af64b Mon Sep 17 00:00:00 2001 From: benjaminjohnson2204 Date: Mon, 27 May 2024 16:18:39 -0700 Subject: [PATCH 52/74] Remove unused zip code invalid error code --- frontend/src/app/staff/vsr/page.tsx | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index e5126f9..75cb8bd 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -24,7 +24,6 @@ import styles from "@/app/staff/vsr/page.module.css"; enum VSRTableError { CANNOT_FETCH_VSRS_NO_INTERNET, CANNOT_FETCH_VSRS_INTERNAL, - ZIPCODE_INVALID, NONE, } @@ -149,30 +148,6 @@ export default function VSRTableView() { }} /> ); - case VSRTableError.ZIPCODE_INVALID: - return ( - { - setTableError(VSRTableError.NONE); - }} - imageComponent={ - Internal Error - } - title="Zip Code Formatting Error" - content="Each zip code must be exactly 5 digits long. Please correct the zip code and try again." - buttonText="Try Again" - onButtonClicked={() => { - setTableError(VSRTableError.NONE); - fetchVSRs(); - }} - /> - ); default: return null; } From 7e945932ba1ada6c51b5017659352445ee63b632 Mon Sep 17 00:00:00 2001 From: benjaminjohnson2204 Date: Mon, 27 May 2024 16:33:28 -0700 Subject: [PATCH 53/74] Remove unused files --- frontend/.env.frontend.development | 10 -- .../shared/DeleteVSRsModal/index.tsx | 116 ------------------ package-lock.json | 6 - package.json | 5 - 4 files changed, 137 deletions(-) delete mode 100644 frontend/.env.frontend.development delete mode 100644 frontend/src/components/shared/DeleteVSRsModal/index.tsx delete mode 100644 package-lock.json delete mode 100644 package.json diff --git a/frontend/.env.frontend.development b/frontend/.env.frontend.development deleted file mode 100644 index 3a9e20b..0000000 --- a/frontend/.env.frontend.development +++ /dev/null @@ -1,10 +0,0 @@ -NEXT_PUBLIC_BACKEND_URL="http://localhost:3001" -NEXT_PUBLIC_FIREBASE_SETTINGS='{ - "apiKey": "AIzaSyAN8IA2AQnmX-tL8x2oQKUcz1TG9KNxiSA", - "authDomain": "tse-pap-dev.firebaseapp.com", - "projectId": "tse-pap-dev", - "storageBucket": "tse-pap-dev.appspot.com", - "messagingSenderId": "769700838421", - "appId": "1:769700838421:web:3049e07fd98a27c77ee21c", - "measurementId": "G-EEL71R3Q0F" -}' \ No newline at end of file diff --git a/frontend/src/components/shared/DeleteVSRsModal/index.tsx b/frontend/src/components/shared/DeleteVSRsModal/index.tsx deleted file mode 100644 index 0c8d335..0000000 --- a/frontend/src/components/shared/DeleteVSRsModal/index.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { deleteVSR } from "@/api/VSRs"; -import { UserContext } from "@/contexts/userContext"; -import { useContext, useState } from "react"; -import { NotificationBanner } from "@/components/shared/NotificationBanner"; -import { BaseModal } from "@/components/shared/BaseModal"; -import { Button } from "@/components/shared/Button"; -import styles from "@/components/shared/DeleteVSRsModal/styles.module.css"; - -interface DeleteVSRsModalProps { - isOpen: boolean; - onClose: () => unknown; - afterDelete: () => unknown; - vsrIds: string[]; -} - -/** - * A modal that asks the user to confirm whether they want to delete one or more - * VSRs, and then deletes them if they confirm that they want to. - */ -export const DeleteVSRsModal = ({ isOpen, onClose, afterDelete, vsrIds }: DeleteVSRsModalProps) => { - const { firebaseUser } = useContext(UserContext); - - const [loadingDelete, setLoadingDelete] = useState(false); - const [successNotificationOpen, setSuccessNotificationOpen] = useState(false); - const [errorNotificationOpen, setErrorNotificationOpen] = useState(false); - - const onDelete = async () => { - if (loadingDelete || !firebaseUser) { - return; - } - - setSuccessNotificationOpen(false); - setErrorNotificationOpen(false); - setLoadingDelete(true); - - try { - const firebaseToken = await firebaseUser.getIdToken(); - if (!firebaseToken) { - setLoadingDelete(false); - return; - } - - await Promise.all( - vsrIds.map((vsrId) => - deleteVSR(vsrId, firebaseToken).then((res) => { - if (res.success) { - return Promise.resolve(); - } else { - return Promise.reject(res.error); - } - }), - ), - ); - - setSuccessNotificationOpen(true); - afterDelete(); - } catch (error) { - console.error(`Error deleting VSR(s): ${error}`); - setErrorNotificationOpen(true); - } finally { - setLoadingDelete(false); - onClose(); - } - }; - - return ( - <> - - {"Deleted VSR’s "} - cannot - {" be recovered. Are you sure you’d like to delete the selected VSR forms ("} - {vsrIds.length} - {")?"} - - } - bottomRow={ -
-
- } - /> - setSuccessNotificationOpen(false)} - /> - setErrorNotificationOpen(false)} - /> - - ); -}; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 1c31bfb..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "PAP-Inventory-Processing", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/package.json b/package.json deleted file mode 100644 index a802f87..0000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "email-validator": "^2.0.4" - } -} From 8131cc4a5fb8ba4bfc4abf0d78206feefe103876 Mon Sep 17 00:00:00 2001 From: benjaminjohnson2204 Date: Mon, 27 May 2024 16:41:46 -0700 Subject: [PATCH 54/74] Save edits from VSR individual page directly rather than showing confirmation popup --- .../VSRIndividual/VSRIndividualPage/index.tsx | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/frontend/src/components/VSRIndividual/VSRIndividualPage/index.tsx b/frontend/src/components/VSRIndividual/VSRIndividualPage/index.tsx index e98a58d..56ba87e 100644 --- a/frontend/src/components/VSRIndividual/VSRIndividualPage/index.tsx +++ b/frontend/src/components/VSRIndividual/VSRIndividualPage/index.tsx @@ -68,7 +68,6 @@ export const VSRIndividualPage = () => { const [loadingUpdateStatus, setLoadingUpdateStatus] = useState(false); const [discardEditsConfirmationModalOpen, setDiscardEditsConfirmationModalOpen] = useState(false); - const [saveEditsConfirmationModalOpen, setSaveEditsConfirmationModalOpen] = useState(false); const [editSuccessNotificationOpen, setEditSuccessNotificationOpen] = useState(false); const [editErrorNotificationOpen, setEditErrorNotificationOpen] = useState(false); const [loadingEdit, setLoadingEdit] = useState(false); @@ -321,7 +320,7 @@ export const VSRIndividualPage = () => { iconAlt="Check" text="Save Changes" hideTextOnMobile - onClick={() => setSaveEditsConfirmationModalOpen(true)} + onClick={(e) => handleSubmit(onSubmitEdits)(e)} /> ) : ( @@ -694,36 +693,6 @@ export const VSRIndividualPage = () => { /> {/* Modals & notifications for saving changes to VSR */} - setSaveEditsConfirmationModalOpen(false)} - title="Save Changes" - content="Would you like to save your changes?" - bottomRow={ -
-
- } - /> Date: Mon, 27 May 2024 16:51:08 -0700 Subject: [PATCH 55/74] Fix linter issues --- .../src/app/staff/furnitureItems/page.tsx | 47 ++++--------------- frontend/src/app/staff/vsr/page.tsx | 26 +++++++--- .../FurnitureRequest/EditTemplate/index.tsx | 18 ++----- 3 files changed, 31 insertions(+), 60 deletions(-) diff --git a/frontend/src/app/staff/furnitureItems/page.tsx b/frontend/src/app/staff/furnitureItems/page.tsx index 2b9081a..3a22569 100644 --- a/frontend/src/app/staff/furnitureItems/page.tsx +++ b/frontend/src/app/staff/furnitureItems/page.tsx @@ -1,21 +1,18 @@ "use client"; import HeaderBar from "@/components/shared/HeaderBar"; -import styles from "src/app/staff/furnitureItems/page.module.css"; import { EditTemplate } from "@/components/FurnitureRequest/EditTemplate"; import { useMemo } from "react"; -import { - FurnitureItem, - getFurnitureItems, - addFurnitureItem, - updateFurnitureItem, - deleteFurnitureItem, -} from "@/api/FurnitureItems"; +import { FurnitureItem, getFurnitureItems } from "@/api/FurnitureItems"; import React, { useEffect, useState } from "react"; -import { ICreateVSRFormInput } from "@/components/VSRForm/VSRFormTypes"; +import { useRedirectToLoginIfNotSignedIn } from "@/hooks/useRedirection"; +import styles from "@/app/staff/furnitureItems/page.module.css"; export default function furnitureItemTemplate() { const [furnitureItems, setFurnitureItems] = useState(); const [editingCategory, setEditingCategory] = useState(); + + useRedirectToLoginIfNotSignedIn(); + useEffect(() => { fetchFurnitureItems(); }, []); @@ -58,8 +55,8 @@ export default function furnitureItemTemplate() {

Furnishing Request Form Template

- Adding, editing, and removing tags. Remember to save your edits after adding or removing - furnishing options for future VSR forms. + Add, edit, or remove furniture items for veterans to select on the VSR. Remember to save + your edits.

@@ -79,37 +76,9 @@ export default function furnitureItemTemplate() { /> )) : null} - - {/* - - - - */}
); } - -// add more furniture items, such as bedroom diff --git a/frontend/src/app/staff/vsr/page.tsx b/frontend/src/app/staff/vsr/page.tsx index fd74249..4ecbb16 100644 --- a/frontend/src/app/staff/vsr/page.tsx +++ b/frontend/src/app/staff/vsr/page.tsx @@ -51,8 +51,8 @@ export default function VSRTableView() { const [selectedVsrIds, setSelectedVsrIds] = useState([]); const [deleteVsrModalOpen, setDeleteVsrModalOpen] = useState(false); const [loadingDelete, setLoadingDelete] = useState(false); - const [successNotificationOpen, setSuccessNotificationOpen] = useState(false); - const [errorNotificationOpen, setErrorNotificationOpen] = useState(false); + const [deleteSuccessNotificationOpen, setDeleteSuccessNotificationOpen] = useState(false); + const [deleteErrorNotificationOpen, setDeleteErrorNotificationOpen] = useState(false); const [filterModalAnchorElement, setFilterModalAnchorElement] = useState( null, @@ -102,8 +102,8 @@ export default function VSRTableView() { return; } - setSuccessNotificationOpen(false); - setErrorNotificationOpen(false); + setDeleteSuccessNotificationOpen(false); + setDeleteErrorNotificationOpen(false); setLoadingDelete(true); try { @@ -124,12 +124,12 @@ export default function VSRTableView() { }), ), ); - setSuccessNotificationOpen(true); + setDeleteSuccessNotificationOpen(true); setSelectedVsrIds([]); fetchVSRs(); } catch (error) { console.error(`Error deleting VSR(s): ${error}`); - setErrorNotificationOpen(true); + setDeleteErrorNotificationOpen(true); } finally { setLoadingDelete(false); setDeleteVsrModalOpen(false); @@ -415,6 +415,20 @@ export default function VSRTableView() { onConfirm={onDelete} buttonLoading={loadingDelete} /> + + setDeleteSuccessNotificationOpen(false)} + /> + setDeleteErrorNotificationOpen(false)} + /> { diff --git a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx index c0a8bb2..1b1d3f3 100644 --- a/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx +++ b/frontend/src/components/FurnitureRequest/EditTemplate/index.tsx @@ -2,7 +2,6 @@ import styles from "@/components/FurnitureRequest/EditTemplate/styles.module.css import { useContext, useState } from "react"; import { FurnitureItem, - getFurnitureItems, addFurnitureItem, updateFurnitureItem, deleteFurnitureItem, @@ -41,7 +40,7 @@ export const EditTemplate = ({ const [isAddingNewItem, setIsAddingNewItem] = useState(false); const [editingItemId, setEditingItemId] = useState(null); const [itemName, setItemName] = useState(""); - const { firebaseUser, papUser } = useContext(UserContext); + const { firebaseUser } = useContext(UserContext); const [allowMultiple, setAllowMultiple] = useState(false); const [confirmDeleteModal, setConfirmDeleteModal] = useState(false); @@ -61,10 +60,6 @@ export const EditTemplate = ({ setAllowMultiple(furnitureItem?.allowMultiple ?? false); }; - const handleStopEditItem = () => { - setEditingItemId(null); - }; - const onDelete = async () => { const firebaseToken = await firebaseUser?.getIdToken(); if (!firebaseToken || editingItemId === null) { @@ -173,7 +168,7 @@ export const EditTemplate = ({ control={ setAllowMultiple(checked)} + onChange={(_, checked) => setAllowMultiple(checked)} /> } /> @@ -187,7 +182,7 @@ export const EditTemplate = ({ */} - - - - - - - - - ) : ( - <> -

{title}

{content}

-
{bottomRow}
+ {bottomRow ?
{bottomRow}
: null} diff --git a/frontend/src/components/shared/ConfirmDeleteModal/index.tsx b/frontend/src/components/shared/ConfirmDeleteModal/index.tsx index 2d1c814..c7040a7 100644 --- a/frontend/src/components/shared/ConfirmDeleteModal/index.tsx +++ b/frontend/src/components/shared/ConfirmDeleteModal/index.tsx @@ -29,32 +29,30 @@ export const ConfirmDeleteModal = ({ onConfirm, }: ConfirmDeleteModalProps) => { return ( - <> - -