From 1d1123ee139f484098f62d8eb2e6b55acf61aa93 Mon Sep 17 00:00:00 2001 From: Jasdeep Matharu Date: Sun, 15 Mar 2026 16:06:27 -0700 Subject: [PATCH 1/3] admin panel and fixed weird pricing glitch for dining halls --- app/admin/page.tsx | 246 ++++++++++++++++++++++++++++++++ app/api/admin/requests/route.ts | 71 +++++++++ app/dashboard/page.tsx | 16 ++- app/requests/create/page.tsx | 23 +-- app/requests/page.tsx | 11 +- prisma/schema.prisma | 1 + 6 files changed, 344 insertions(+), 24 deletions(-) create mode 100644 app/admin/page.tsx create mode 100644 app/api/admin/requests/route.ts diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..78e793e --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,246 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; + +interface RequestUser { + id: string; + name: string | null; + email: string; +} + +interface AdminRequest { + id: string; + location: string; + pointsRequested: number; + status: string; + message: string | null; + inPersonAllowed: boolean; + qrCodeAllowed: boolean; + selectedFulfillmentMode: string | null; + completedAt: string | null; + createdAt: string; + updatedAt: string; + requester: RequestUser; + donor: RequestUser | null; +} + +interface Stats { + total: number; + pending: number; + accepted: number; + completed: number; + declined: number; +} + +export default function AdminPage() { + const [requests, setRequests] = useState([]); + const [stats, setStats] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + setIsLoading(true); + const res = await fetch("/api/admin/requests"); + const data = await res.json(); + + if (!res.ok) { + setError(data.error || "Failed to load admin data"); + return; + } + + setRequests(data.requests); + setStats(data.stats); + setError(""); + } catch { + setError("An error occurred. Please try again."); + } finally { + setIsLoading(false); + } + }; + + // helper to get the color for each status + const getStatusColor = (status: string) => { + if (status === "pending") return "text-yellow-600"; + if (status === "accepted") return "text-green-600"; + if (status === "completed") return "text-blue-600"; + if (status === "declined") return "text-red-600"; + return "text-gray-600"; + }; + + if (isLoading) { + return ( +
+
+

Loading...

