diff --git a/app/admin/applications/page.tsx b/app/admin/applications/page.tsx index 99d42c5..caeb291 100644 --- a/app/admin/applications/page.tsx +++ b/app/admin/applications/page.tsx @@ -1,40 +1,197 @@ +"use client"; + import Link from "next/link"; -import { getApplications } from "@/services/mockApplications"; +import { useEffect, useState, useCallback } from "react"; + +type ApplicationStatus = "pending" | "approved" | "rejected"; +interface Application { + id: string; + type: string; + status: ApplicationStatus; + submitterName: string; + submitterEmail: string; + createdAt: string; +} + +const STATUS_OPTIONS: { label: string; value: string }[] = [ + { label: "All Statuses", value: "" }, + { label: "Pending", value: "pending" }, + { label: "Approved", value: "approved" }, + { label: "Rejected", value: "rejected" }, +]; + +const STATUS_STYLES: Record = { + pending: "bg-amber-50 text-amber-700 ring-1 ring-amber-200", + approved: "bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200", + rejected: "bg-red-50 text-red-600 ring-1 ring-red-200", +}; export default function ApplicationsPage() { - const applications = getApplications(); + const [applications, setApplications] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [statusFilter, setStatusFilter] = useState(""); + const [typeFilter, setTypeFilter] = useState(""); + const [typeInput, setTypeInput] = useState(""); + + const fetchApplications = useCallback(async () => { + setLoading(true); + setError(null); + try { + const params = new URLSearchParams(); + if (statusFilter) params.set("status", statusFilter); + if (typeFilter) params.set("type", typeFilter); + + const res = await fetch(`/api/admin/applications?${params.toString()}`); + if (!res.ok) { + const body = await res.json(); + throw new Error(body.error ?? "Failed to load applications"); + } + const { data } = await res.json(); + setApplications(data); + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }, [statusFilter, typeFilter]); + + useEffect(() => { + const timer = setTimeout(() => setTypeFilter(typeInput), 400); + return () => clearTimeout(timer); + }, [typeInput]); + + useEffect(() => { + fetchApplications(); + }, [fetchApplications]); return ( -
-

Applications

+
+
+

Applications

+

+ Review and manage submitted applications +

+
+ +
+ setTypeInput(e.target.value)} + placeholder="Filter by type..." + className="text-sm border border-gray-200 rounded-md px-3 py-2 bg-white text-gray-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + + + + {(statusFilter || typeInput) && ( + + )} +
+ + {error && ( +
+ {error} +
+ )} -
- - +
+
+ - - - - + {["Name", "Email", "Type", "Status", "Submitted"].map((h) => ( + + ))} - - - {applications.map((app) => ( - - + {loading ? ( + Array.from({ length: 5 }).map((_, i) => ( + + {Array.from({ length: 5 }).map((_, j) => ( + + ))} + + )) + ) : applications.length === 0 ? ( + + - - - - ))} + ) : ( + applications.map((app) => ( + + + + + + + + )) + )}
NameEmailRoleStatus + {h} +
- - {app.name} - +
+
+
+

No applications found

+ {(statusFilter || typeFilter) && ( +

+ Try adjusting your filters +

+ )}
{app.email}{app.role}{app.status}
+ + {app.submitterName} + + + {app.submitterEmail} + + {app.type} + + + {app.status} + + + {new Date(app.createdAt).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + })} +
+ + {!loading && applications.length > 0 && ( +

+ Showing {applications.length} application{applications.length !== 1 ? "s" : ""} +

+ )}
); } diff --git a/app/api/admin/applications/[id]/route.ts b/app/api/admin/applications/[id]/route.ts new file mode 100644 index 0000000..fe26622 --- /dev/null +++ b/app/api/admin/applications/[id]/route.ts @@ -0,0 +1,87 @@ +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient, Role } from "@/generated/prisma/client"; +import { auth } from "@/auth"; + +const prisma = new PrismaClient(); + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } }, +) { + const session = await auth(); + + if (!session?.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const dbUser = await prisma.user.findUnique({ + where: { email: session.user.email }, + select: { role: true }, + }); + + if ( + !dbUser || + (dbUser.role !== Role.REVIEWER && dbUser.role !== Role.SUPER_ADMIN) + ) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const application = await prisma.application.findUnique({ + where: { id: params.id }, + }); + + if (!application) { + return NextResponse.json({ error: "Not found" }, { status: 404 }); + } + + return NextResponse.json({ data: application }, { status: 200 }); +} + + +export async function PATCH( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await auth(); + + if (!session?.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const dbUser = await prisma.user.findUnique({ + where: { email: session.user.email }, + select: { role: true }, + }); + + if ( + !dbUser || + (dbUser.role !== Role.REVIEWER && + dbUser.role !== Role.SUPER_ADMIN) + ) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { status } = await request.json(); + + if (status !== "approved" && status !== "rejected") { + return NextResponse.json( + { error: "Invalid status" }, + { status: 400 } + ); + } + + const updated = await prisma.application.update({ + where: { id: params.id }, + data: { status }, + }); + + return NextResponse.json({ data: updated }, { status: 200 }); + + } catch (err: any) { + return NextResponse.json( + { error: "Failed to update application" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/admin/applications/route.ts b/app/api/admin/applications/route.ts index ccaf148..918f798 100644 --- a/app/api/admin/applications/route.ts +++ b/app/api/admin/applications/route.ts @@ -1,11 +1,40 @@ -import { NextResponse } from "next/server"; -import { PrismaClient } from "@/generated/prisma/client"; +import { NextRequest, NextResponse } from "next/server"; +import { PrismaClient, Role } from "@/generated/prisma/client"; +import { auth } from "@/auth"; const prisma = new PrismaClient(); -export async function GET() { +export async function GET(request: NextRequest) { try { + const session = await auth(); + + if (!session?.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const dbUser = await prisma.user.findUnique({ + where: { email: session.user.email }, + select: { role: true }, + }); + + if ( + !dbUser || + (dbUser.role !== Role.REVIEWER && + dbUser.role !== Role.SUPER_ADMIN) + ) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const { searchParams } = new URL(request.url); + const type = searchParams.get("type"); + const status = searchParams.get("status"); + + const where: any = {}; + if (type) where.type = type; + if (status) where.status = status; + const applications = await prisma.application.findMany({ + where, select: { id: true, type: true, @@ -19,6 +48,7 @@ export async function GET() { }); return NextResponse.json({ data: applications }, { status: 200 }); + } catch (err) { console.error(err); return NextResponse.json(