From b4e17f994e1b8d87d8ad61ba835330c11fb30bca Mon Sep 17 00:00:00 2001 From: Jonathan Kim <113380851+jkim-21@users.noreply.github.com> Date: Mon, 7 Apr 2025 13:46:34 -0500 Subject: [PATCH 1/7] Implement user type frontend page --- .../Admin/UserTypesPage/UserTypesPage.tsx | 77 +++++++++++++++++++ frontend/src/routes/appRoutes.tsx | 2 + 2 files changed, 79 insertions(+) create mode 100644 frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx diff --git a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx new file mode 100644 index 0000000..0b11c79 --- /dev/null +++ b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx @@ -0,0 +1,77 @@ +// frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx + +import React, { useEffect, useState } from "react"; +import apiClient from "../../../services/apiClient"; +import { Pencil, Trash2 } from "lucide-react"; + +interface UserType { + _id: string; + name: string; + userCount: number; +} + +export default function UserTypesPage() { + const [userTypes, setUserTypes] = useState([]); + + useEffect(() => { + const fetchUserTypes = async () => { + try { + const res = await apiClient.get("/user-types"); + setUserTypes(res.data); + } catch (err) { + console.error("Error fetching user types:", err); + } + }; + + fetchUserTypes(); + }, []); + + return ( +
+
+
+

User Types

+ +
+ + + + + + + + + + + {userTypes.length === 0 ? ( + + + + ) : ( + userTypes.map((ut) => ( + + + + + + )) + )} + +
TypeNumber of UsersActions
+ No user types found. +
{ut.name}{ut.userCount} + + +
+
+
+ ); +} diff --git a/frontend/src/routes/appRoutes.tsx b/frontend/src/routes/appRoutes.tsx index 0bc9563..a0bd57e 100644 --- a/frontend/src/routes/appRoutes.tsx +++ b/frontend/src/routes/appRoutes.tsx @@ -38,6 +38,7 @@ import Registrants from "../pages/Admin/NewProductPage/Registrants"; import SurveySummary from "../pages/Admin/SurveySummaryPage/SurveySummary"; import CourseManagerPage from "../pages/Admin/CourseManagerPage/CourseManagerPage"; import UserManagementPage from "../pages/Admin/UserManagementPage/Users"; +import UserTypesPage from "../pages/Admin/UserTypesPage/UserTypesPage"; // import AdminPage from "../pages/Admin/AdminPage"; function AppRoutes() { @@ -202,6 +203,7 @@ function AppRoutes() { } /> } /> } /> + } /> } /> Date: Mon, 7 Apr 2025 13:52:50 -0500 Subject: [PATCH 2/7] Format table to be similar to all users page --- frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx index 0b11c79..310a06b 100644 --- a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx +++ b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx @@ -27,11 +27,11 @@ export default function UserTypesPage() { }, []); return ( -
-
+
+

User Types

