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) => {