diff --git a/backend/app.ts b/backend/app.ts index bb9d877..1c25471 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -24,6 +24,7 @@ import speakerRoutes from "./routes/speakerRoutes"; import pdfRoutes from "./routes/pdfRoutes"; import emailTemplateRoutes from "./routes/emailTemplateRoutes"; import zoomRoutes from "./routes/zoomRoutes"; +import userTypeRoutes from "./routes/userTypeRoutes"; // Import middleware import { notFound, errorHandler } from "./middlewares/errorMiddleware"; @@ -94,7 +95,8 @@ app.use("/api/emails", verifyFirebaseAuth, emailRoutes); app.use("/api/emailTemplates", verifyFirebaseAuth, emailTemplateRoutes); app.use("/api/speakers", verifyFirebaseAuth, speakerRoutes); app.use("/api/upload", verifyFirebaseAuth, uploadRoutes); -app.use("/api/zoom", verifyFirebaseAuth, zoomRoutes) +app.use("/api/user-types", userTypeRoutes); +app.use("/api/zoom", verifyFirebaseAuth, zoomRoutes); app.use("/api/certificatePDFs", verifyFirebaseAuth, pdfRoutes); app.use(notFound); 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/backend/controllers/userTypeController.ts b/backend/controllers/userTypeController.ts new file mode 100644 index 0000000..1d9bbe5 --- /dev/null +++ b/backend/controllers/userTypeController.ts @@ -0,0 +1,69 @@ +import { Request, Response, NextFunction, RequestHandler } from "express"; +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, cost } = req.body; + + // 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 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 + } +}; + +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, 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 f0a86c0..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,20 +29,8 @@ const userSchema: Schema = new Schema( email: { type: String, required: true }, 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, + type: Schema.Types.ObjectId, + ref: "UserType", }, name: { type: String, required: true }, address1: { type: String, required: true }, @@ -63,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 new file mode 100644 index 0000000..1e75e1a --- /dev/null +++ b/backend/models/userTypeModel.ts @@ -0,0 +1,21 @@ +// backend/models/userTypeModel.ts +import mongoose, { Schema, Document, Model } from "mongoose"; + +export interface IUserType extends Document { + name: string; + cost: number; +} + +const userTypeSchema: Schema = new Schema( + { + name: { type: String, required: true, unique: true }, + cost: { type: Number, required: true }, + }, + { timestamps: true } +); + +const UserType: Model = mongoose.model( + "UserType", + userTypeSchema +); +export default UserType; diff --git a/backend/routes/userTypeRoutes.ts b/backend/routes/userTypeRoutes.ts new file mode 100644 index 0000000..d9a9f37 --- /dev/null +++ b/backend/routes/userTypeRoutes.ts @@ -0,0 +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; 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 new file mode 100644 index 0000000..8f60261 --- /dev/null +++ b/frontend/src/pages/Admin/UserTypesPage/UserTypesPage.tsx @@ -0,0 +1,308 @@ +import React, { useEffect, useState } from "react"; +import apiClient from "../../../services/apiClient"; +import { Pencil, Trash2 } from "lucide-react"; +import { UserType } from "../../../shared/types"; + +export default function UserTypesPage() { + 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? +

+
+ + +
+
+
+ )} +
+ ); +} 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({