diff --git a/apps/web/src/actions/admin/scanner-admin-actions.ts b/apps/web/src/actions/admin/scanner-admin-actions.ts index 3447db0d..11f5963b 100644 --- a/apps/web/src/actions/admin/scanner-admin-actions.ts +++ b/apps/web/src/actions/admin/scanner-admin-actions.ts @@ -45,7 +45,7 @@ export const createScan = volunteerAction eventID: eventID, }); } - return { success: true }; + return { success: true, name: user.firstName }; }, ); diff --git a/apps/web/src/app/admin/events/page.tsx b/apps/web/src/app/admin/events/page.tsx index d43e3b4d..604bc1b7 100644 --- a/apps/web/src/app/admin/events/page.tsx +++ b/apps/web/src/app/admin/events/page.tsx @@ -5,7 +5,7 @@ import { columns } from "@/components/events/shared/EventColumns"; import { Button } from "@/components/shadcn/ui/button"; import { PlusCircle } from "lucide-react"; import Link from "next/link"; -import { getAllEvents, getUser } from "db/functions"; +import { getAllEventsWithScans, getUser } from "db/functions"; import { auth } from "@clerk/nextjs/server"; import FullScreenMessage from "@/components/shared/FullScreenMessage"; import { isUserAdmin } from "@/lib/utils/server/admin"; @@ -26,7 +26,7 @@ export default async function Page() { ); } - const events = await getAllEvents(); + const events = await getAllEventsWithScans(); const isUserAuthorized = isUserAdmin(userData); return (
diff --git a/apps/web/src/app/admin/users/[slug]/page.tsx b/apps/web/src/app/admin/users/[slug]/page.tsx index f4333182..a75fb07b 100644 --- a/apps/web/src/app/admin/users/[slug]/page.tsx +++ b/apps/web/src/app/admin/users/[slug]/page.tsx @@ -10,11 +10,11 @@ import { ProfileInfo, } from "@/components/admin/users/ServerSections"; import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, } from "@/components/shadcn/ui/dropdown-menu"; import { auth } from "@clerk/nextjs/server"; import { notFound } from "next/navigation"; @@ -49,7 +49,7 @@ export default async function Page({ params }: { params: { slug: string } }) { {/*

{users.length} Total Users

*/}
-
+
@@ -71,43 +71,52 @@ export default async function Page({ params }: { params: { slug: string } }) { /> )}
-
- - - - - - - - - - - - - - - - - -
- -
+
+ + + + + + + + + + + + + + + + + +
+ +
- {(c.featureFlags.core.requireUsersApproval as boolean) && ( - - )} -
-
+ {(c.featureFlags.core + .requireUsersApproval as boolean) && ( + + )} + +
diff --git a/apps/web/src/app/dash/pass/page.tsx b/apps/web/src/app/dash/pass/page.tsx index 3746e840..baf48a46 100644 --- a/apps/web/src/app/dash/pass/page.tsx +++ b/apps/web/src/app/dash/pass/page.tsx @@ -101,7 +101,7 @@ function EventPass({ qrPayload, user, clerk, guild }: EventPassProps) { c.startDate, "h:mma, MMM d, yyyy", )}`}

-

+

{c.prettyLocation}

diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index a35dbcef..8494cab5 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -139,7 +139,6 @@ } @keyframes pulseDot { - 0%, 100% { transform: scale(1); @@ -148,4 +147,4 @@ 50% { transform: scale(1.1); } -} \ No newline at end of file +} diff --git a/apps/web/src/components/admin/scanner/PassScanner.tsx b/apps/web/src/components/admin/scanner/PassScanner.tsx index f7c1d936..3e47c4ad 100644 --- a/apps/web/src/components/admin/scanner/PassScanner.tsx +++ b/apps/web/src/components/admin/scanner/PassScanner.tsx @@ -21,6 +21,7 @@ import { Button } from "@/components/shadcn/ui/button"; import Link from "next/link"; import { useRouter, usePathname, useSearchParams } from "next/navigation"; import { toast } from "sonner"; +import { ACTION_VALIDATION_ERRORS } from "@/lib/constants"; /* @@ -46,7 +47,36 @@ export default function PassScanner({ scanUser, }: PassScannerProps) { const [scanLoading, setScanLoading] = useState(false); - const { execute: runScanAction } = useAction(createScan, {}); + const { execute: runScanAction } = useAction(createScan, { + onExecute: () => { + toast.loading("Processing scan..."); + }, + onSettled: () => { + toast.dismiss(); + }, + onError: (error) => { + if (error.error.validationErrors?._errors) { + const errors = error.error.validationErrors?._errors; + if ( + errors.includes( + ACTION_VALIDATION_ERRORS.UNAUTHORIZED_NO_USER_ID, + ) || + errors.includes( + ACTION_VALIDATION_ERRORS.UNAUTHORIZED_NOT_ADMIN, + ) + ) { + toast.error( + "You do not have permission to scan users. Please ask a super admin for assistance.", + ); + return; + } + } + toast.error("Error scanning user. Please try again."); + }, + onSuccess: (res) => { + toast.success(`${res.data?.name || "User"} scanned successfully!`); + }, + }); useEffect(() => { if (hasScanned) { @@ -64,6 +94,7 @@ export default function PassScanner({ const guild = Object.keys(c.groups)[scanUser?.hackerData.group || 0] ?? "None"; const role = scanUser?.role ? scanUser?.role : "Not Found"; + const dietaryRestrictions = scanUser?.dietRestrictions || []; function handleScanCreate() { const params = new URLSearchParams(searchParams.toString()); @@ -90,7 +121,6 @@ export default function PassScanner({ }); } - toast.success("Successfully Scanned User In"); router.replace(`${path}`); } @@ -186,6 +216,15 @@ export default function PassScanner({ {" "} {guild} + {dietaryRestrictions?.length > 0 && ( +

+ + Dietary Restrictions: + {" "} + {dietaryRestrictions.join(", ") || + "None"} +

+ )} diff --git a/apps/web/src/components/events/shared/EventColumns.tsx b/apps/web/src/components/events/shared/EventColumns.tsx index fb6ed5a6..404130f4 100644 --- a/apps/web/src/components/events/shared/EventColumns.tsx +++ b/apps/web/src/components/events/shared/EventColumns.tsx @@ -23,17 +23,15 @@ import { } from "@/components/shadcn/ui/alert-dialog"; import { Badge } from "@/components/shadcn/ui/badge"; import c from "config"; -import { eventTableValidatorType } from "@/lib/types/events"; +import { EventsWithScansType } from "@/lib/types/events"; import { useState } from "react"; import { MoreHorizontal } from "lucide-react"; import { useRouter } from "next/navigation"; import { useAction } from "next-safe-action/hooks"; import { deleteEventAction } from "@/actions/admin/event-actions"; import { toast } from "sonner"; -import { LoaderCircle } from "lucide-react"; -import { error } from "console"; -type EventRow = eventTableValidatorType & { isUserAdmin: boolean }; +type EventRow = EventsWithScansType & { isUserAdmin: boolean }; export const columns: ColumnDef[] = [ { @@ -88,6 +86,11 @@ export const columns: ColumnDef[] = [ ), }, + { + accessorKey: "totalCheckins", + header: "Total Scans", + cell: ({ row }) => {row.original.totalScans || 0}, + }, { accessorKey: "actions", header: "Actions", diff --git a/apps/web/src/lib/constants/index.ts b/apps/web/src/lib/constants/index.ts index 2de61d33..4ac7bd94 100644 --- a/apps/web/src/lib/constants/index.ts +++ b/apps/web/src/lib/constants/index.ts @@ -11,3 +11,7 @@ export const HACKER_REGISTRATION_STORAGE_KEY = `${c.hackathonName}_${c.itteratio export const HACKER_REGISTRATION_RESUME_STORAGE_KEY = "hackerRegistrationResume"; export const NOT_LOCAL_SCHOOL = "NOT_LOCAL_SCHOOL"; +export const ACTION_VALIDATION_ERRORS = { + UNAUTHORIZED_NO_USER_ID: "Unauthorized (No User ID)", + UNAUTHORIZED_NOT_ADMIN: "Unauthorized (Not Admin)", +}; diff --git a/apps/web/src/lib/safe-action.ts b/apps/web/src/lib/safe-action.ts index 822408a9..3de1badf 100644 --- a/apps/web/src/lib/safe-action.ts +++ b/apps/web/src/lib/safe-action.ts @@ -6,6 +6,7 @@ import { auth } from "@clerk/nextjs/server"; import { getUser } from "db/functions"; import { z } from "zod"; import { isUserAdmin } from "./utils/server/admin"; +import { ACTION_VALIDATION_ERRORS } from "@/lib/constants"; export const publicAction = createSafeActionClient(); @@ -15,7 +16,7 @@ export const authenticatedAction = publicAction.use( const { userId } = await auth(); if (!userId) returnValidationErrors(z.null(), { - _errors: ["Unauthorized (No User ID)"], + _errors: [ACTION_VALIDATION_ERRORS.UNAUTHORIZED_NO_USER_ID], }); // TODO: add check for registration return next({ ctx: { userId } }); @@ -30,7 +31,7 @@ export const volunteerAction = authenticatedAction.use( !["admin", "super_admin", "volunteer"].includes(user.role) ) { returnValidationErrors(z.null(), { - _errors: ["Unauthorized (Not Admin)"], + _errors: [ACTION_VALIDATION_ERRORS.UNAUTHORIZED_NOT_ADMIN], }); } return next({ ctx: { user, ...ctx } }); @@ -41,7 +42,7 @@ export const adminAction = authenticatedAction.use(async ({ next, ctx }) => { const user = await getUser(ctx.userId); if (!user || !isUserAdmin(user)) { returnValidationErrors(z.null(), { - _errors: ["Unauthorized (Not Admin)"], + _errors: [ACTION_VALIDATION_ERRORS.UNAUTHORIZED_NOT_ADMIN], }); } return next({ ctx: { user, ...ctx } }); diff --git a/apps/web/src/lib/types/events.ts b/apps/web/src/lib/types/events.ts index 7aaf90bd..9efad2fe 100644 --- a/apps/web/src/lib/types/events.ts +++ b/apps/web/src/lib/types/events.ts @@ -15,15 +15,19 @@ export type EventTypeEnum = [ ...Array, ]; -export type eventTableValidatorType = Pick< +export type EventTableValidatorType = Pick< z.infer, "title" | "location" | "startTime" | "endTime" | "id" | "type" >; +export type EventsWithScansType = EventTableValidatorType & { + totalScans: string | null; +}; + export interface NewEventFormProps { defaultDate: Date; } -export interface getAllEventsOptions { +export interface GetAllEventsOptions { descending?: boolean; } diff --git a/packages/db/functions/events.ts b/packages/db/functions/events.ts index 4ae4da04..4283977b 100644 --- a/packages/db/functions/events.ts +++ b/packages/db/functions/events.ts @@ -1,12 +1,12 @@ -import { db, asc, desc, eq } from ".."; +import { db, asc, desc, eq, getTableColumns, sum } from ".."; import { eventEditType, eventInsertType, - getAllEventsOptions, + GetAllEventsOptions, } from "../../../apps/web/src/lib/types/events"; -import { events } from "../schema"; +import { events, scans } from "../schema"; -export function createNewEvent(event: eventInsertType) { +export async function createNewEvent(event: eventInsertType) { return db .insert(events) .values({ @@ -17,7 +17,7 @@ export function createNewEvent(event: eventInsertType) { }); } -export function getAllEvents(options?: getAllEventsOptions) { +export async function getAllEvents(options?: GetAllEventsOptions) { const orderByClause = options?.descending ? [desc(events.startTime)] : [asc(events.startTime)]; @@ -27,6 +27,22 @@ export function getAllEvents(options?: getAllEventsOptions) { }); } +export async function getAllEventsWithScans(options?: GetAllEventsOptions) { + const orderByClause = options?.descending + ? desc(events.startTime) + : asc(events.startTime); + + return db + .select({ + ...getTableColumns(events), + totalScans: sum(scans.count), + }) + .from(events) + .leftJoin(scans, eq(events.id, scans.eventID)) + .groupBy(events.id, scans.eventID) + .orderBy(orderByClause); +} + export async function getEventById(eventId: number) { return db.query.events.findFirst({ where: eq(events.id, eventId) }); }