-
From 95d23ffeec0c84dbcaad265c3176a5890af77b66 Mon Sep 17 00:00:00 2001 From: Jonathan Kim <113380851+jkim-21@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:33:11 -0500 Subject: [PATCH 3/7] Create user type model and controller --- backend/app.ts | 2 ++ backend/config/cloudinaryStorage.ts | 2 -- backend/controllers/userTypeController.ts | 30 +++++++++++++++++++++++ backend/models/userModel.ts | 12 --------- backend/models/userTypeModel.ts | 16 ++++++++++++ backend/routes/userRoutes.ts | 1 + backend/routes/userTypeRoutes.ts | 12 +++++++++ 7 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 backend/controllers/userTypeController.ts create mode 100644 backend/models/userTypeModel.ts create mode 100644 backend/routes/userTypeRoutes.ts diff --git a/backend/app.ts b/backend/app.ts index 132f85b..2c93010 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -21,6 +21,7 @@ import handoutRoutes from "./routes/handoutRoutes"; import courseCategoriesRoutes from "./routes/courseCategoriesRoutes" import emailRoutes from './routes/emailRoutes'; import speakerRoutes from "./routes/speakerRoutes"; +import userTypeRoutes from "./routes/userTypeRoutes"; // Import middleware import { notFound, errorHandler } from "./middlewares/errorMiddleware"; @@ -89,6 +90,7 @@ app.use("/api/handout", verifyFirebaseAuth, handoutRoutes); app.use('/api/emails', verifyFirebaseAuth, emailRoutes); app.use("/api/speakers", verifyFirebaseAuth, speakerRoutes); app.use("/api/upload", verifyFirebaseAuth, uploadRoutes); +app.use("/api/user-types", userTypeRoutes); // Error middleware app.use((err: any, req: Request, res: Response, next: NextFunction): void => { diff --git a/backend/config/cloudinaryStorage.ts b/backend/config/cloudinaryStorage.ts index 8d792c1..1aa992e 100644 --- a/backend/config/cloudinaryStorage.ts +++ b/backend/config/cloudinaryStorage.ts @@ -4,8 +4,6 @@ import cloudinary from "./cloudinary"; import { Request } from "express"; import { Options } from "multer-storage-cloudinary"; -console.log("cloundaryStorage"); - // Explicit type to override the broken one interface CustomParams { folder: string; diff --git a/backend/controllers/userTypeController.ts b/backend/controllers/userTypeController.ts new file mode 100644 index 0000000..ee27dd6 --- /dev/null +++ b/backend/controllers/userTypeController.ts @@ -0,0 +1,30 @@ +// backend/controllers/userTypeController.ts +import { Request, Response } from "express"; +import UserType from "../models/userTypeModel"; + +export const createUserType = async (req: Request, res: Response) => { + try { + const { name } = req.body; + + if (!name || ["foster parent", "staff"].includes(name.toLowerCase())) { + return res.status(400).json({ message: "This role is reserved or invalid." }); + } + + const existing = await UserType.findOne({ name }); + if (existing) return res.status(409).json({ message: "Role already exists." }); + + const newType = await UserType.create({ name }); + res.status(201).json({ success: true, data: newType }); + } catch (err) { + res.status(500).json({ message: "Server error" }); + } +}; + +export const getUserTypes = async (_req: Request, res: Response) => { + try { + const types = await UserType.find().sort({ name: 1 }); + res.status(200).json({ success: true, data: types }); + } catch { + res.status(500).json({ message: "Server error" }); + } +}; \ No newline at end of file diff --git a/backend/models/userModel.ts b/backend/models/userModel.ts index c801c0f..8d2adc9 100644 --- a/backend/models/userModel.ts +++ b/backend/models/userModel.ts @@ -38,18 +38,6 @@ const userSchema: Schema = new Schema( isColorado: { type: Boolean, required: true }, role: { type: String, - enum: [ - "foster parent", - "certified kin", - "non-certified kin", - "staff", - "casa", - "teacher", - "county/cpa worker", - "speaker", - "former parent", - "caregiver", - ], required: true, }, name: { type: String, required: true }, diff --git a/backend/models/userTypeModel.ts b/backend/models/userTypeModel.ts new file mode 100644 index 0000000..21516e1 --- /dev/null +++ b/backend/models/userTypeModel.ts @@ -0,0 +1,16 @@ +// backend/models/userTypeModel.ts +import mongoose, { Schema, Document, Model } from "mongoose"; + +export interface IUserType extends Document { + name: string; +} + +const userTypeSchema: Schema = new Schema( + { + name: { type: String, required: true, unique: true }, + }, + { timestamps: true } +); + +const UserType: Model = mongoose.model("UserType", userTypeSchema); +export default UserType; \ No newline at end of file diff --git a/backend/routes/userRoutes.ts b/backend/routes/userRoutes.ts index 11329f5..6d016e9 100644 --- a/backend/routes/userRoutes.ts +++ b/backend/routes/userRoutes.ts @@ -7,6 +7,7 @@ import { register, checkAdmin, } from "../controllers/userController"; +import { createUserType, getUserTypes } from "../controllers/userTypeController"; import { verifyFirebaseAuth } from "../middlewares/authMiddleware"; const router = express.Router(); diff --git a/backend/routes/userTypeRoutes.ts b/backend/routes/userTypeRoutes.ts new file mode 100644 index 0000000..91a30d5 --- /dev/null +++ b/backend/routes/userTypeRoutes.ts @@ -0,0 +1,12 @@ +import express from "express"; +import { + createUserType, + getUserTypes, +} from "../controllers/userTypeController"; + +const router = express.Router(); + +router.post("/", createUserType); +router.get("/", getUserTypes); + +export default router; \ No newline at end of file From 8b748d3785c54e333d34fa55b0dcb629ca40eadb Mon Sep 17 00:00:00 2001 From: Jonathan Kim <113380851+jkim-21@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:56:38 -0500 Subject: [PATCH 4/7] Connect the usertypes dynamic roles with user types page --- backend/controllers/userTypeController.ts | 22 +++-- backend/routes/userRoutes.ts | 1 - .../Admin/UserTypesPage/UserTypesPage.tsx | 83 ++++++++++++++++--- frontend/src/services/userTypeService.ts | 11 +++ 4 files changed, 94 insertions(+), 23 deletions(-) create mode 100644 frontend/src/services/userTypeService.ts diff --git a/backend/controllers/userTypeController.ts b/backend/controllers/userTypeController.ts index ee27dd6..7299569 100644 --- a/backend/controllers/userTypeController.ts +++ b/backend/controllers/userTypeController.ts @@ -1,30 +1,34 @@ -// backend/controllers/userTypeController.ts -import { Request, Response } from "express"; +import { Request, Response, NextFunction, RequestHandler } from "express"; import UserType from "../models/userTypeModel"; -export const createUserType = async (req: Request, res: Response) => { +// Fix: Don't return the Response object from the controller function +export const createUserType: RequestHandler = async (req, res, next) => { try { const { name } = req.body; if (!name || ["foster parent", "staff"].includes(name.toLowerCase())) { - return res.status(400).json({ message: "This role is reserved or invalid." }); + res.status(400).json({ message: "This role is reserved or invalid." }); + return; } const existing = await UserType.findOne({ name }); - if (existing) return res.status(409).json({ message: "Role already exists." }); + if (existing) { + res.status(409).json({ message: "Role already exists." }); + return; + } const newType = await UserType.create({ name }); res.status(201).json({ success: true, data: newType }); } catch (err) { - res.status(500).json({ message: "Server error" }); + next(err); // Pass errors to Express error handler } }; -export const getUserTypes = async (_req: Request, res: Response) => { +export const getUserTypes: RequestHandler = async (_req, res, next) => { try { const types = await UserType.find().sort({ name: 1 }); res.status(200).json({ success: true, data: types }); - } catch { - res.status(500).json({ message: "Server error" }); + } catch (err) { + next(err); // Pass errors to Express error handler } }; \ No newline at end of file diff --git a/backend/routes/userRoutes.ts b/backend/routes/userRoutes.ts index 6d016e9..11329f5 100644 --- a/backend/routes/userRoutes.ts +++ b/backend/routes/userRoutes.ts @@ -7,7 +7,6 @@ import { register, checkAdmin, } from "../controllers/userController"; -import { createUserType, getUserTypes } from "../controllers/userTypeController"; import { verifyFirebaseAuth } from "../middlewares/authMiddleware"; const router = express.Router(); diff --git a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx index 310a06b..8e62745 100644 --- a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx +++ b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx @@ -7,31 +7,55 @@ import { Pencil, Trash2 } from "lucide-react"; interface UserType { _id: string; name: string; - userCount: number; + userCount?: number; } export default function UserTypesPage() { const [userTypes, setUserTypes] = useState([]); + const [showModal, setShowModal] = useState(false); + const [newTypeName, setNewTypeName] = useState(""); + const [error, setError] = useState(""); useEffect(() => { - const fetchUserTypes = async () => { - try { - const res = await apiClient.get("/user-types"); - setUserTypes(res.data); - } catch (err) { - console.error("Error fetching user types:", err); - } - }; - fetchUserTypes(); }, []); + const fetchUserTypes = async () => { + try { + const res = await apiClient.get("/user-types"); + setUserTypes(res.data.data); + } catch (err) { + console.error("Error fetching user types:", err); + } + }; + + const handleCreateUserType = async () => { + if (!newTypeName.trim()) { + setError("Type name is required."); + return; + } + + try { + await apiClient.post("/user-types", { name: newTypeName.trim() }); + setNewTypeName(""); + setShowModal(false); + fetchUserTypes(); + } catch (err: any) { + setError( + err?.response?.data?.message || "Error creating user type." + ); + } + }; + return (

