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) });
}