From a53ce5852e2e7a81afe3ebcfac33eb1fdfd1fee8 Mon Sep 17 00:00:00 2001 From: Christian Walker Date: Sun, 26 Oct 2025 22:59:02 -0500 Subject: [PATCH 1/3] Handles scanner bug for unauthorized users and adds better feedback --- .../actions/admin/scanner-admin-actions.ts | 2 +- .../components/admin/scanner/PassScanner.tsx | 35 +++++++++++++++++-- apps/web/src/lib/constants/index.ts | 4 +++ apps/web/src/lib/safe-action.ts | 7 ++-- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/apps/web/src/actions/admin/scanner-admin-actions.ts b/apps/web/src/actions/admin/scanner-admin-actions.ts index 3447db0d..6820fa81 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/components/admin/scanner/PassScanner.tsx b/apps/web/src/components/admin/scanner/PassScanner.tsx index f7c1d936..93fe1f5b 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,28 @@ 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 +86,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 +113,6 @@ export default function PassScanner({ }); } - toast.success("Successfully Scanned User In"); router.replace(`${path}`); } @@ -186,6 +208,15 @@ export default function PassScanner({ {" "} {guild} + {dietaryRestrictions?.length > 0 && ( +

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

+ )} diff --git a/apps/web/src/lib/constants/index.ts b/apps/web/src/lib/constants/index.ts index 2de61d33..ca60c091 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)", +}; \ No newline at end of file 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 } }); From 86f1ba3d5ec48214fd6a39849a07110d7f16253d Mon Sep 17 00:00:00 2001 From: Christian Walker Date: Sun, 26 Oct 2025 22:59:18 -0500 Subject: [PATCH 2/3] Adds total scans to events admin table --- apps/web/src/app/admin/events/page.tsx | 4 ++-- .../components/events/shared/EventColumns.tsx | 13 +++++++---- apps/web/src/lib/types/events.ts | 8 +++++-- packages/db/functions/events.ts | 22 ++++++++++++++----- 4 files changed, 34 insertions(+), 13 deletions(-) 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/components/events/shared/EventColumns.tsx b/apps/web/src/components/events/shared/EventColumns.tsx index fb6ed5a6..4f5f6fd6 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,13 @@ 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/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..d58b1bfb 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,18 @@ 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) }); } From bbdaafd68846512fdba14c270eda7495b8566515 Mon Sep 17 00:00:00 2001 From: Christian Walker Date: Sun, 26 Oct 2025 22:59:40 -0500 Subject: [PATCH 3/3] formatter --- .../actions/admin/scanner-admin-actions.ts | 2 +- apps/web/src/app/admin/users/[slug]/page.tsx | 93 ++++++++++--------- apps/web/src/app/dash/pass/page.tsx | 2 +- apps/web/src/app/globals.css | 3 +- .../components/admin/scanner/PassScanner.tsx | 28 ++++-- .../components/events/shared/EventColumns.tsx | 8 +- apps/web/src/lib/constants/index.ts | 2 +- packages/db/functions/events.ts | 14 ++- 8 files changed, 85 insertions(+), 67 deletions(-) diff --git a/apps/web/src/actions/admin/scanner-admin-actions.ts b/apps/web/src/actions/admin/scanner-admin-actions.ts index 6820fa81..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, name:user.firstName }; + return { success: true, name: user.firstName }; }, ); 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 93fe1f5b..3e47c4ad 100644 --- a/apps/web/src/components/admin/scanner/PassScanner.tsx +++ b/apps/web/src/components/admin/scanner/PassScanner.tsx @@ -48,26 +48,34 @@ export default function PassScanner({ }: PassScannerProps) { const [scanLoading, setScanLoading] = useState(false); const { execute: runScanAction } = useAction(createScan, { - onExecute:() =>{ + onExecute: () => { toast.loading("Processing scan..."); }, - onSettled:() => { - toast.dismiss() + onSettled: () => { + toast.dismiss(); }, - onError:(error) => { - if (error.error.validationErrors?._errors){ + 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."); + 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!`); + onSuccess: (res) => { + toast.success(`${res.data?.name || "User"} scanned successfully!`); }, - }); useEffect(() => { diff --git a/apps/web/src/components/events/shared/EventColumns.tsx b/apps/web/src/components/events/shared/EventColumns.tsx index 4f5f6fd6..404130f4 100644 --- a/apps/web/src/components/events/shared/EventColumns.tsx +++ b/apps/web/src/components/events/shared/EventColumns.tsx @@ -87,11 +87,9 @@ export const columns: ColumnDef[] = [ ), }, { - accessorKey:"totalCheckins", - header:"Total Scans", - cell:({row})=>( - {row.original.totalScans || 0} - ) + accessorKey: "totalCheckins", + header: "Total Scans", + cell: ({ row }) => {row.original.totalScans || 0}, }, { accessorKey: "actions", diff --git a/apps/web/src/lib/constants/index.ts b/apps/web/src/lib/constants/index.ts index ca60c091..4ac7bd94 100644 --- a/apps/web/src/lib/constants/index.ts +++ b/apps/web/src/lib/constants/index.ts @@ -14,4 +14,4 @@ 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)", -}; \ No newline at end of file +}; diff --git a/packages/db/functions/events.ts b/packages/db/functions/events.ts index d58b1bfb..4283977b 100644 --- a/packages/db/functions/events.ts +++ b/packages/db/functions/events.ts @@ -27,16 +27,20 @@ export async function getAllEvents(options?: GetAllEventsOptions) { }); } -export async function getAllEventsWithScans(options?: GetAllEventsOptions){ +export async function getAllEventsWithScans(options?: GetAllEventsOptions) { const orderByClause = options?.descending ? desc(events.startTime) : asc(events.startTime); - - return db.select({ + 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); + 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) {