User Types

-
@@ -61,7 +85,7 @@ export default function UserTypesPage() { className="border-t border-gray-200 text-sm hover:bg-gray-50" > {ut.name} - {ut.userCount} + {ut.userCount ?? 0} @@ -72,6 +96,39 @@ export default function UserTypesPage() {
+ + {showModal && ( +
+
+

Create New User Type

+ setNewTypeName(e.target.value)} + /> + {error &&

{error}

} +
+ + +
+
+
+ )}
); -} +} \ No newline at end of file diff --git a/frontend/src/services/userTypeService.ts b/frontend/src/services/userTypeService.ts new file mode 100644 index 0000000..dd0c6de --- /dev/null +++ b/frontend/src/services/userTypeService.ts @@ -0,0 +1,11 @@ +import apiClient from "./apiClient"; + +export const fetchUserTypes = async () => { + const res = await apiClient.get("/user-types"); + return res.data.data; // assuming the shape is { success: true, data: [...] } +}; + +export const createUserType = async (name: string) => { + const res = await apiClient.post("/user-types", { name }); + return res.data; +}; \ No newline at end of file From 508274c506cc31a18b29ca041976bb5ea426578b Mon Sep 17 00:00:00 2001 From: Jonathan Kim <113380851+jkim-21@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:11:28 -0500 Subject: [PATCH 5/7] Provide delete and update functionality --- backend/controllers/userTypeController.ts | 31 ++++ backend/routes/userTypeRoutes.ts | 7 +- .../Admin/UserTypesPage/UserTypesPage.tsx | 175 ++++++++++++------ 3 files changed, 159 insertions(+), 54 deletions(-) diff --git a/backend/controllers/userTypeController.ts b/backend/controllers/userTypeController.ts index 7299569..9d9c802 100644 --- a/backend/controllers/userTypeController.ts +++ b/backend/controllers/userTypeController.ts @@ -31,4 +31,35 @@ export const getUserTypes: RequestHandler = async (_req, res, next) => { } catch (err) { next(err); // Pass errors to Express error handler } +}; + +export const deleteUserType: RequestHandler = async (req, res, next) => { + try { + const deleted = await UserType.findByIdAndDelete(req.params.id); + if (!deleted) { + res.status(404).json({ message: "User type not found" }); + return; + } + res.status(200).json({ success: true, message: "User type deleted" }); + } catch (err) { + next(err); + } +}; + +export const updateUserType: RequestHandler = async (req, res, next) => { + try { + const { name } = req.body; + const updated = await UserType.findByIdAndUpdate( + req.params.id, + { name }, + { new: true } + ); + if (!updated) { + res.status(404).json({ message: "User type not found" }); + return; + } + res.status(200).json({ success: true, data: updated }); + } catch (err) { + next(err); + } }; \ No newline at end of file diff --git a/backend/routes/userTypeRoutes.ts b/backend/routes/userTypeRoutes.ts index 91a30d5..d9a9f37 100644 --- a/backend/routes/userTypeRoutes.ts +++ b/backend/routes/userTypeRoutes.ts @@ -1,12 +1,17 @@ +// backend/routes/userTypeRoutes.ts import express from "express"; import { createUserType, getUserTypes, + deleteUserType, + updateUserType, } from "../controllers/userTypeController"; const router = express.Router(); router.post("/", createUserType); router.get("/", getUserTypes); +router.put("/:id", updateUserType); +router.delete("/:id", deleteUserType); -export default router; \ No newline at end of file +export default router; diff --git a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx index 8e62745..2cfe213 100644 --- a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx +++ b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx @@ -1,5 +1,3 @@ -// frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx - import React, { useEffect, useState } from "react"; import apiClient from "../../../services/apiClient"; import { Pencil, Trash2 } from "lucide-react"; @@ -7,18 +5,17 @@ import { Pencil, Trash2 } from "lucide-react"; interface UserType { _id: string; name: string; - userCount?: number; + userCount: number; } export default function UserTypesPage() { const [userTypes, setUserTypes] = useState([]); - const [showModal, setShowModal] = useState(false); - const [newTypeName, setNewTypeName] = useState(""); - const [error, setError] = useState(""); - - useEffect(() => { - fetchUserTypes(); - }, []); + const [selectedType, setSelectedType] = useState(null); + const [newName, setNewName] = useState(""); + const [showCreate, setShowCreate] = useState(false); + const [showEdit, setShowEdit] = useState(false); + const [showDelete, setShowDelete] = useState(false); + const [isDefault, setIsDefault] = useState(false); const fetchUserTypes = async () => { try { @@ -29,21 +26,41 @@ export default function UserTypesPage() { } }; - const handleCreateUserType = async () => { - if (!newTypeName.trim()) { - setError("Type name is required."); - return; + useEffect(() => { + fetchUserTypes(); + }, []); + + const handleCreate = async () => { + try { + await apiClient.post("/user-types", { name: newName }); + setShowCreate(false); + setNewName(""); + fetchUserTypes(); + } catch (err) { + console.error("Error creating user type:", err); } + }; + + const handleEdit = async () => { + try { + await apiClient.put(`/user-types/${selectedType?._id}`, { name: newName }); + setShowEdit(false); + setSelectedType(null); + setNewName(""); + fetchUserTypes(); + } catch (err) { + console.error("Error editing user type:", err); + } + }; + const handleDelete = async () => { try { - await apiClient.post("/user-types", { name: newTypeName.trim() }); - setNewTypeName(""); - setShowModal(false); + await apiClient.delete(`/user-types/${selectedType?._id}`); + setShowDelete(false); + setSelectedType(null); fetchUserTypes(); - } catch (err: any) { - setError( - err?.response?.data?.message || "Error creating user type." - ); + } catch (err) { + console.error("Error deleting user type:", err); } }; @@ -54,7 +71,7 @@ export default function UserTypesPage() {

User Types

@@ -71,24 +88,31 @@ export default function UserTypesPage() { {userTypes.length === 0 ? ( - + No user types found. ) : ( userTypes.map((ut) => ( - + {ut.name} - {ut.userCount ?? 0} + {ut.userCount} - - + { + setSelectedType(ut); + setNewName(ut.name); + setShowEdit(true); + }} + /> + { + setSelectedType(ut); + setShowDelete(true); + }} + /> )) @@ -97,33 +121,78 @@ export default function UserTypesPage() {
- {showModal && ( -
-
-

Create New User Type

+ {/* Create Modal */} + {showCreate && ( +
+
+
+

Create User Type

+ +
setNewTypeName(e.target.value)} + type="text" + className="w-full border p-2 mb-4" + value={newName} + onChange={(e) => setNewName(e.target.value)} + placeholder="Name" /> - {error &&

{error}

} -
+
+ +
+
+
+ )} + + {/* Edit Modal */} + {showEdit && ( +
+
+
+

Edit User Type

+ +
+ setNewName(e.target.value)} + placeholder="New Name" + /> +
+ + +
+
+
+ )} + + {/* Delete Confirmation Modal */} + {showDelete && ( +
+
+
+

Confirmation

+ +
+

Are you sure you want to remove this user type?

+
+
From 65fc3829d5a0a24105a08ba67ab35c73f3434526 Mon Sep 17 00:00:00 2001 From: Yifei Fang Date: Tue, 6 May 2025 16:57:49 -0500 Subject: [PATCH 6/7] added prices to user types --- backend/controllers/userTypeController.ts | 100 ++-- backend/models/userModel.ts | 24 +- backend/models/userTypeModel.ts | 19 +- .../Admin/UserTypesPage/UserTypesPage.tsx | 501 +++++++++++------- 4 files changed, 379 insertions(+), 265 deletions(-) diff --git a/backend/controllers/userTypeController.ts b/backend/controllers/userTypeController.ts index 9d9c802..1d9bbe5 100644 --- a/backend/controllers/userTypeController.ts +++ b/backend/controllers/userTypeController.ts @@ -3,63 +3,67 @@ import UserType from "../models/userTypeModel"; // Fix: Don't return the Response object from the controller function export const createUserType: RequestHandler = async (req, res, next) => { - try { - const { name } = req.body; + try { + const { name, cost } = req.body; - if (!name || ["foster parent", "staff"].includes(name.toLowerCase())) { - res.status(400).json({ message: "This role is reserved or invalid." }); - return; - } + // if ( + // !name || + // !cost || + // ["foster parent", "staff"].includes(name.toLowerCase()) + // ) { + // res.status(400).json({ message: "This role is reserved or invalid." }); + // return; + // } - const existing = await UserType.findOne({ name }); - if (existing) { - res.status(409).json({ message: "Role already exists." }); - return; - } + const existing = await UserType.findOne({ name }); + if (existing) { + res.status(409).json({ message: "Role already exists." }); + return; + } - const newType = await UserType.create({ name }); - res.status(201).json({ success: true, data: newType }); - } catch (err) { - next(err); // Pass errors to Express error handler - } + const newType = await UserType.create({ name, cost }); + res.status(201).json({ success: true, data: newType }); + } catch (err) { + next(err); // Pass errors to Express error handler + } }; export const getUserTypes: RequestHandler = async (_req, res, next) => { - try { - const types = await UserType.find().sort({ name: 1 }); - res.status(200).json({ success: true, data: types }); - } catch (err) { - next(err); // Pass errors to Express error handler - } + try { + const types = await UserType.find().sort({ name: 1 }); + res.status(200).json({ success: true, data: types }); + } catch (err) { + next(err); // Pass errors to Express error handler + } }; export const deleteUserType: RequestHandler = async (req, res, next) => { - try { - const deleted = await UserType.findByIdAndDelete(req.params.id); - if (!deleted) { - res.status(404).json({ message: "User type not found" }); - return; - } - res.status(200).json({ success: true, message: "User type deleted" }); - } catch (err) { - next(err); - } + try { + const deleted = await UserType.findByIdAndDelete(req.params.id); + if (!deleted) { + res.status(404).json({ message: "User type not found" }); + return; + } + res.status(200).json({ success: true, message: "User type deleted" }); + } catch (err) { + next(err); + } }; export const updateUserType: RequestHandler = async (req, res, next) => { - try { - const { name } = req.body; - const updated = await UserType.findByIdAndUpdate( - req.params.id, - { name }, - { new: true } - ); - if (!updated) { - res.status(404).json({ message: "User type not found" }); - return; - } - res.status(200).json({ success: true, data: updated }); - } catch (err) { - next(err); - } -}; \ No newline at end of file + try { + const { name, cost } = req.body; + const updated = await UserType.findByIdAndUpdate( + req.params.id, + { name, cost }, + { new: true } + ); + if (!updated) { + res.status(404).json({ message: "User type not found" }); + return; + } + res.status(200).json({ success: true, data: updated }); + } catch (err) { + next(err); + } +}; diff --git a/backend/models/userModel.ts b/backend/models/userModel.ts index 1a45d34..e4f67af 100644 --- a/backend/models/userModel.ts +++ b/backend/models/userModel.ts @@ -1,23 +1,13 @@ import mongoose, { Schema, Document, Model } from "mongoose"; import { IProgress } from "./progressModel"; import { IPayment } from "./paymentModel"; +import { IUserType } from "./userTypeModel"; export interface IUser extends Document { firebaseId: string; email: string; isColorado: boolean; - role: - | "foster parent" - | "certified kin" - | "non-certified kin" - | "staff" - | "casa" - | "teacher" - | "county/cpa worker" - | "speaker" - | "former parent" - | "caregiver"; - // TODO: update after user types can be created in admin + role: mongoose.Types.ObjectId | IUserType; name: string; address1: string; address2?: string; @@ -39,8 +29,8 @@ const userSchema: Schema = new Schema( email: { type: String, required: true }, isColorado: { type: Boolean, required: true }, role: { - type: String, - required: true, + type: Schema.Types.ObjectId, + ref: "UserType", }, name: { type: String, required: true }, address1: { type: String, required: true }, @@ -51,7 +41,11 @@ const userSchema: Schema = new Schema( certification: { type: String, required: true }, company: { type: String, required: true }, phone: { type: String, required: true }, - language: { type: String, enum: ["English", "Spanish"], default: "English" }, + language: { + type: String, + enum: ["English", "Spanish"], + default: "English", + }, progress: [ { type: Schema.Types.ObjectId, diff --git a/backend/models/userTypeModel.ts b/backend/models/userTypeModel.ts index 21516e1..1e75e1a 100644 --- a/backend/models/userTypeModel.ts +++ b/backend/models/userTypeModel.ts @@ -2,15 +2,20 @@ import mongoose, { Schema, Document, Model } from "mongoose"; export interface IUserType extends Document { - name: string; + name: string; + cost: number; } const userTypeSchema: Schema = new Schema( - { - name: { type: String, required: true, unique: true }, - }, - { timestamps: true } + { + name: { type: String, required: true, unique: true }, + cost: { type: Number, required: true }, + }, + { timestamps: true } ); -const UserType: Model = mongoose.model("UserType", userTypeSchema); -export default UserType; \ No newline at end of file +const UserType: Model = mongoose.model( + "UserType", + userTypeSchema +); +export default UserType; diff --git a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx index 2cfe213..70c0e2f 100644 --- a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx +++ b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx @@ -3,201 +3,312 @@ import apiClient from "../../../services/apiClient"; import { Pencil, Trash2 } from "lucide-react"; interface UserType { - _id: string; - name: string; - userCount: number; + _id: string; + name: string; + userCount: number; + cost: number; } export default function UserTypesPage() { - const [userTypes, setUserTypes] = useState([]); - const [selectedType, setSelectedType] = useState(null); - const [newName, setNewName] = useState(""); - const [showCreate, setShowCreate] = useState(false); - const [showEdit, setShowEdit] = useState(false); - const [showDelete, setShowDelete] = useState(false); - const [isDefault, setIsDefault] = useState(false); - - const fetchUserTypes = async () => { - try { - const res = await apiClient.get("/user-types"); - setUserTypes(res.data.data); - } catch (err) { - console.error("Error fetching user types:", err); - } - }; - - useEffect(() => { - fetchUserTypes(); - }, []); - - const handleCreate = async () => { - try { - await apiClient.post("/user-types", { name: newName }); - setShowCreate(false); - setNewName(""); - fetchUserTypes(); - } catch (err) { - console.error("Error creating user type:", err); - } - }; - - const handleEdit = async () => { - try { - await apiClient.put(`/user-types/${selectedType?._id}`, { name: newName }); - setShowEdit(false); - setSelectedType(null); - setNewName(""); - fetchUserTypes(); - } catch (err) { - console.error("Error editing user type:", err); - } - }; - - const handleDelete = async () => { - try { - await apiClient.delete(`/user-types/${selectedType?._id}`); - setShowDelete(false); - setSelectedType(null); - fetchUserTypes(); - } catch (err) { - console.error("Error deleting user type:", err); - } - }; - - return ( -
-
-
-

User Types

- -
- - - - - - - - - - - {userTypes.length === 0 ? ( - - - - ) : ( - userTypes.map((ut) => ( - - - - - - )) - )} - -
TypeNumber of UsersActions
- No user types found. -
{ut.name}{ut.userCount} - { - setSelectedType(ut); - setNewName(ut.name); - setShowEdit(true); - }} - /> - { - setSelectedType(ut); - setShowDelete(true); - }} - /> -
-
- - {/* Create Modal */} - {showCreate && ( -
-
-
-

Create User Type

- -
- setNewName(e.target.value)} - placeholder="Name" - /> -
- - -
-
-
- )} - - {/* Edit Modal */} - {showEdit && ( -
-
-
-

Edit User Type

- -
- setNewName(e.target.value)} - placeholder="New Name" - /> -
- - -
-
-
- )} - - {/* Delete Confirmation Modal */} - {showDelete && ( -
-
-
-

Confirmation

- -
-

Are you sure you want to remove this user type?

-
- - -
-
-
- )} -
- ); -} \ No newline at end of file + const [userTypes, setUserTypes] = useState([]); + const [selectedType, setSelectedType] = useState(null); + const [newName, setNewName] = useState(""); + const [newCost, setNewCost] = useState(""); + const [showCreate, setShowCreate] = useState(false); + const [showEdit, setShowEdit] = useState(false); + const [showDelete, setShowDelete] = useState(false); + const [isDefault, setIsDefault] = useState(false); + + const fetchUserTypes = async () => { + try { + const res = await apiClient.get("/user-types"); + setUserTypes(res.data.data); + } catch (err) { + console.error("Error fetching user types:", err); + } + }; + + useEffect(() => { + fetchUserTypes(); + }, []); + + const handleCreate = async () => { + try { + const numericCost = Number(newCost); + if (isNaN(numericCost) || numericCost < 0) { + alert("Please enter a valid cost."); + return; + } + const response = await apiClient.post("/user-types", { + name: newName, + cost: numericCost, + }); + const createdType = response.data.data; + setUserTypes((prev) => [...prev, { ...createdType, userCount: 0 }]); + setShowCreate(false); + setNewName(""); + setNewCost(""); + } catch (err) { + console.error("Error creating user type:", err); + } + }; + + const handleEdit = async () => { + try { + const numericCost = Number(newCost); + if (isNaN(numericCost) || numericCost < 0) { + alert("Please enter a valid cost."); + return; + } + + await apiClient.put(`/user-types/${selectedType?._id}`, { + name: newName, + cost: numericCost, + }); + + setUserTypes((prev) => + prev.map((ut) => + ut._id === selectedType?._id + ? { ...ut, name: newName, cost: numericCost } + : ut + ) + ); + + setShowEdit(false); + setSelectedType(null); + setNewName(""); + setNewCost(""); + } catch (err) { + console.error("Error editing user type:", err); + } + }; + + const handleDelete = async () => { + try { + await apiClient.delete(`/user-types/${selectedType?._id}`); + setUserTypes((prev) => prev.filter((ut) => ut._id !== selectedType?._id)); + setShowDelete(false); + setSelectedType(null); + } catch (err) { + console.error("Error deleting user type:", err); + } + }; + + return ( +
+
+
+

User Types

+ +
+ + + + + + + {/* */} + + + + + {userTypes.length === 0 ? ( + + + + ) : ( + userTypes.map((ut) => ( + + + + {/* */} + + + )) + )} + +
TypeCostNumber of UsersActions
+ No user types found. +
{ut.name} + {new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(ut.cost)} + {ut.userCount} + {ut.name === "Foster Parent (Colorado)" || + ut.name === "Staff" ? ( + <> + ) : ( +
+ { + setSelectedType(ut); + setNewName(ut.name); + setNewCost(String(ut.cost)); + setShowEdit(true); + }} + /> + { + setSelectedType(ut); + setShowDelete(true); + }} + /> +
+ )} +
+
+ + {/* Create Modal */} + {showCreate && ( +
+
+
+

Create User Type

+ +
+ + + +
+ + +
+
+
+ )} + + {/* Edit Modal */} + {showEdit && ( +
+
+
+

Edit User Type

+ +
+ + +
+ + +
+
+
+ )} + + {/* Delete Confirmation Modal */} + {showDelete && ( +
+
+
+

Confirmation

+ +
+

+ Are you sure you want to remove this user type? +

+
+ + +
+
+
+ )} +
+ ); +} From 90cec056e3115f0e0679f6af48080c62d66e25a5 Mon Sep 17 00:00:00 2001 From: Yifei Fang Date: Tue, 6 May 2025 20:57:47 -0500 Subject: [PATCH 7/7] pricing reflects on user side --- backend/controllers/loginController.ts | 7 ++- backend/controllers/userController.ts | 13 +++-- .../components/AdminSidebar/AdminSidebar.tsx | 2 +- frontend/src/components/Sidebar/sidebar.tsx | 8 +-- .../Admin/UserTypesPage/UserTypesPage.tsx | 8 +-- frontend/src/pages/CartPage/cart.tsx | 6 +- .../pages/Catalog/CatalogCourseComponent.tsx | 32 +++++++---- frontend/src/pages/UserAuth/Register.tsx | 56 ++++++++++++++++++- frontend/src/routes/appRoutes.tsx | 2 +- frontend/src/services/authService.ts | 4 +- frontend/src/services/registrationServices.ts | 2 +- frontend/src/shared/types.tsx | 7 +++ 12 files changed, 111 insertions(+), 36 deletions(-) diff --git a/backend/controllers/loginController.ts b/backend/controllers/loginController.ts index 37b3429..1a4215f 100644 --- a/backend/controllers/loginController.ts +++ b/backend/controllers/loginController.ts @@ -69,7 +69,7 @@ export const createUser = async ( company, progress, payments, - role = "foster parent", // Default role + role, isColorado, // Default value } = req.body; @@ -118,6 +118,7 @@ export const createUser = async ( }); const savedUser = await newUser.save(); + await savedUser.populate("role"); // // Generate JWT token // const accessToken = jwt.sign( @@ -163,7 +164,9 @@ export const loginUser = async (req: Request, res: Response): Promise => { } // Find user in database - let user = await User.findOne({ firebaseId }); + let user = await User.findOne({ firebaseId }).populate("role"); + + console.log(user); if (!user) { res.status(404).json({ message: "User not found" }); diff --git a/backend/controllers/userController.ts b/backend/controllers/userController.ts index 10da7ef..9dbbdad 100644 --- a/backend/controllers/userController.ts +++ b/backend/controllers/userController.ts @@ -228,12 +228,17 @@ export const checkAdmin = async (req: AuthenticatedRequest, res: Response) => { .json({ message: "Unauthorized: No user data found" }); } - const user = await User.findOne({ firebaseId: req.user.uid }); + const user = await User.findOne({ firebaseId: req.user.uid }).populate( + "role" + ); - if (!user) { - return res.status(404).json({ message: "User not found" }); + if (!user || !user.role) { + return res + .status(404) + .json({ message: "User not found or role missing" }); } - const isAdmin = user.role === "staff"; + + const isAdmin = (user.role as any).name?.toLowerCase() === "staff"; return res.status(200).json({ isAdmin }); } catch (error) { diff --git a/frontend/src/components/AdminSidebar/AdminSidebar.tsx b/frontend/src/components/AdminSidebar/AdminSidebar.tsx index c82f8cb..41a08d3 100644 --- a/frontend/src/components/AdminSidebar/AdminSidebar.tsx +++ b/frontend/src/components/AdminSidebar/AdminSidebar.tsx @@ -108,7 +108,7 @@ interface AdminSidebarProps { export function AdminSidebar({ isLoggedIn, setIsLoggedIn }: AdminSidebarProps) { // User Info const name = isLoggedIn ? JSON.parse(localStorage.user).name : "Log In"; - const role = isLoggedIn ? JSON.parse(localStorage.user).role : "Log In"; + const role = isLoggedIn ? JSON.parse(localStorage.user).role.name : "Log In"; // State for tracking the active item (can be parent or sub-item href) const [activeItem, setActiveItem] = useState( diff --git a/frontend/src/components/Sidebar/sidebar.tsx b/frontend/src/components/Sidebar/sidebar.tsx index c1b31e3..580f28b 100644 --- a/frontend/src/components/Sidebar/sidebar.tsx +++ b/frontend/src/components/Sidebar/sidebar.tsx @@ -20,10 +20,10 @@ import authService from "../../services/authService"; // User information export const userInfo = { name: "First L.", - role: localStorage.user ? localStorage.user.role : "No role", + role: localStorage.user ? JSON.parse(localStorage.user).role.name : "No role", isLoggedIn: false, isAdmin: localStorage.user - ? localStorage.user.role === "staff" + ? JSON.parse(localStorage.user).role.name === "Staff" ? true : false : false, @@ -102,7 +102,7 @@ export function Sidebar({ }: SidebarProps) { // User Info const name = isLoggedIn ? JSON.parse(localStorage.user).name : "Log In"; - const role = isLoggedIn ? JSON.parse(localStorage.user).role : "Log In"; + const role = isLoggedIn ? JSON.parse(localStorage.user).role.name : "Log In"; // Automatically collapse sidebar for narrow screens useEffect(() => { const handleResize = () => { @@ -187,7 +187,7 @@ export function SidebarItems({ cartItemCount, }: SidebarItemsProps) { const [isAdmin, setIsAdmin] = useState( - localStorage.user && JSON.parse(localStorage.user).role === "staff" + localStorage.user && JSON.parse(localStorage.user).role.name === "Staff" ); // const checkAdmin = async () => { diff --git a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx index 70c0e2f..8f60261 100644 --- a/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx +++ b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx @@ -1,13 +1,7 @@ import React, { useEffect, useState } from "react"; import apiClient from "../../../services/apiClient"; import { Pencil, Trash2 } from "lucide-react"; - -interface UserType { - _id: string; - name: string; - userCount: number; - cost: number; -} +import { UserType } from "../../../shared/types"; export default function UserTypesPage() { const [userTypes, setUserTypes] = useState([]); diff --git a/frontend/src/pages/CartPage/cart.tsx b/frontend/src/pages/CartPage/cart.tsx index ff2c6ae..039b8cf 100644 --- a/frontend/src/pages/CartPage/cart.tsx +++ b/frontend/src/pages/CartPage/cart.tsx @@ -31,13 +31,13 @@ export default function Cart() { }; return ( -
+

Checkout

{cartItems.map((item, index) => (

{item.className}

@@ -65,7 +65,7 @@ export default function Cart() {

Summary

-

+

Total Cost: ${totalCost.toFixed(2)}

{totalCost === 0 ? ( diff --git a/frontend/src/pages/Catalog/CatalogCourseComponent.tsx b/frontend/src/pages/Catalog/CatalogCourseComponent.tsx index 70c5ab1..7db6d9d 100644 --- a/frontend/src/pages/Catalog/CatalogCourseComponent.tsx +++ b/frontend/src/pages/Catalog/CatalogCourseComponent.tsx @@ -56,9 +56,9 @@ export default function CatalogCourseComponent({ {course.ratings.length > 0 ? ( - course.ratings.reduce((sum, r) => sum + r.rating, 0) / - course.ratings.length - ).toFixed(1) + course.ratings.reduce((sum, r) => sum + r.rating, 0) / + course.ratings.length + ).toFixed(1) : "No ratings"} @@ -66,9 +66,9 @@ export default function CatalogCourseComponent({ parseInt( course.ratings.length > 0 ? ( - course.ratings.reduce((sum, r) => sum + r.rating, 0) / - course.ratings.length - ).toFixed(1) + course.ratings.reduce((sum, r) => sum + r.rating, 0) / + course.ratings.length + ).toFixed(1) : "0" ) )} @@ -94,12 +94,22 @@ export default function CatalogCourseComponent({