+
+
+ ); + } + + if (error) { + return ( +
+
+ + ← Back to Dashboard + +
+ {error === "Forbidden" ? "You do not have admin access." : error} +
+
+
+ ); + } + + return ( +
+
+ + {/* header */} +
+
+

+ Admin +

+

+ All Requests +

+
+
+ + + Back To Dashboard + +
+
+ + {/* stats section */} + {stats && ( +
+

+ Overview +

+
+
+

{stats.total}

+

Total

+
+
+

{stats.pending}

+

Pending

+
+
+

{stats.accepted}

+

Accepted

+
+
+

{stats.completed}

+

Completed

+
+
+

{stats.declined}

+

Declined

+
+
+
+ )} + + {/* requests list */} +
+

+ All requests ({requests.length}) +

+ + {requests.length === 0 ? ( + + + No requests found. + + + ) : ( +
+ {requests.map((req) => ( + + +
+
+ {req.location} + ID: {req.id} +
+ + {req.status} + +
+
+ + +

+ {req.pointsRequested}{" "} + points +

+ +

+ Requester:{" "} + {req.requester.name ?? req.requester.email} ({req.requester.email}) +

+ + {req.donor && ( +

+ Donor:{" "} + {req.donor.name ?? req.donor.email} ({req.donor.email}) +

+ )} + +

+ Fulfillment:{" "} + {req.inPersonAllowed && "In person"} + {req.inPersonAllowed && req.qrCodeAllowed && " & "} + {req.qrCodeAllowed && "QR code"} + {req.selectedFulfillmentMode && ` — selected: ${req.selectedFulfillmentMode.replace("_", " ")}`} +

+ + {req.message && ( +

+ “{req.message}” +

+ )} + +

+ Created: {new Date(req.createdAt).toLocaleString()} + {req.completedAt && ` · Completed: ${new Date(req.completedAt).toLocaleString()}`} +

+
+
+ ))} +
+ )} +
+ +
+
+ ); +} diff --git a/app/api/admin/requests/route.ts b/app/api/admin/requests/route.ts new file mode 100644 index 0000000..6f94f2b --- /dev/null +++ b/app/api/admin/requests/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from "next/server"; +import { getCurrentUser } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + try { + const user = await getCurrentUser(); + + if (!user || !user.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // get the user from the database to check if they are an admin + const dbUser = await prisma.user.findUnique({ + where: { id: user.id }, + }); + + if (!dbUser || !dbUser.isAdmin) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + // get all requests with requester and donor info + const requests = await prisma.request.findMany({ + include: { + requester: { + select: { + id: true, + name: true, + email: true, + }, + }, + donor: { + select: { + id: true, + name: true, + email: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + }); + + // count requests by status for the stats section + let pendingCount = 0; + let acceptedCount = 0; + let completedCount = 0; + let declinedCount = 0; + + for (const req of requests) { + if (req.status === "pending") pendingCount++; + if (req.status === "accepted") acceptedCount++; + if (req.status === "completed") completedCount++; + if (req.status === "declined") declinedCount++; + } + + const stats = { + total: requests.length, + pending: pendingCount, + accepted: acceptedCount, + completed: completedCount, + declined: declinedCount, + }; + + return NextResponse.json({ requests, stats }); + } catch (error) { + console.error("Error fetching admin requests:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 075c47a..bc27330 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -9,6 +9,7 @@ import { Calculator, BookOpen, Plug, + ShieldCheck, } from "lucide-react"; export default async function DashboardPage() { @@ -20,13 +21,15 @@ export default async function DashboardPage() { const dbUser = await prisma.user.findUnique({ where: { id: user.id }, + select: { id: true, isAdmin: true }, }); if (!dbUser) { redirect("/auth/logout-stale"); } - const actions = [ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const actions: { href: string; title: string; description: string; icon: any }[] = [ { href: "/requests/create", title: "Create Request", @@ -57,7 +60,16 @@ export default async function DashboardPage() { description: "Link and monitor your GET session", icon: Plug, }, - ] as const; + ]; + + if (dbUser?.isAdmin) { + actions.push({ + href: "/admin", + title: "Admin Panel", + description: "Monitor all system requests", + icon: ShieldCheck, + }); + } return (
diff --git a/app/requests/create/page.tsx b/app/requests/create/page.tsx index 57f49e3..742ab3d 100644 --- a/app/requests/create/page.tsx +++ b/app/requests/create/page.tsx @@ -32,24 +32,13 @@ const getDayKey = () => { const getMealPeriod = () => { const now = new Date(); const time = now.getHours() + now.getMinutes() / 60; - - if (time >= 7 && time < 11){ //7AM-2PM - return 'breakfast'; - } - - if (time >= 11.5 && time < 14){ //11:30AM-2PM - return 'lunch'; - } - - if (time >= 17 && time < 20){ //5PM-8PM - return 'dinner'; - } - - if (time >= 20 && time < 23){ //8PM-11PM - return 'lateNight'; - } - return 'continuousDining'; + if (time >= 7 && time < 11) return 'breakfast'; // 7:00–11:00 AM + if (time >= 11 && time < 14) return 'lunch'; // 11:00 AM–2:00 PM (covers 11–11:30 gap) + if (time >= 14 && time < 17) return 'dinner'; // 2:00–5:00 PM (show upcoming dinner price) + if (time >= 17 && time < 20) return 'dinner'; // 5:00–8:00 PM + if (time >= 20 && time < 24) return 'lateNight'; // 8:00 PM–midnight + return 'breakfast'; // midnight–7 AM (dining halls closed anyway) }; const isCurrentlyOpen = (schedule: any) => { diff --git a/app/requests/page.tsx b/app/requests/page.tsx index eff6afd..1e45573 100644 --- a/app/requests/page.tsx +++ b/app/requests/page.tsx @@ -41,11 +41,12 @@ const getDayKey = () => { const getMealPeriod = () => { const now = new Date(); const time = now.getHours() + now.getMinutes() / 60; - if (time >= 7 && time < 11) return "breakfast"; - if (time >= 11.5 && time < 14) return "lunch"; - if (time >= 17 && time < 20) return "dinner"; - if (time >= 20 && time < 23) return "lateNight"; - return "continuousDining"; + if (time >= 7 && time < 11) return "breakfast"; // 7:00-11:00 AM + if (time >= 11 && time < 14) return "lunch"; // 11:00 AM-2:00 PM (covers 11-11:30 gap) + if (time >= 14 && time < 17) return "dinner"; // 2:00-5:00 PM (show upcoming dinner price) + if (time >= 17 && time < 20) return "dinner"; // 5:00-8:00 PM + if (time >= 20 && time < 24) return "lateNight"; // 8:00 PM-midnight + return "breakfast"; // midnight-7 AM (dining halls closed anyway) }; const isCurrentlyOpen = (schedule: any) => { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 099ed94..f66b12b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,6 +16,7 @@ model User { email String @unique emailVerified DateTime? phone String? + isAdmin Boolean @default(false) image String? qrCodeExpiryMinutes Int @default(60) autoPullQrEnabled Boolean @default(false) From 94958db8540f6fd2348d0f4e8e5345b2686d254a Mon Sep 17 00:00:00 2001 From: Jasdeep Matharu Date: Tue, 17 Mar 2026 16:07:59 -0700 Subject: [PATCH 2/3] admin_panel migration --- prisma/migrations/20260317230606_admin_panel/migration.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 prisma/migrations/20260317230606_admin_panel/migration.sql diff --git a/prisma/migrations/20260317230606_admin_panel/migration.sql b/prisma/migrations/20260317230606_admin_panel/migration.sql new file mode 100644 index 0000000..7db3f39 --- /dev/null +++ b/prisma/migrations/20260317230606_admin_panel/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "isAdmin" BOOLEAN NOT NULL DEFAULT false; From 7fc7dafdb0c23590ed116bc75c0c1f695eee15b6 Mon Sep 17 00:00:00 2001 From: Jasdeep Matharu <160673412+jasmath03@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:59:48 -0700 Subject: [PATCH 3/3] Update page.tsx --- app/admin/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 78e793e..50bb2b9 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -95,7 +95,7 @@ export default function AdminPage() {
- ← Back to Dashboard + Back to Dashboard
{error === "Forbidden" ? "You do not have admin access." : error}