diff --git a/prisma/migrations/20250113182505_add_admin_deposit_method/migration.sql b/prisma/migrations/20250113182505_add_admin_deposit_method/migration.sql new file mode 100644 index 00000000..08df6993 --- /dev/null +++ b/prisma/migrations/20250113182505_add_admin_deposit_method/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "DepositMethod" ADD VALUE 'ADMIN'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 05572108..0fa2225d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -217,4 +217,5 @@ enum DepositMethod { MANUAL_MOBILEPAY STRIPE ACCOUNT_MIGRATION + ADMIN } diff --git a/src/app/(admin)/admin/edit-products/AdminProductSection.tsx b/src/app/(admin)/admin/edit-products/AdminProductSection.tsx index cca2466f..a50f789a 100644 --- a/src/app/(admin)/admin/edit-products/AdminProductSection.tsx +++ b/src/app/(admin)/admin/edit-products/AdminProductSection.tsx @@ -1,10 +1,11 @@ "use client"; import { useRef, useState } from "react"; -import { HiX } from "react-icons/hi"; +import { HiPlus, HiX } from "react-icons/hi"; import { ClientProduct } from "@/common/types"; import { AnimatedPopup, PopupRefActions } from "@/components/ui/AnimatedPopup"; +import { FatButton } from "@/components/ui/Buttons/FatButton"; import { EditProductForm } from "@/components/ui/EditProductForm"; import { Input } from "@/components/ui/Input"; import { ListItem } from "@/components/ui/ListItem"; @@ -30,9 +31,14 @@ export const AdminProductSection = ({ products }: Props) => { Displaying {filteredProducts.length} of {products.length} products - setProductFilter(e.target.value.toLowerCase())} + +
diff --git a/src/app/(admin)/admin/edit-products/new/page.tsx b/src/app/(admin)/admin/edit-products/new/page.tsx new file mode 100644 index 00000000..a9b82e75 --- /dev/null +++ b/src/app/(admin)/admin/edit-products/new/page.tsx @@ -0,0 +1,15 @@ +import { AdminTitle } from "@/components/ui/AdminTitle"; +import { EditProductForm } from "@/components/ui/EditProductForm"; + +const NewProduct = () => { + return ( +
+ +
+ +
+
+ ); +}; + +export default NewProduct; diff --git a/src/app/(admin)/admin/newProduct/page.tsx b/src/app/(admin)/admin/newProduct/page.tsx deleted file mode 100644 index 3827d28f..00000000 --- a/src/app/(admin)/admin/newProduct/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ - -import { EditProductForm } from "@/components/ui/EditProductForm"; - -const NewProduct = () => { - return ( -
- -
- ); -}; - -export default NewProduct; diff --git a/src/app/(admin)/admin/superadmin/AdminList.tsx b/src/app/(admin)/admin/superadmin/AdminList.tsx index 1bd18d74..b8062186 100644 --- a/src/app/(admin)/admin/superadmin/AdminList.tsx +++ b/src/app/(admin)/admin/superadmin/AdminList.tsx @@ -3,6 +3,7 @@ import toast from "react-hot-toast"; import { HiX } from "react-icons/hi"; +import { ClientUser } from "@/common/types"; import { changeUserRole } from "@/server/actions/admin/changeRole"; import { useAutoAnimate } from "@formkit/auto-animate/react"; import { Role, User } from "@prisma/client"; @@ -10,7 +11,7 @@ import { Role, User } from "@prisma/client"; import { NewAdminDialog } from "./NewAdminDialog"; interface Props { - users: User[]; + users: ClientUser[]; } export const AdminList = ({ users }: Props) => { @@ -18,7 +19,7 @@ export const AdminList = ({ users }: Props) => { const [parent] = useAutoAnimate({ duration: 200 }); - const changeRole = async (user: User, role: Role) => { + const changeRole = async (user: ClientUser, role: Role) => { try { await changeUserRole(user.id, role); toast.success("User role changed successfully"); diff --git a/src/app/(admin)/admin/superadmin/NewAdminDialog.tsx b/src/app/(admin)/admin/superadmin/NewAdminDialog.tsx index eb789bda..712ae127 100644 --- a/src/app/(admin)/admin/superadmin/NewAdminDialog.tsx +++ b/src/app/(admin)/admin/superadmin/NewAdminDialog.tsx @@ -4,6 +4,7 @@ import { useRef, useState } from "react"; import toast from "react-hot-toast"; import { HiPlus, HiX } from "react-icons/hi"; +import { ClientUser } from "@/common/types"; import { AnimatedPopup, PopupRefActions } from "@/components/ui/AnimatedPopup"; import { FatButton } from "@/components/ui/Buttons/FatButton"; import { InputWithLabel } from "@/components/ui/Input"; @@ -12,7 +13,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { Role, User } from "@prisma/client"; interface Props { - users: User[]; + users: ClientUser[]; } const AddAdminButton = ( @@ -39,7 +40,7 @@ export const NewAdminDialog = ({ users }: Props) => { const popupRef = useRef(undefined); - const changeRole = async (user: User, role: Role) => { + const changeRole = async (user: ClientUser, role: Role) => { try { await changeUserRole(user.id, role); toast.success("User role changed successfully"); diff --git a/src/app/(admin)/admin/users/ManageUserDialog.tsx b/src/app/(admin)/admin/users/ManageUserDialog.tsx new file mode 100644 index 00000000..fd7ef99c --- /dev/null +++ b/src/app/(admin)/admin/users/ManageUserDialog.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { format } from "date-fns"; +import { useRef, useState } from "react"; +import toast from "react-hot-toast"; +import { HiPencil, HiPlus, HiX } from "react-icons/hi"; + +import { ClientUser } from "@/common/types"; +import { AnimatedPopup, PopupRefActions } from "@/components/ui/AnimatedPopup"; +import { FatButton } from "@/components/ui/Buttons/FatButton"; +import { IconButton } from "@/components/ui/Buttons/IconButton"; +import { InputWithLabel } from "@/components/ui/Input"; +import { adminAddFundsAction } from "@/server/actions/transaction/addFunds"; +import { Role, User } from "@prisma/client"; + +interface Props { + user: ClientUser; +} + +const EditUserButton = ( + +); + +export const ManageUserDialog = ({ user }: Props) => { + const [modifyBalanceAmount, setModifyBalanceAmount] = useState( + "", + ); + + const popupRef = useRef(undefined); + + const handleInputChange = (e: React.ChangeEvent) => { + const value = Math.round(parseFloat(e.target.value) * 100) / 100; + if (!Number.isNaN(value)) { + setModifyBalanceAmount(value); + } else { + setModifyBalanceAmount(""); + } + }; + + const addToBalance = async () => { + try { + if (modifyBalanceAmount === "" || modifyBalanceAmount === 0) + throw new Error("Please enter a valid amount"); + await adminAddFundsAction(modifyBalanceAmount, user.id); + toast.success("Balance added successfully"); + } catch (error) { + toast.error(`Failed to add balance: ${error}`); + } + setModifyBalanceAmount(""); + }; + + const removeFromBalance = async () => { + try { + if (modifyBalanceAmount === "" || modifyBalanceAmount === 0) + throw new Error("Please enter a valid amount"); + await adminAddFundsAction(-modifyBalanceAmount, user.id); + toast.success("Balance removed successfully"); + } catch (error) { + toast.error(`Failed to remove balance: ${error}`); + } + setModifyBalanceAmount(""); + }; + + return ( + +
+
+

+ Manage user +

+ {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
popupRef?.current?.closeContainer()} + > + +
+
+
+ + {user.firstName} {user.lastName} + {" "} +
Username: {user.userName}
+
Role: {user.role}
+
+ Created: {format(user.createdAt, "dd.MM.yyyy")} +
+
+ +
+ +
+ removeFromBalance()} + /> + addToBalance()} + /> +
+
+
+
+ ); +}; diff --git a/src/app/(admin)/admin/users/UserList.tsx b/src/app/(admin)/admin/users/UserList.tsx new file mode 100644 index 00000000..e7488345 --- /dev/null +++ b/src/app/(admin)/admin/users/UserList.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { ClientUser } from "@/common/types"; +import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { User } from "@prisma/client"; + +import { ManageUserDialog } from "./ManageUserDialog"; + +interface Props { + users: ClientUser[]; +} + +export const UserList = ({ users }: Props) => { + const [parent] = useAutoAnimate({ duration: 200 }); + + return ( +
+
+ + Displaying {users.length} users + +
+
+ {users.map((user) => ( +
+
+ + {user.firstName} {user.lastName} + {" "} + ({user.userName}) +
+ {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} + +
+ ))} +
+
+ ); +}; diff --git a/src/app/(admin)/admin/users/page.tsx b/src/app/(admin)/admin/users/page.tsx new file mode 100644 index 00000000..16ab03fe --- /dev/null +++ b/src/app/(admin)/admin/users/page.tsx @@ -0,0 +1,16 @@ +import { AdminTitle } from "@/components/ui/AdminTitle"; +import { getAllUsers } from "@/server/db/queries/account"; + +import { UserList } from "./UserList"; + +const ManageUsers = async () => { + const users = await getAllUsers(); + return ( +
+ + +
+ ); +}; + +export default ManageUsers; diff --git a/src/app/(admin)/login/admin/AdminLoginForm.tsx b/src/app/(admin)/login/admin/AdminLoginForm.tsx index 42b93cb5..26ce104c 100644 --- a/src/app/(admin)/login/admin/AdminLoginForm.tsx +++ b/src/app/(admin)/login/admin/AdminLoginForm.tsx @@ -24,6 +24,8 @@ export const AdminLoginForm = () => { labelText="Namu ID" placeholder={"Namu Admin"} name="userName" + autoCapitalize="none" + spellCheck="false" required /> ; +export type ClientUser = Pick< + User, + | "id" + | "userName" + | "firstName" + | "lastName" + | "role" + | "createdAt" + | "updatedAt" +>; + export const CartProductParser = ClientProductParser.extend({ quantity: z .number() diff --git a/src/components/ui/AdminSidebar/index.tsx b/src/components/ui/AdminSidebar/index.tsx index 741f5be8..5659e883 100644 --- a/src/components/ui/AdminSidebar/index.tsx +++ b/src/components/ui/AdminSidebar/index.tsx @@ -1,14 +1,11 @@ "use client"; -import { useState } from "react"; import { FaCrown, FaLock } from "react-icons/fa6"; import { HiChartBar, - HiChevronLeft, - HiChevronRight, - HiOutlinePlusCircle, HiShoppingCart, HiSparkles, + HiUsers, } from "react-icons/hi"; import { SidebarItem } from "@/components/ui/AdminSidebar/SidebarItem"; @@ -26,12 +23,8 @@ export const AdminSidebar = ({ superadmin }: Props) => { Icon={HiShoppingCart} href="/admin/edit-products" /> - + {superadmin && ( {withBackButton && ( router.back()} /> )} diff --git a/src/server/actions/transaction/addFunds.tsx b/src/server/actions/transaction/addFunds.tsx index 2de2c143..8a37d2ea 100644 --- a/src/server/actions/transaction/addFunds.tsx +++ b/src/server/actions/transaction/addFunds.tsx @@ -47,3 +47,23 @@ export const addFundsAction = async ( return { error: message }; } }; + +export const adminAddFundsAction = async (amount: number, userId: number) => { + try { + await db.$transaction(async (tx) => { + try { + await newDeposit( + tx as PrismaClient, + userId, + amount, + DepositMethod.ADMIN, + ); + } catch (error: any) { + throw error?.message || "Unknown error when adding funds"; + } + }); + } catch (error: any) { + const message = error?.message || "Unknown error when adding funds"; + return { error: message }; + } +}; diff --git a/src/server/db/queries/account.ts b/src/server/db/queries/account.ts index 182852c9..dd4a7f5e 100644 --- a/src/server/db/queries/account.ts +++ b/src/server/db/queries/account.ts @@ -1,7 +1,7 @@ "use server"; import { getSession } from "@/auth/ironsession"; -import type { CreateAccountCredentials } from "@/common/types"; +import type { ClientUser, CreateAccountCredentials } from "@/common/types"; import { db } from "@/server/db/prisma"; import { createPincodeHash } from "@/server/db/utils/auth"; import type { GenericClient } from "@/server/db/utils/dbTypes"; @@ -73,8 +73,18 @@ export const createAccount = async ({ return newUser; }; -export const getAllUsers = async () => { - return db.user.findMany(); +export const getAllUsers = async (): Promise => { + return db.user.findMany({ + select: { + id: true, + firstName: true, + lastName: true, + userName: true, + role: true, + createdAt: true, + updatedAt: true, + }, + }); }; export const updatePincode = async (newPincode: string, userId: number) => {