diff --git a/src/app/actions/change-role.ts b/src/app/actions/change-role.ts index b28fea2..c639054 100644 --- a/src/app/actions/change-role.ts +++ b/src/app/actions/change-role.ts @@ -1,25 +1,56 @@ "use server"; import prisma from "@/server/db"; +import { getServerSideSession } from "@/lib/get-server-session"; import { revalidatePath } from "next/cache"; +import getErrorMessage from "@/utils/getErrorMessage"; +import { UserRole,ADMIN_USERS_PATH } from "@/constants"; -async function updateUserRole(id: string, role: string) { +async function updateUserRole(id: string, role: UserRole) { + const VALID_ROLES = Object.values(UserRole); + if (!VALID_ROLES.includes(role)) { + throw new Error(`Invalid role: ${role}`); + } + const session = await getServerSideSession(); + if (!session || session.user.role !== UserRole.ADMIN) { + throw new Error(`Unauthorized Access...`); + } try { const updatedUser = await prisma.user.update({ where: { id }, data: { role }, }); - revalidatePath("/admin/users"); + revalidatePath(ADMIN_USERS_PATH); return updatedUser; } catch (error) { - console.error("Error updating user role:", error); - return null; + console.error("Error updating user role:", getErrorMessage(error)); + throw new Error("Failed to update user role. Please try again later."); } } + export const makeAdmin = async (userId: string) => { - return await updateUserRole(userId, "ADMIN"); + try { + return await updateUserRole(userId, UserRole.ADMIN); + } catch (error) { + console.error("Failed to make user admin:", getErrorMessage(error)); + return null; + } }; export const makeParticipant = async (userId: string) => { - return await updateUserRole(userId, "PARTICIPANT"); + try { + return await updateUserRole(userId, UserRole.PARTICIPANT); + } catch (error) { + console.error("Failed to make user participant:", getErrorMessage(error)); + return null; + } +}; + +export const makeCoordinator = async (userId: string) => { + try { + return await updateUserRole(userId, UserRole.COORDINATOR); + } catch (error) { + console.error("Failed to make user coordinator:", getErrorMessage(error)); + return null; + } }; diff --git a/src/app/actions/create-coupon-code.ts b/src/app/actions/create-coupon-code.ts index b659ffb..7053578 100644 --- a/src/app/actions/create-coupon-code.ts +++ b/src/app/actions/create-coupon-code.ts @@ -1,17 +1,46 @@ "use server"; +import { generateCouponCode } from "@/lib/helper"; import prisma from "@/server/db"; +import { couponSchema } from "@/utils/zod-schemas"; export const saveCoupon = async ( - coupon: string, - id: string, - discount: string = "20", + coupon: string, + createdById: string, + discount: number = 20, + numberOfCoupons: number = 1 ) => { - const resp = await prisma.referral.create({ - data: { - code: coupon, - isUsed: false, - createdById: id, - discountPercentage: discount, - }, - }); + try { + const validatCoupon = couponSchema.parse({ coupon, createdById, discount }); + + // Check if the coupon already exists + const couponExists = await prisma.referral.findFirst({ + where: { code: validatCoupon.coupon }, + }); + if (couponExists) { + throw new Error("Coupon code already exists"); + } + + // Create coupons + const createCoupon = async (code: string) => { + return prisma.referral.create({ + data: { + code, + isUsed: false, + createdById: validatCoupon.createdById, + discountPercentage: validatCoupon.discount.toString(), + }, + }); + }; + + const couponCodes = + numberOfCoupons === 1 + ? [validatCoupon.coupon] + : Array.from({ length: numberOfCoupons }, () => generateCouponCode(10)); + + const responses = await Promise.all(couponCodes.map(createCoupon)); + return responses; + } catch (error) { + console.error("Error creating coupon:", error); + throw new Error("Failed to create coupon. Please try again later."); + } }; diff --git a/src/app/actions/get-payment-count.ts b/src/app/actions/get-payment-count.ts new file mode 100644 index 0000000..53e98a4 --- /dev/null +++ b/src/app/actions/get-payment-count.ts @@ -0,0 +1,15 @@ +"use server"; + +import { getServerSideSession } from "@/lib/get-server-session"; +import prisma from "@/server/db"; + +export default async function getPaymentCount() { + const session = await getServerSideSession(); + if (!session) { + return null; + } + + const paymentCount = await prisma.payment.count(); + + return paymentCount; +} diff --git a/src/app/actions/get-user-by-id.ts b/src/app/actions/get-user-by-id.ts index 1b3af8d..24c3f91 100644 --- a/src/app/actions/get-user-by-id.ts +++ b/src/app/actions/get-user-by-id.ts @@ -1,13 +1,20 @@ import prisma from "@/server/db"; // Adjust the import based on your structure export async function getUserById(userId: string) { - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - id: true, - role: true, // Include any other fields you need - }, - }); - - return user; + try { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + role: true, + }, + }); + if (!user) { + throw new Error(`User with ID ${userId} not found`); + } + return user; + } catch (error) { + console.error("Error getting user by id:", error); + throw new Error("Failed to get user. Please try again later."); + } } diff --git a/src/app/actions/get-user-count.ts b/src/app/actions/get-user-count.ts new file mode 100644 index 0000000..2cfd371 --- /dev/null +++ b/src/app/actions/get-user-count.ts @@ -0,0 +1,14 @@ +"use server"; +import prisma from "@/server/db"; +import { getServerSideSession } from "@/lib/get-server-session"; + +export default async function getUserCount() { + const session = await getServerSideSession(); + if (!session) { + return null; + } + + const userCount = await prisma.user.count(); + + return userCount; +} \ No newline at end of file diff --git a/src/app/actions/is-allowed-to-access.ts b/src/app/actions/is-allowed-to-access.ts index a9bd130..4a2cd57 100644 --- a/src/app/actions/is-allowed-to-access.ts +++ b/src/app/actions/is-allowed-to-access.ts @@ -3,10 +3,16 @@ import prisma from "@/server/db"; export const isAllowedToAccess = async (email: string): Promise => { + if (!email) return false; + try { const user = await prisma.sjecUser.findFirst({ - where: { - email: email, - }, + where: { + email: email, + }, }); return user !== null; + } catch (error) { + console.error("Error getting user by email:", error); + return false; + } }; diff --git a/src/app/actions/submit-form.ts b/src/app/actions/submit-form.ts index 3d19454..d0634af 100644 --- a/src/app/actions/submit-form.ts +++ b/src/app/actions/submit-form.ts @@ -3,26 +3,37 @@ import { getServerSideSession } from "@/lib/get-server-session"; import prisma from "@/server/db"; import { FormDataInterface } from "@/types"; +import { z } from "zod"; + +const amountSchema = z.number().positive("Amount must be a positive number."); export async function submitForm(data: FormDataInterface, amount: number) { const session = await getServerSideSession(); if (!session) { - return; + throw new Error("User is not authenticated"); } + const validatedAmount = amountSchema.parse(amount); + + const totalAmount = Math.round(validatedAmount + validatedAmount * 0.02); - return await prisma.form.create({ - data: { - name: data.name, - usn: data.usn, - email: data.email, - foodPreference: data.foodPreference, - contact: data.phone, - designation: data.designation, - paidAmount: amount, - photo: data.photo, - collegeIdCard: data.idCard, - createdById: session.user.id, - entityName: data.entityName, - }, - }); + try { + return await prisma.form.create({ + data: { + name: data.name, + usn: data.usn, + email: data.email, + foodPreference: data.foodPreference, + contact: data.phone, + designation: data.designation, + paidAmount: totalAmount, + photo: data.photo, + collegeIdCard: data.idCard, + createdById: session.user.id, + entityName: data.entityName, + }, + }); + } catch (error) { + console.error("Error creating form:", error); + throw new Error("Failed to submit the form. Please try again later."); + } } diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 27062f5..00050ab 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -5,7 +5,9 @@ import { Inter } from "next/font/google"; import "./globals.css"; import Providers from "@/components/layout/Provider"; import { AdminNavbar } from "@/components/admin/Navbar/navbar"; -import { useSession } from "next-auth/react"; +import { signIn, useSession } from "next-auth/react"; +import { useEffect, useState } from "react"; +import { tailChase } from "ldrs"; const inter = Inter({ subsets: ["latin"] }); @@ -14,22 +16,62 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const { data: session } = useSession({ + const { data: session, status } = useSession({ required: true, + onUnauthenticated: async () => { + await signIn("google"); + }, }); + if (typeof window !== "undefined") { + tailChase.register(); + } + + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (status === "loading") { + setIsLoading(true); + } else { + setIsLoading(false); + } + }, [status]); + + if (isLoading || status !== "authenticated" || !session) { + // Show the loading spinner if session is loading or not authenticated + return ( +
+ +
+ ); + } + if (!session) { return ( -
- Unauthorized +
+
+

Unauthorized

+

+ You need to log in to access this page. +

+
); } - if (session.user.role !== "ADMIN") { + if (session.user.role !== "ADMIN" && session.user.role !== "COORDINATOR") { return ( -
- Forbidden +
+
+

Forbidden

+

+ You do not have the required permissions to view this page. +

+
); } @@ -40,7 +82,7 @@ export default function RootLayout({
-
+
{children}
diff --git a/src/app/admin/loading.tsx b/src/app/admin/loading.tsx new file mode 100644 index 0000000..9410ac2 --- /dev/null +++ b/src/app/admin/loading.tsx @@ -0,0 +1,31 @@ +"use client" +import React from "react"; + +const Loading: React.FC = () => { + return ( +
+
+ +
+ ); +}; + +export default Loading; diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index ddcca3d..fc474c9 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,17 +1,30 @@ "use client"; import { Coupon } from "@/components/admin/code-generation-card"; import { useSession } from "next-auth/react"; +import Payments from "./payment/page"; export default function AdminPage() { const { data: session } = useSession(); - if (!session || session.user.role != "ADMIN") { + if ( + !session || + (session.user.role != "ADMIN" && session.user.role != "COORDINATOR") + ) { return
Unauthorized
; } return ( <> -
- +
+ {session.user.role === "ADMIN" ? ( + + ) : session.user.role === "COORDINATOR" ? ( +
+

Welcome, Coordinator!

+

+ You have successfully logged in with the Coordinator role. +

+
+ ) : null}
); diff --git a/src/app/admin/payment/page.tsx b/src/app/admin/payment/page.tsx index c995e17..39528fe 100644 --- a/src/app/admin/payment/page.tsx +++ b/src/app/admin/payment/page.tsx @@ -1,12 +1,12 @@ -import { SearchableInfiniteScrollTable } from "@/components/common/searchable-infinite-scroll-table"; +import { PaymentCards } from "@/components/common/searchable-infinite-scroll-table"; import React from "react"; export default async function Payments() { - return ( - <> -
- -
- - ); + return ( + <> +
+ +
+ + ); } diff --git a/src/app/admin/razorpay/[id]/page.tsx b/src/app/admin/razorpay/[id]/page.tsx index 2dcd4a0..e3e5ff9 100644 --- a/src/app/admin/razorpay/[id]/page.tsx +++ b/src/app/admin/razorpay/[id]/page.tsx @@ -65,6 +65,16 @@ function FetchRazorpayPaymentData({ params }: { params: { id: string } }) { const [loading, setLoading] = useState(true); const [loadingForButton, setLoadingForButton] = useState(false); + const handleSendEmail = async () => { + if (!paymentData) { + return; + } + setLoadingForButton(true) + await sendEmail(paymentData.id) + setLoadingForButton(false) + } + + async function sendEmail(paymentId: string) { setLoadingForButton(true); try { @@ -127,67 +137,62 @@ function FetchRazorpayPaymentData({ params }: { params: { id: string } }) { } return ( -
- - - - Payment Data of {paymentData.notes.customerName || "Unknown"} - - - - - - - - - - - - - -
+
+ + + + Payment Data of {paymentData.notes.customerName || "Unknown"} + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
); } diff --git a/src/app/admin/razorpay/page.tsx b/src/app/admin/razorpay/page.tsx index afeab50..5afe394 100644 --- a/src/app/admin/razorpay/page.tsx +++ b/src/app/admin/razorpay/page.tsx @@ -1,4 +1,6 @@ -"use client"; +"use client" + +import { Button } from "@/components/ui/button" import { Form, FormControl, @@ -7,70 +9,66 @@ import { FormItem, FormLabel, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useRouter } from "next/navigation"; -import React from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { zodResolver } from "@hookform/resolvers/zod" +import { useRouter } from "next/navigation" +import React from "react" +import { useForm } from "react-hook-form" +import { z } from "zod" const formSchema = z.object({ razorpayPaymentId: z.string().nonempty("Payment ID is required"), -}); +}) const FetchRazorpayPaymentData = () => { const form = useForm>({ resolver: zodResolver(formSchema), - }); - const router = useRouter(); + }) + const router = useRouter() const onSubmit = async (data: z.infer) => { - router.push(`/admin/razorpay/${data.razorpayPaymentId}`); - }; + router.push(`/admin/razorpay/${data.razorpayPaymentId}`) + } return ( -
-

- Search by Razorpay Payment ID -

-
- - ( - - - Razorpay Payment ID: - - - - - - Search for a payment by its Razorpay Payment ID - - - - )} - /> - - - +
+ + + + Search by Razorpay Payment ID + + + +
+ + ( + + Razorpay Payment ID + + + + + Search for a payment by its Razorpay Payment ID + + + + )} + /> + + + +
+
- ); -}; + ) +} + +export default FetchRazorpayPaymentData -export default FetchRazorpayPaymentData; diff --git a/src/app/admin/verify/page.tsx b/src/app/admin/verify/page.tsx index e0c56e9..d119bcb 100644 --- a/src/app/admin/verify/page.tsx +++ b/src/app/admin/verify/page.tsx @@ -15,7 +15,7 @@ const QRCodeScanner = () => { const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera; return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test( - userAgent.toLowerCase(), + userAgent.toLowerCase() ); }; setIsMobile(checkMobile()); @@ -30,7 +30,7 @@ const QRCodeScanner = () => { const url = new URL(result); if (!url.href.startsWith("https://tedxsjec")) { - throw new Error("Invalid QR code . Please scan a valid QR code"); + throw new Error("Invalid QR code . Please scan a valid QR code"); } // Redirect to the scanned URL router.push(url.toString()); @@ -41,8 +41,14 @@ const QRCodeScanner = () => { } }; + + const handleError = (error: unknown) => { - if (error instanceof Error) { + if (error instanceof DOMException && error.name === "NotAllowedError") { + setError( + "Camera access denied. Please allow camera access to scan QR codes." + ); + } else if (error instanceof Error) { setError("Error accessing camera: " + error.message); } else { setError("An unknown error occurred."); @@ -57,8 +63,8 @@ const QRCodeScanner = () => { Error

- This feature is only available on mobile devices. Please access this - page from your smartphone or tablet. + This feature is not available on mobile devices. Please access this + page from your tablet or laptop.

diff --git a/src/app/api/users/payment/route.ts b/src/app/api/users/payment/route.ts index 0e3c476..7f1c30c 100644 --- a/src/app/api/users/payment/route.ts +++ b/src/app/api/users/payment/route.ts @@ -5,63 +5,85 @@ import { NextRequest, NextResponse } from "next/server"; export const dynamic = "force-dynamic"; export async function GET(request: NextRequest) { - const session = await getServerSideSession(); - if (!session) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); - } + const session = await getServerSideSession(); + if (!session) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); + } - if (session.user?.role !== "ADMIN") { - return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); - } - const { searchParams } = new URL(request.url); - try { - const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); - const search = searchParams.get("search") || ""; - const limit = 10; + if (session.user?.role !== "ADMIN" && session.user?.role !== "COORDINATOR") { + return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); + } + const { searchParams } = new URL(request.url); + try { + const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); + const search = searchParams.get("search") || ""; + const limit = 10; - const [users, totalCount] = await Promise.all([ - prisma.payment.findMany({ - skip: (page - 1) * limit, - take: limit, - where: { - razorpayPaymentId: { - contains: search, - }, - }, - include: { - user: { - select: { - name: true, - email: true, - }, - }, - }, - }), - prisma.payment.count({ - where: { - razorpayPaymentId: { - contains: search, - }, - }, - }), - ]); + const [users, totalCount] = await Promise.all([ + prisma.payment.findMany({ + skip: (page - 1) * limit, + take: limit, + where: { + OR: [ + // Update to search in multiple fields + { + razorpayPaymentId: { + contains: search, + }, + }, + { + user: { + name: { + contains: search, + }, + }, + }, + { + user: { + email: { + contains: search, + }, + }, + }, + ], + }, + include: { + user: { + select: { + name: true, + email: true, + forms: { + select: { + photo: true, + }, + take: 1, + }, + }, + }, + }, + }), + prisma.payment.count({ + where: { + razorpayPaymentId: { + contains: search, + }, + }, + }), + ]); - const totalPages = Math.ceil(totalCount / limit); + const totalPages = Math.ceil(totalCount / limit); - return NextResponse.json({ - users, - pagination: { - currentPage: page, - totalCount, - totalPages, - limit, - }, - }); - } catch (error) { - console.error("Error fetching payment details:", error); - return NextResponse.json( - { error: "Failed to fetch data" }, - { status: 500 }, - ); - } + return NextResponse.json({ + users, + pagination: { + currentPage: page, + totalCount, + totalPages, + limit, + }, + }); + } catch (error) { + console.error("Error fetching payment details:", error); + return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 }); + } } diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index b0ce895..61e9383 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -9,7 +9,7 @@ export async function GET(req: Request) { return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); } - if (session.user?.role !== "ADMIN") { + if (session.user?.role !== "ADMIN" && session.user?.role !== "COORDINATOR") { return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); } diff --git a/src/app/auth/signin/signin-page.tsx b/src/app/auth/signin/signin-page.tsx index 0acb1bd..ca78789 100644 --- a/src/app/auth/signin/signin-page.tsx +++ b/src/app/auth/signin/signin-page.tsx @@ -53,7 +53,11 @@ export default function SignIn() {
) : ( -

Redirecting...

+
+ +

Please wait, redirecting...

+
+ )}
); diff --git a/src/app/auth/signout/page.tsx b/src/app/auth/signout/page.tsx index 684e85e..811050e 100644 --- a/src/app/auth/signout/page.tsx +++ b/src/app/auth/signout/page.tsx @@ -39,7 +39,16 @@ export default function Signin() { > ) : ( -

Redirecting...

+
+ +

+ Please wait, redirecting... +

+
)} ); diff --git a/src/components/admin/Navbar/navbar.tsx b/src/components/admin/Navbar/navbar.tsx index e92a6e2..310116f 100644 --- a/src/components/admin/Navbar/navbar.tsx +++ b/src/components/admin/Navbar/navbar.tsx @@ -4,14 +4,14 @@ import { IconType } from "react-icons"; import { FiChevronDown, FiChevronsRight, FiUser } from "react-icons/fi"; import { RiCoupon3Line } from "react-icons/ri"; import { MdPayment } from "react-icons/md"; -import { SiTicktick } from "react-icons/si"; -import { SiRazorpay } from "react-icons/si"; +import { SiTicktick, SiRazorpay } from "react-icons/si"; import { motion } from "framer-motion"; import Link from "next/link"; +import { useSession } from "next-auth/react"; export const AdminNavbar = () => { return ( -
+
@@ -21,11 +21,12 @@ export const AdminNavbar = () => { const Sidebar = () => { const [open, setOpen] = useState(true); const [selected, setSelected] = useState("Dashboard"); + const { data: session } = useSession(); return ( {
-
@@ -104,8 +111,8 @@ const Option = ({ onClick={() => setSelected(title)} className={`relative flex h-10 w-full items-center rounded-md transition-colors ${ selected === title - ? "bg-indigo-100 text-indigo-800" - : "text-slate-500 hover:bg-slate-100" + ? "bg-red-500 text-white" + : "text-gray-400 hover:bg-gray-800 hover:text-white" }`} > {title} @@ -135,7 +142,7 @@ const Option = ({ }} style={{ y: "-50%" }} transition={{ delay: 0.5 }} - className="absolute right-2 top-1/2 size-4 rounded bg-indigo-500 text-xs text-white" + className="absolute right-2 top-1/2 size-4 rounded bg-red-500 text-xs text-white" > {notifs} @@ -147,8 +154,8 @@ const Option = ({ const TitleSection = ({ open }: { open: boolean }) => { return ( -
-
+
+
{open && ( @@ -158,23 +165,24 @@ const TitleSection = ({ open }: { open: boolean }) => { animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.125 }} > - Tedxsjec - Admin Page + + Tedxsjec + + Admin Page )}
- {open && } + {open && }
); }; const Logo = () => { - // Temp logo from https://logoipsum.com/ return ( { viewBox="0 0 50 39" fill="none" xmlns="http://www.w3.org/2000/svg" - className="fill-slate-50" + className="fill-gray-50" > setOpen((pv) => !pv)} - className="absolute bottom-0 left-0 right-0 border-t border-slate-300 transition-colors hover:bg-slate-100" + className="absolute bottom-0 left-0 right-0 border-t border-gray-700 transition-colors hover:bg-gray-800" >
Hide @@ -235,4 +243,6 @@ const ToggleClose = ({ ); }; -const NavbarContent = () =>
; +const NavbarContent = () => ( +
+); diff --git a/src/components/admin/change-role.tsx b/src/components/admin/change-role.tsx index 28e5fa9..71ee239 100644 --- a/src/components/admin/change-role.tsx +++ b/src/components/admin/change-role.tsx @@ -1,6 +1,17 @@ "use client"; -import { makeAdmin, makeParticipant } from "@/app/actions/change-role"; +import { + makeAdmin, + makeCoordinator, + makeParticipant, +} from "@/app/actions/change-role"; import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@radix-ui/react-popover"; +import { ChevronDownIcon } from "lucide-react"; +import { useState } from "react"; export default function ChangeRole({ userId, @@ -9,32 +20,62 @@ export default function ChangeRole({ userId: string; userRole: string; }) { - async function handleMakeAdmin() { - await makeAdmin(userId); - } + const [currentRole, setCurrentRole] = useState(userRole); - async function handleMakeParticipant() { - await makeParticipant(userId); + async function handleRoleChange(newRole: string) { + switch (newRole) { + case "ADMIN": + await makeAdmin(userId); + break; + case "PARTICIPANT": + await makeParticipant(userId); + break; + case "COORDINATOR": + await makeCoordinator(userId); + break; + default: + break; + } + setCurrentRole(newRole); } return ( -
- - -
+ + + + + +
+ + + +
+
+
); } diff --git a/src/components/admin/code-generation-card.tsx b/src/components/admin/code-generation-card.tsx index 45c5e6b..f09d288 100644 --- a/src/components/admin/code-generation-card.tsx +++ b/src/components/admin/code-generation-card.tsx @@ -19,91 +19,160 @@ import { type Session as NextAuthSession } from "next-auth"; import CouponGeneratorDialog from "../payment/coupon-generator-dialog"; import { Checkbox } from "../ui/checkbox"; import { useState } from "react"; +// import { toast } from "sonner"; +import getErrorMessage from "@/utils/getErrorMessage"; +import { Copy, Check } from "lucide-react"; +import { toast } from "sonner"; export function Coupon({ session }: { session: NextAuthSession }) { const [discount, setDiscount] = useState("20"); + const [numberOfCoupons, setNumberOfCoupons] = useState("1"); const [checked, setChecked] = useState(false); + const [numberOfCouponsChecked, setNumberOfCouponsChecked] = useState(false); + const [disabled, setDisabled] = useState(true); + const [copied, setCopied] = useState(false); const { data, isPending, isError, error, refetch } = useQuery({ queryKey: ["coupon"], queryFn: createCouponCode, enabled: false, }); + const handleCopy = () => { + if (!data) return; + navigator.clipboard.writeText(data).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); // Reset after 2 seconds + }); + }; + + const handleGenerateCoupon = async () => { + try { + await saveCoupon(data as string, session.user.id, Number(discount) , Number(numberOfCoupons)); + // refetch(); + setDisabled(true); + alert("Coupon code saved successfully"); + } catch (error) { + alert(getErrorMessage(error)); + } + }; + return ( - - - Create - Store - - - - - Coupon code - - Here you can create the coupon code and add it to the database - - - -
- - -
-
- - { - setDiscount(e.target.value); - }} - /> - { - setChecked(!checked); - }} - /> - -
-
- - - -
-
- - - - Coupon code - - You can see the generated coupon code below - - - -
- - -
-
- - - -
-
-
+ + + Create + Store + + + + + Coupon code + + Here you can create the coupon code and add it to the database + + + +
+ + +
+
+ + { + setDiscount(e.target.value); + }} + /> +
+
+ { + setChecked(!checked); + }} + /> + +
+
+ + { + setNumberOfCoupons(e.target.value); + }} + /> +
+
+ { + setNumberOfCouponsChecked(!numberOfCouponsChecked); + }} + /> + +
+
+ + { + refetch(); + setDisabled(false); + }} + /> + +
+
+ + + + Coupon code + You can see the generated coupon code below + + +
+ +
+ + +
+
+
+ + + +
+
+
); } diff --git a/src/components/admin/user-list.tsx b/src/components/admin/user-list.tsx index e921fd4..7cab81e 100644 --- a/src/components/admin/user-list.tsx +++ b/src/components/admin/user-list.tsx @@ -10,6 +10,7 @@ import { ChevronDownIcon, SearchIcon } from "lucide-react"; import { Input } from "../ui/input"; import debounce from "lodash.debounce"; import ChangeRole from "./change-role"; +import getUserCount from "@/app/actions/get-user-count"; export interface User { id: string; @@ -31,6 +32,7 @@ const UsersList: React.FC = ({ initialUsers, initialPage }) => { const [hasMore, setHasMore] = useState(true); const [searchQuery, setSearchQuery] = useState(""); const loader = useRef(null); + const [totalNumberOfUsers, setTotalNumberOfUsers] = useState(0); const fetchUsers = useCallback(async (page: number, query: string, isNewSearch: boolean) => { if (loading) return; @@ -92,6 +94,14 @@ const UsersList: React.FC = ({ initialUsers, initialPage }) => { return () => observer.disconnect(); }, [hasMore, loadMoreUsers]); + useEffect(() => { + async function getNumberOfUsers() { + const count = await getUserCount(); + setTotalNumberOfUsers(count ?? 0); // Use 0 if count is null + } + getNumberOfUsers(); + }, []); + return (
@@ -107,7 +117,7 @@ const UsersList: React.FC = ({ initialUsers, initialPage }) => {
- Users + Users {totalNumberOfUsers} Manage user roles and permissions. @@ -126,17 +136,8 @@ const UsersList: React.FC = ({ initialUsers, initialPage }) => {

- - - - - - - - + +
))} diff --git a/src/components/common/searchable-infinite-scroll-table.tsx b/src/components/common/searchable-infinite-scroll-table.tsx index 738d63e..586a45a 100644 --- a/src/components/common/searchable-infinite-scroll-table.tsx +++ b/src/components/common/searchable-infinite-scroll-table.tsx @@ -1,176 +1,175 @@ "use client"; import { useEffect, useRef, useState, useCallback } from "react"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Input } from "@/components/ui/input"; import { Loader2, Search } from "lucide-react"; import axios from "axios"; import debounce from "lodash.debounce"; - -interface TableData { - user: { - name: string; - email: string; - }; - usn?: string; - razorpayPaymentId: string; - contactNumber: string; - amount: number; +import getPaymentCount from "@/app/actions/get-payment-count"; + +interface PaymentData { + user: { + name: string; + email: string; + image: string; + forms: [{ photo: string }]; + }; + usn?: string; + razorpayPaymentId: string; + contactNumber: string; + amount: number; } -export function SearchableInfiniteScrollTable() { - const [data, setData] = useState([]); - const [filteredData, setFilteredData] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [page, setPage] = useState(1); - const [searchTerm, setSearchTerm] = useState(""); - const [hasMoreData, setHasMoreData] = useState(true); - const loaderRef = useRef(null); - const observerRef = useRef(null); - - const getPaymentDetails = async (page: number, query: string) => { - if (isLoading || !hasMoreData) return; - - setIsLoading(true); - try { - const response = await axios.get( - `/api/users/payment?page=${page}&search=${encodeURIComponent(query)}`, - ); - const users = response.data.users; - - if (users.length === 0) { - setHasMoreData(false); // No more data to load - } - - setData((prevData) => { - const newData = [...prevData, ...users]; - // Remove duplicates - const uniqueData = Array.from( - new Map( - newData.map((item) => [item.razorpayPaymentId, item]), - ).values(), - ); - return uniqueData; - }); - setPage((prevPage) => prevPage + 1); - } catch (error) { - console.error("Error fetching payment details:", error); - } finally { - setIsLoading(false); - } - }; - - const loadMoreData = () => { - if (searchTerm === "") { - getPaymentDetails(page, ""); - } - }; - - const fetchSearchResults = useCallback(async (query: string) => { - setPage(1); // Reset page number - setHasMoreData(true); // Reset hasMoreData - try { - const response = await axios.get( - `/api/users/payment?page=1&search=${encodeURIComponent(query)}`, - ); - const users = response.data.users; - setData(users); // Set new data from search - setFilteredData(users); // Set filtered data to the same as new data - } catch (error) { - console.error("Error fetching payment details:", error); - } - }, []); - - // eslint-disable-next-line react-hooks/exhaustive-deps - const debouncedFetch = useCallback( - debounce((query: string) => { - fetchSearchResults(query); - }, 500), - [], - ); - - const handleSearch = (event: React.ChangeEvent) => { - const value = event.target.value; - setSearchTerm(value); - debouncedFetch(value); // Use debounced fetch function - }; - - useEffect(() => { - loadMoreData(); // Initial load - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && !isLoading) { - loadMoreData(); +export function PaymentCards() { + const [data, setData] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [totalNumberOfPayments, setTotalNumberOfPayments] = useState(0); + const [page, setPage] = useState(1); + const [searchTerm, setSearchTerm] = useState(""); + const [hasMoreData, setHasMoreData] = useState(true); + const loaderRef = useRef(null); + const observerRef = useRef(null); + + const getPaymentDetails = async (page: number, query: string) => { + if (isLoading || !hasMoreData) return; + + setIsLoading(true); + try { + const response = await axios.get( + `/api/users/payment?page=${page}&search=${encodeURIComponent(query)}` + ); + const users = response.data.users; + + if (users.length === 0) { + setHasMoreData(false); + } + + setData((prevData) => { + const newData = [...prevData, ...users]; + const uniqueData = Array.from( + new Map(newData.map((item) => [item.razorpayPaymentId, item])).values() + ); + return uniqueData; + }); + setPage((prevPage) => prevPage + 1); + } catch (error) { + console.error("Error fetching payment details:", error); + } finally { + setIsLoading(false); } - }, - { threshold: 1.0 }, - ); + }; + + const loadMoreData = () => { + if (searchTerm === "") { + getPaymentDetails(page, ""); + } + }; + + const fetchSearchResults = useCallback(async (query: string) => { + setPage(1); + setHasMoreData(true); + try { + const response = await axios.get(`/api/users/payment?page=1&search=${encodeURIComponent(query)}`); + const users = response.data.users; + setData(users); + setFilteredData(users); + } catch (error) { + console.error("Error fetching payment details:", error); + } + }, []); - if (loaderRef.current) { - observer.observe(loaderRef.current); - } + const debouncedFetch = useCallback( + debounce((query: string) => { + fetchSearchResults(query); + }, 500), + [] + ); - return () => { - if (loaderRef.current) { - // eslint-disable-next-line react-hooks/exhaustive-deps - observer.unobserve(loaderRef.current); - } - observer.disconnect(); + const handleSearch = (event: React.ChangeEvent) => { + const value = event.target.value; + setSearchTerm(value); + debouncedFetch(value); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading]); - - return ( -
-
- - -
- - - - Name - Email - Payment ID - Amount - - - - {(searchTerm ? filteredData : data).map((item, index) => ( - - {item.user.name} - {item.user.email} - {item.razorpayPaymentId} - ₹{item.amount.toFixed(2)} - - ))} - -
- {searchTerm === "" && hasMoreData && ( -
- {isLoading && } + + useEffect(() => { + loadMoreData(); + async function getNumberOfPayments() { + const count = await getPaymentCount(); + setTotalNumberOfPayments(count ?? 0); + } + getNumberOfPayments(); + }, []); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && !isLoading) { + loadMoreData(); + } + }, + { threshold: 1.0 } + ); + + if (loaderRef.current) { + observer.observe(loaderRef.current); + } + + return () => { + if (loaderRef.current) { + observer.unobserve(loaderRef.current); + } + observer.disconnect(); + }; + }, [isLoading]); + + return ( +
+

Payments ({totalNumberOfPayments})

+
+ + +
+
+ {(searchTerm ? filteredData : data).map((item, index) => ( + + +
+ + + {item.user.name.charAt(0)} + +
+
+ + {item.user.name} +

{item.user.email}

+

ID: {item.razorpayPaymentId}

+

Amount: ₹{item.amount.toFixed(2)}

+
+
+ ))} +
+ {searchTerm === "" && hasMoreData && ( +
+ {isLoading && } +
+ )}
- )} -
- ); + ); } diff --git a/src/constants/index.ts b/src/constants/index.ts index 9d08021..e438240 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -5,6 +5,13 @@ export const basePrice = 980.39; export const initialdiscount = 0; export const sjecStudentPrice = 735.29; export const sjecFacultyPrice = 784.31; +export enum UserRole { + ADMIN = "ADMIN", + PARTICIPANT = "PARTICIPANT", + COORDINATOR = "COORDINATOR", +} + +export const ADMIN_USERS_PATH = "/admin/users"; export const speakers: Speaker[] = [ { id: 1, diff --git a/src/middleware.ts b/src/middleware.ts index 4c5cbfc..46bffdd 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -9,16 +9,10 @@ export async function middleware(request: NextRequest) { const url = request.nextUrl; if (url.pathname.startsWith("/admin")) { - if (token?.role !== "ADMIN") { + if (token?.role !== "ADMIN" && token?.role !== "COORDINATOR") { return NextResponse.redirect(new URL("/", request.url)); } } - // if (url.pathname.startsWith("/register")) { - // if (token?.role !== "ADMIN") { - // return NextResponse.redirect(new URL("/", request.url)); - // } - // } - } // See "Matching Paths" below to learn more diff --git a/src/types/index.ts b/src/types/index.ts index 4b46500..ecd13bf 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,6 @@ import { ReactNode } from "react"; -export type UserRoleType = "ADMIN" | "PARTICIPANT"; +export type UserRoleType = "ADMIN" | "PARTICIPANT" | "COORDINATOR"; export interface Speaker { id: number; @@ -45,7 +45,7 @@ export interface ResendEmailOptions { } export interface FormDataInterface { - designation: "student" | "faculty" | "employee"; + designation: "student" | "faculty" | "external"; foodPreference: "veg" | "non-veg"; name: string; email: string; diff --git a/src/utils/zod-schemas.ts b/src/utils/zod-schemas.ts index 83fbef0..508f32b 100644 --- a/src/utils/zod-schemas.ts +++ b/src/utils/zod-schemas.ts @@ -44,3 +44,10 @@ export const studentFormSchema = z.object({ idCard: z.string().min(1, { message: "ID Card is required for students." }), photo: z.string().min(1, { message: "Photo is required." }), }); + + +export const couponSchema = z.object({ + coupon: z.string().min(1, { message: "Coupon code is required" }), + createdById: z.string(), + discount: z.number().min(0).max(100).default(20), +});