diff --git a/src/app/(main)/dashboard/applications/[applicationId]/page.tsx b/src/app/(main)/dashboard/applications/[applicationId]/page.tsx new file mode 100644 index 0000000..e890e20 --- /dev/null +++ b/src/app/(main)/dashboard/applications/[applicationId]/page.tsx @@ -0,0 +1,40 @@ +import connectMongoDB from "@/repository/mongoose"; +import Application from "@/repository/models/application"; +import { notFound } from "next/navigation"; +import ApplicationView, { type ApplicationDetails } from "./view"; + +export const dynamic = "force-dynamic"; + +export default async function Page({ params }: { params: { applicationId: string } }) { + if (!params?.applicationId) return notFound(); + + await connectMongoDB(); + const app = (await Application.findById(params.applicationId).lean()) as any; + if (!app) return notFound(); + + const application: ApplicationDetails = { + _id: String(app._id), + firstName: app.firstName, + lastName: app.lastName, + age: app.age, + phoneNumber: app.phoneNumber, + email: app.email, + status: app.status, + processedBy: app.processedBy, + processedAt: app.processedAt?.toISOString?.() ?? undefined, + isFromMontreal: app.isFromMontreal, + country: app.country, + city: app.city, + school: app.school, + discipline: app.discipline, + shirtSize: app.shirtSize, + dietaryRestrictions: Array.isArray(app.dietaryRestrictions) ? app.dietaryRestrictions : [], + dietaryRestrictionsDescription: app.dietaryRestrictionsDescription, + hackathons: app.hackathons, + github: app.github, + linkedin: app.linkedin, + hasResume: Boolean(app?.resume?.id), + }; + + return ; +} diff --git a/src/app/(main)/dashboard/applications/[applicationId]/view.tsx b/src/app/(main)/dashboard/applications/[applicationId]/view.tsx new file mode 100644 index 0000000..9005bbb --- /dev/null +++ b/src/app/(main)/dashboard/applications/[applicationId]/view.tsx @@ -0,0 +1,261 @@ +"use client"; + +import * as React from "react"; +import { CheckCircle2, Hourglass, XCircle } from "lucide-react"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { useRouter } from "next/navigation"; + +export type ApplicationDetails = { + _id: string; + firstName: string; + lastName: string; + age?: string; + phoneNumber?: string; + email: string; + status: string; + processedBy?: string; + processedAt?: string; + isFromMontreal?: boolean; + country?: string; + city?: string; + school?: string; + discipline?: string; + shirtSize?: string; + dietaryRestrictions?: string[]; + dietaryRestrictionsDescription?: string; + hackathons?: number; + github?: string; + linkedin?: string; + hasResume?: boolean; +}; + +export default function ApplicationView({ + application: initial, + adminEmail: initialAdminEmail, +}: { + application: ApplicationDetails; + adminEmail: string | null; +}) { + const router = useRouter(); + + const [application, setApplication] = React.useState(initial); + const [adminEmail, setAdminEmail] = React.useState(initialAdminEmail); + const [error, setError] = React.useState(null); + const [isSaving, setIsSaving] = React.useState(null); + + React.useEffect(() => { + let active = true; + async function loadAdmin() { + try { + if (adminEmail) return; + const meRes = await fetch(`/api/auth-token/me`, { cache: "no-store" }); + if (!meRes.ok) return; + const meJson = await meRes.json(); + if (!active) return; + setAdminEmail(meJson?.data?.email ?? null); + } catch {} + } + loadAdmin(); + return () => { + active = false; + }; + }, [adminEmail]); + + async function updateStatus(action: "admit" | "waitlist" | "reject") { + try { + setIsSaving(action); + setError(null); + const res = await fetch(`/api/status/${application._id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action, adminEmail }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `Update failed with ${res.status}`); + } + const json = await res.json(); + const newStatus = json?.data as string; + setApplication((prev) => ({ ...prev, status: newStatus })); + } catch (e: any) { + setError(e?.message ?? "Failed to update status"); + } finally { + setIsSaving(null); + } + } + + async function checkIn() { + try { + setIsSaving("checkin"); + setError(null); + const res = await fetch(`/api/check-in/${application._id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status: "Checked-in" }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `Check-in failed with ${res.status}`); + } + const json = await res.json(); + const newStatus = json?.data?.status ?? "Checked-in"; + setApplication((prev) => ({ ...prev, status: newStatus })); + } catch (e: any) { + setError(e?.message ?? "Failed to check in"); + } finally { + setIsSaving(null); + } + } + + return ( +
+
+ + + Applicant + Review application and take action. + + + {error ? ( +
{error}
+ ) : ( + <> +
+
+
+ {application.firstName} {application.lastName} +
+
{application.email}
+
+ {application.status} +
+ + + +
+
+
School
+
{application.school || "—"}
+
+
+
Discipline
+
{application.discipline || "—"}
+
+
+
Country
+
{application.country || "—"}
+
+
+
City
+
{application.city || "—"}
+
+
+
Shirt size
+
{application.shirtSize || "—"}
+
+
+
Hackathons
+
{application.hackathons ?? "—"}
+
+
+
Dietary restrictions
+
+ {application.dietaryRestrictions && application.dietaryRestrictions.length > 0 + ? application.dietaryRestrictions.join(", ") + : "—"} +
+
+
+
From Montreal
+
{application.isFromMontreal ? "Yes" : "No"}
+
+
+
Resume
+
+ {application.hasResume ? ( + + Download Resume + + ) : ( + "—" + )} +
+
+
+ + + + {(() => { + const status = application.status; + const isSubmitted = status === "Submitted"; + const isConfirmed = status === "Confirmed"; + const isCheckedIn = status === "CheckedIn" || status === "Checked-in"; + + if (isSubmitted) { + return ( +
+ + + +
+ ); + } + + if (isConfirmed) { + return ( +
+ +
+ ); + } + + if (isCheckedIn) { + return ( +
+ +
+ ); + } + + return null; + })()} + + )} +
+
+
+
+ +
+
+ ); +} diff --git a/src/app/(main)/dashboard/applications/_components/columns.tsx b/src/app/(main)/dashboard/applications/_components/columns.tsx index 21cf1de..6649f5e 100644 --- a/src/app/(main)/dashboard/applications/_components/columns.tsx +++ b/src/app/(main)/dashboard/applications/_components/columns.tsx @@ -1,5 +1,7 @@ +import * as React from "react"; import { ColumnDef } from "@tanstack/react-table"; -import { EllipsisVertical, Trash2, Eye } from "lucide-react"; +import { EllipsisVertical, Trash2, Eye, UserPlus } from "lucide-react"; +import { toast } from "sonner"; import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"; import { Badge } from "@/components/ui/badge"; @@ -11,6 +13,16 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; export type ApplicationTableRow = { _id: string; @@ -21,87 +33,211 @@ export type ApplicationTableRow = { processedBy?: string; }; -export const applicationsColumns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( -
- table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> -
- ), - cell: ({ row }) => ( -
- row.toggleSelected(!!value)} - aria-label="Select row" - /> -
- ), - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "firstName", - header: ({ column }) => , - cell: ({ row }) => ( - - {row.original.firstName} {row.original.lastName} - - ), - enableHiding: false, - }, - { - accessorKey: "email", - header: ({ column }) => , - cell: ({ row }) => {row.original.email}, - }, - { - accessorKey: "status", - header: ({ column }) => , - cell: ({ row }) => {row.original.status}, - }, - { - accessorKey: "processedBy", - header: ({ column }) => , - cell: ({ row }) => {row.original.processedBy ?? "Not processed"}, - enableSorting: false, - }, - { - id: "actions", - cell: ({ row }) => ( - - - + - - - { - // placeholder for delete action - console.log("Delete", row.original._id); - }} - > - Delete - - { - // placeholder for review action - console.log("Review", row.original._id); - }} - > - Review - - - - ), - enableSorting: false, - }, -]; + + + + ); +} + +export function getApplicationsColumns(isSuperAdmin: boolean): ColumnDef[] { + return [ + { + id: "select", + header: ({ table }) => ( +
+ table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> +
+ ), + cell: ({ row }) => ( +
+ row.toggleSelected(!!value)} + aria-label="Select row" + /> +
+ ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "firstName", + header: ({ column }) => , + cell: ({ row }) => ( + + {row.original.firstName} {row.original.lastName} + + ), + enableHiding: false, + }, + { + accessorKey: "email", + header: ({ column }) => , + cell: ({ row }) => {row.original.email}, + }, + { + accessorKey: "status", + header: ({ column }) => , + cell: ({ row }) => {row.original.status}, + }, + { + accessorKey: "processedBy", + header: ({ column }) => , + cell: ({ row }) => {row.original.processedBy ?? "Not processed"}, + enableSorting: false, + }, + { + id: "actions", + cell: ({ row }) => { + const [assignOpen, setAssignOpen] = React.useState(false); + return ( + <> + + + + + + {isSuperAdmin && ( + { + e.preventDefault(); + setAssignOpen(true); + }} + > + Assign to admin + + )} + { + window.open(`/dashboard/applications/${row.original._id}`, "_blank", "noopener,noreferrer"); + }} + > + Review + + { + console.log("Delete", row.original._id); + }} + > + Delete + + + + {isSuperAdmin && ( + { + row.original.processedBy = email; + }} + /> + )} + + ); + }, + enableSorting: false, + }, + ]; +} diff --git a/src/app/(main)/dashboard/applications/_components/filters.tsx b/src/app/(main)/dashboard/applications/_components/filters.tsx new file mode 100644 index 0000000..09a03c6 --- /dev/null +++ b/src/app/(main)/dashboard/applications/_components/filters.tsx @@ -0,0 +1,70 @@ +"use client"; + +import * as React from "react"; +import type { Table } from "@tanstack/react-table"; + +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +import type { ApplicationTableRow } from "./columns"; + +type ApplicationsFiltersProps = { + table: Table; +}; + +const STATUS_OPTIONS = [ + "Unverified", + "Incomplete", + "Submitted", + "Admitted", + "Waitlisted", + "Refused", + "Confirmed", + "Declined", + "CheckedIn", +]; + +export default function ApplicationsFilters({ table }: ApplicationsFiltersProps) { + const ALL_VALUE = "__all__"; + const [email, setEmail] = React.useState(""); + const [status, setStatus] = React.useState(ALL_VALUE); + + React.useEffect(() => { + table.getColumn("email")?.setFilterValue(email || undefined); + }, [email, table]); + + React.useEffect(() => { + table.getColumn("status")?.setFilterValue(status === ALL_VALUE ? undefined : status); + }, [status, table]); + + function reset() { + setEmail(""); + setStatus(ALL_VALUE); + } + + return ( +
+
+ setEmail(e.target.value)} /> +
+
+ +
+ +
+ ); +} diff --git a/src/app/(main)/dashboard/applications/_components/table-cards.tsx b/src/app/(main)/dashboard/applications/_components/table-cards.tsx index 3ff24df..396c475 100644 --- a/src/app/(main)/dashboard/applications/_components/table-cards.tsx +++ b/src/app/(main)/dashboard/applications/_components/table-cards.tsx @@ -1,7 +1,7 @@ "use client"; +import { Download, UserPlus } from "lucide-react"; import * as React from "react"; -import { Download } from "lucide-react"; import { DataTable } from "@/components/data-table/data-table"; import { DataTablePagination } from "@/components/data-table/data-table-pagination"; @@ -9,63 +9,97 @@ import { DataTableViewOptions } from "@/components/data-table/data-table-view-op import { Button } from "@/components/ui/button"; import { Card, CardHeader, CardTitle, CardContent, CardDescription, CardAction } from "@/components/ui/card"; import { useDataTableInstance } from "@/hooks/use-data-table-instance"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { applicationsColumns, type ApplicationTableRow } from "./columns"; +import { getApplicationsColumns, type ApplicationTableRow } from "./columns"; +import ApplicationsFilters from "./filters"; -export function TableCards() { - const [data, setData] = React.useState([]); - const [isLoading, setIsLoading] = React.useState(true); - const [error, setError] = React.useState(null); +type TableCardsProps = { + initialData: ApplicationTableRow[]; + isSuperAdmin: boolean; +}; - React.useEffect(() => { - const controller = new AbortController(); +export function TableCards({ initialData, isSuperAdmin }: TableCardsProps) { + const table = useDataTableInstance({ + data: initialData ?? [], + columns: getApplicationsColumns(isSuperAdmin), + getRowId: (row) => row._id, + }); + const [bulkOpen, setBulkOpen] = React.useState(false); + const [adminEmails, setAdminEmails] = React.useState([]); + const [selectedEmail, setSelectedEmail] = React.useState(""); + const [assigning, setAssigning] = React.useState(false); + const [, forceRerender] = React.useState(0); + + const selectedRows = table.getSelectedRowModel().rows; + const selectedIds = React.useMemo(() => selectedRows.map((r) => r.original._id), [selectedRows]); - async function fetchApplications() { + React.useEffect(() => { + if (!bulkOpen) return; + let cancelled = false; + (async () => { try { - setIsLoading(true); - setError(null); - const res = await fetch(`/api/users`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - cache: "no-store", - signal: controller.signal, - }); + const res = await fetch("/api/admin/get-emails"); + if (!res.ok) throw new Error("Failed to load admin emails"); + const json = await res.json(); + const emails: string[] = Array.isArray(json?.data) ? json.data : []; + if (!cancelled) setAdminEmails(emails); + } catch (_e) { + toast.error("Failed to load admins"); + } + })(); + return () => { + cancelled = true; + }; + }, [bulkOpen]); - if (!res.ok) { - const text = await res.text(); - throw new Error(text || `Request failed with ${res.status}`); + async function handleBulkAssign() { + if (!selectedIds.length) { + toast.error("No applications selected"); + return; + } + if (!selectedEmail) { + toast.error("Please select an admin"); + return; + } + setAssigning(true); + try { + const promise = fetch("/api/admin/assign-applications", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ selectedAdminEmail: selectedEmail, selectedApplicants: selectedIds }), + }).then(async (r) => { + if (!r.ok) { + const err = await r.json().catch(() => ({})); + throw new Error(err?.message || "Failed to assign"); } - - const json = await res.json(); - const applications = (json?.data ?? []) as any[]; - const rows: ApplicationTableRow[] = applications.map((a) => ({ - _id: a._id, - firstName: a.firstName, - lastName: a.lastName, - email: a.email, - status: a.status, - processedBy: a.processedBy, - })); - setData(rows); - } catch (e: any) { - if (e?.name === "AbortError") return; - setError(e?.message ?? "Failed to load applications"); - } finally { - setIsLoading(false); + }); + await toast.promise(promise, { + loading: "Assigning selected applications...", + success: "Applications assigned", + error: (e) => e.message || "Failed to assign", + }); + // Optimistic update for processedBy + for (const row of selectedRows) { + (row.original as any).processedBy = selectedEmail; } + table.resetRowSelection(); + forceRerender((n) => n + 1); + setBulkOpen(false); + } finally { + setAssigning(false); } - - fetchApplications(); - return () => controller.abort(); - }, []); - - const table = useDataTableInstance({ - data, - columns: applicationsColumns, - getRowId: (row) => row._id, - }); + } return (
@@ -74,28 +108,66 @@ export function TableCards() { Applications Track and manage your applications and their status. -
- - +
+ +
+ + {isSuperAdmin && ( + + )} + +
- {isLoading ? ( -
Loading applications…
- ) : error ? ( -
{error}
- ) : ( - - )} +
+ {isSuperAdmin && ( + + + + Assign {selectedIds.length} selected + Select an admin to assign the selected applications. + +
+ + +
+ + + + +
+
+ )}
); } diff --git a/src/app/(main)/dashboard/applications/page.tsx b/src/app/(main)/dashboard/applications/page.tsx index 4b3a216..7c6bc04 100644 --- a/src/app/(main)/dashboard/applications/page.tsx +++ b/src/app/(main)/dashboard/applications/page.tsx @@ -1,15 +1,89 @@ -import { InsightCards } from "./_components/insight-cards"; -import { OperationalCards } from "./_components/operational-cards"; -import { OverviewCards } from "./_components/overview-cards"; import { TableCards } from "./_components/table-cards"; +import { type ApplicationTableRow } from "./_components/columns"; +import connectMongoDB from "@/repository/mongoose"; +import Application from "@/repository/models/application"; +import Admin from "@/repository/models/admin"; +import { cookies } from "next/headers"; +import { COOKIE_NAME, verifyAuthToken } from "@/lib/auth-token"; + +export const dynamic = "force-dynamic"; + +async function getApplicationsSSR(): Promise { + try { + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; + if (!token) { + return []; + } + + const payload = await verifyAuthToken(token); + if (!payload) { + return []; + } + + await connectMongoDB(); + + // Super admin can see all applications + if (payload.isSuperAdmin) { + const apps = await Application.find({}, "email firstName lastName status processedBy").lean(); + return (apps ?? []).map((a: any) => ({ + _id: String(a._id), + firstName: a.firstName, + lastName: a.lastName, + email: a.email, + status: a.status, + processedBy: a.processedBy, + })); + } + + // Non-super admins: only see assigned applications + const admin = await Admin.findById(payload.adminId).select("assignedApplications").lean(); + if (!admin) { + return []; + } + + const assignedApplications: string[] = (admin as any).assignedApplications || []; + if (!assignedApplications.length) { + return []; + } + + const apps = await Application.find( + { _id: { $in: assignedApplications } }, + "email firstName lastName status processedBy", + ).lean(); + + return (apps ?? []).map((a: any) => ({ + _id: String(a._id), + firstName: a.firstName, + lastName: a.lastName, + email: a.email, + status: a.status, + processedBy: a.processedBy, + })); + } catch { + return []; + } +} + +async function getIsSuperAdminSSR(): Promise { + try { + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; + if (!token) return false; + const payload = await verifyAuthToken(token); + return !!payload?.isSuperAdmin; + } catch { + return false; + } +} + +export default async function Page() { + const initialData = await getApplicationsSSR(); + const isSuperAdmin = await getIsSuperAdminSSR(); -export default function Page() { return (
- {/* */} - {/* - */} - +
); } diff --git a/src/app/api/(group)/admin/assign-applications/route.ts b/src/app/api/(group)/admin/assign-applications/route.ts index 268ebf8..3594d70 100644 --- a/src/app/api/(group)/admin/assign-applications/route.ts +++ b/src/app/api/(group)/admin/assign-applications/route.ts @@ -1,52 +1,67 @@ -import type { NextRequest } from 'next/server' +import type { NextRequest } from "next/server"; -import connectMongoDB from '@/repository/mongoose' -import { sendErrorResponse, sendSuccessResponse } from '@/repository/response' -import Admin from '@/repository/models/admin' -import Application from '@/repository/models/application' +import connectMongoDB from "@/repository/mongoose"; +import { sendErrorResponse, sendSuccessResponse } from "@/repository/response"; +import Admin from "@/repository/models/admin"; +import Application from "@/repository/models/application"; +import { COOKIE_NAME, verifyAuthToken } from "@/lib/auth-token"; export const POST = async (req: NextRequest) => { try { - const { selectedAdminEmail, selectedApplicants } = await req.json() + const token = req.cookies.get(COOKIE_NAME)?.value; + if (!token) { + return sendErrorResponse("Unauthorized", null, 401); + } + + const payload = await verifyAuthToken(token); + if (!payload) { + return sendErrorResponse("Unauthorized", null, 401); + } + + if (!payload.isSuperAdmin) { + return sendErrorResponse("Forbidden", null, 403); + } + + const { selectedAdminEmail, selectedApplicants } = await req.json(); - console.log('Selected Admin Email', selectedAdminEmail) - console.log('Selected Applicants', selectedApplicants) + console.log("Selected Admin Email", selectedAdminEmail); + console.log("Selected Applicants", selectedApplicants); if (!(selectedAdminEmail && selectedApplicants)) { - return sendErrorResponse('Admin email or applicants list is not present in the body', null, 400) + return sendErrorResponse("Admin email or applicants list is not present in the body", null, 400); } - await connectMongoDB() + await connectMongoDB(); - const admin = await Admin.findOne({ email: selectedAdminEmail }) + const admin = await Admin.findOne({ email: selectedAdminEmail }); if (!admin) { - return sendErrorResponse('Admin not found', null, 404) + return sendErrorResponse("Admin not found", null, 404); } // Create a new array with unique values - const existingApplications = admin.assignedApplications || [] - const uniqueApplications = Array.from(new Set([...existingApplications, ...selectedApplicants])) + const existingApplications = admin.assignedApplications || []; + const uniqueApplications = Array.from(new Set([...existingApplications, ...selectedApplicants])); - admin.assignedApplications = uniqueApplications + admin.assignedApplications = uniqueApplications; for (const applicationId of selectedApplicants) { - const applicant = await Application.findById(applicationId) + const applicant = await Application.findById(applicationId); if (!applicant) { - return sendErrorResponse('Applicant not found', null, 404) + return sendErrorResponse("Applicant not found", null, 404); } - if (applicant.processedBy !== selectedAdminEmail && applicant.processedBy !== 'Not processed') { + if (applicant.processedBy !== selectedAdminEmail && applicant.processedBy !== "Not processed") { // Find previous admin and remove the application from their assignedApplications - const previousAdmin = await Admin.findOne({ email: applicant.processedBy }) + const previousAdmin = await Admin.findOne({ email: applicant.processedBy }); if (previousAdmin) { previousAdmin.assignedApplications = previousAdmin.assignedApplications.filter( - (appId: string) => appId !== applicationId - ) + (appId: string) => appId !== applicationId, + ); - await previousAdmin.save() + await previousAdmin.save(); } } @@ -54,20 +69,20 @@ export const POST = async (req: NextRequest) => { const updateApplicant = await Application.findByIdAndUpdate( applicationId, { - $set: { processedBy: selectedAdminEmail } + $set: { processedBy: selectedAdminEmail }, }, - { new: true } - ) + { new: true }, + ); if (!updateApplicant) { - return sendErrorResponse('Failed to update applicant', null, 500) + return sendErrorResponse("Failed to update applicant", null, 500); } } - await admin.save() + await admin.save(); - return sendSuccessResponse('Applications successfully assigned', admin.assignedApplications, 200) + return sendSuccessResponse("Applications successfully assigned", admin.assignedApplications, 200); } catch (error) { - return sendErrorResponse('Failed to assign applications', error, 500) + return sendErrorResponse("Failed to assign applications", error, 500); } -} +}; diff --git a/src/app/api/(group)/users/resume/[userId]/route.ts b/src/app/api/(group)/users/resume/[userId]/route.ts index 55fa107..d5a68d8 100644 --- a/src/app/api/(group)/users/resume/[userId]/route.ts +++ b/src/app/api/(group)/users/resume/[userId]/route.ts @@ -1,75 +1,75 @@ -import type { NextRequest} from 'next/server'; -import { NextResponse } from 'next/server' +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; -import mongoose from 'mongoose' -import { GridFSBucket } from 'mongodb' +import mongoose from "mongoose"; +import { GridFSBucket } from "mongodb"; -import { sendErrorResponse } from '@/repository/response' -import connectMongoDB from '@/repository/mongoose' -import Application from '@/repository/models/application' +import { sendErrorResponse } from "@/repository/response"; +import connectMongoDB from "@/repository/mongoose"; +import Application from "@/repository/models/application"; -export const GET = async (req: NextRequest, { params }: { params: { userId: string } }) => { - const { userId } = params +export const GET = async (req: NextRequest, ctx: { params: Promise<{ userId: string }> }) => { + const { userId } = await ctx.params; if (!userId) { - return sendErrorResponse('userId is not defined', {}, 404) + return sendErrorResponse("userId is not defined", {}, 404); } try { - await connectMongoDB() - const application = await Application.findById(userId) + await connectMongoDB(); + const application = await Application.findById(userId); if (!application) { - return sendErrorResponse('No matching user found for the provided user ID', {}, 404) + return sendErrorResponse("No matching user found for the provided user ID", {}, 404); } - const resumeId = application.resume?.id // Ensure it's optional chaining + const resumeId = application.resume?.id; // stored as string in schema if (!resumeId) { - return sendErrorResponse('User has no resume on file', {}, 404) + return sendErrorResponse("User has no resume on file", {}, 404); } if (!mongoose.connection.db) { - return sendErrorResponse('Database connection is not established', {}, 500) + return sendErrorResponse("Database connection is not established", {}, 500); } - const gridFSBucket = new GridFSBucket(mongoose.connection.db) + const gridFSBucket = new GridFSBucket(mongoose.connection.db); + const objectId = new mongoose.Types.ObjectId(resumeId); + const downloadStream = gridFSBucket.openDownloadStream(objectId); - const downloadStream = gridFSBucket.openDownloadStream(resumeId) - - const headers = new Headers() + const headers = new Headers(); return new Promise((resolve, reject) => { - downloadStream.on('file', file => { - headers.append('Content-Type', file.contentType || 'application/octet-stream') - headers.append('Content-Disposition', `attachment; filename="${file.filename || 'resume'}"`) - }) + downloadStream.on("file", (file) => { + headers.append("Content-Type", file.contentType || "application/octet-stream"); + headers.append("Content-Disposition", `attachment; filename="${file.filename || "resume"}"`); + }); - const chunks: any[] = [] + const chunks: any[] = []; - downloadStream.on('data', chunk => { - chunks.push(chunk) - }) + downloadStream.on("data", (chunk) => { + chunks.push(chunk); + }); - downloadStream.on('end', () => { - const buffer = Buffer.concat(chunks) + downloadStream.on("end", () => { + const buffer = Buffer.concat(chunks); const response = new NextResponse(buffer, { headers, - status: 200 - }) + status: 200, + }); - resolve(response) // Ensure this is a valid NextResponse - }) + resolve(response); // Ensure this is a valid NextResponse + }); - downloadStream.on('error', error => { - console.error('Error retrieving file:', error) - reject(new NextResponse('Error retrieving file', { status: 500 })) - }) - }) + downloadStream.on("error", (error) => { + console.error("Error retrieving file:", error); + reject(new NextResponse("Error retrieving file", { status: 500 })); + }); + }); } catch (error) { - console.error('Error retrieving file:', error) + console.error("Error retrieving file:", error); -return sendErrorResponse('Error retrieving file', error, 500) + return sendErrorResponse("Error retrieving file", error, 500); } -} +}; diff --git a/src/hooks/use-data-table-instance.ts b/src/hooks/use-data-table-instance.ts index e0a7a36..df8fa08 100644 --- a/src/hooks/use-data-table-instance.ts +++ b/src/hooks/use-data-table-instance.ts @@ -50,6 +50,9 @@ export function useDataTableInstance({ columnFilters, pagination, }, + // Avoid state updates triggered by auto-resets during initial render/mount + autoResetPageIndex: false, + autoResetAll: false, enableRowSelection, getRowId: getRowId ?? ((row) => (row as any).id.toString()), onRowSelectionChange: setRowSelection, diff --git a/src/utils/admissionEmailConfig.ts b/src/utils/admissionEmailConfig.ts new file mode 100644 index 0000000..6774cdc --- /dev/null +++ b/src/utils/admissionEmailConfig.ts @@ -0,0 +1,17 @@ +// Minimal email helpers to unblock status updates. +// Replace implementations with real email service integration when ready. + +export async function sendAdmittedEmail(email: string, firstName: string, lastName: string): Promise { + console.log(`[Email] Admitted -> to: ${email}, name: ${firstName} ${lastName}`); + return true; +} + +export async function sendWaitlistedEmail(email: string, firstName: string, lastName: string): Promise { + console.log(`[Email] Waitlisted -> to: ${email}, name: ${firstName} ${lastName}`); + return true; +} + +export async function sendRefusedEmail(email: string, firstName: string, lastName: string): Promise { + console.log(`[Email] Refused -> to: ${email}, name: ${firstName} ${lastName}`); + return true; +}