diff --git a/apps/api/src/admin/participant_manager.py b/apps/api/src/admin/participant_manager.py index 7ebcf1cc..785ad907 100644 --- a/apps/api/src/admin/participant_manager.py +++ b/apps/api/src/admin/participant_manager.py @@ -97,6 +97,7 @@ async def check_in_participant(uid: str, badge_number: str, associate: User) -> log.info(f"Applicant {uid} ({badge_number}) checked in by {associate.uid}") + async def confirm_attendance_non_hacker(uid: str, director: User) -> None: """Update status for Role.Attending for non-hackers.""" @@ -117,3 +118,13 @@ async def confirm_attendance_non_hacker(uid: str, director: User) -> None: raise RuntimeError(f"Could not update status to ATTENDING for {uid}.") log.info(f"Non-hacker {uid} status updated to attending by {director.uid}") + + +async def subevent_checkin(event_id: str, uid: str, organizer: User) -> None: + checkin = (uid, utc_now()) + res = await mongodb_handler.raw_update_one( + Collection.EVENTS, {"_id": event_id}, {"$push": {"checkins": checkin}} + ) + if not res: + raise RuntimeError(f"Could not update events table for {event_id} with {uid}") + log.info(f"{organizer.uid} checked in {uid} to {event_id}") diff --git a/apps/api/src/routers/admin.py b/apps/api/src/routers/admin.py index 0746b9f0..b1724695 100644 --- a/apps/api/src/routers/admin.py +++ b/apps/api/src/routers/admin.py @@ -288,6 +288,21 @@ async def update_attendance( raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) +@router.get("/events", dependencies=[Depends(require_checkin_associate)]) +async def events() -> list[dict[str, object]]: + """Get list of events""" + return await mongodb_handler.retrieve(Collection.EVENTS, {}) + + +@router.post("/event-checkin/{event}") +async def subevent_checkin( + event: str, + uid: Annotated[str, Body()], + organizer: Annotated[User, Depends(require_checkin_associate)], +) -> None: + await participant_manager.subevent_checkin(event, uid, organizer) + + async def _process_status(uids: Sequence[str], status: Status) -> None: ok = await mongodb_handler.update( Collection.USERS, {"_id": {"$in": uids}}, {"status": status} diff --git a/apps/api/src/services/mongodb_handler.py b/apps/api/src/services/mongodb_handler.py index 5e0c5825..4ed75f96 100644 --- a/apps/api/src/services/mongodb_handler.py +++ b/apps/api/src/services/mongodb_handler.py @@ -43,6 +43,7 @@ class Collection(str, Enum): USERS = "users" TESTING = "testing" SETTINGS = "settings" + EVENTS = "events" async def insert( diff --git a/apps/site/src/app/admin/events/Events.tsx b/apps/site/src/app/admin/events/Events.tsx new file mode 100644 index 00000000..9e9ac898 --- /dev/null +++ b/apps/site/src/app/admin/events/Events.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useState } from "react"; + +import ContentLayout from "@cloudscape-design/components/content-layout"; +import Select, { SelectProps } from "@cloudscape-design/components/select"; + +import useEvents from "@/lib/admin/useEvents"; +import useParticipants, { Participant } from "@/lib/admin/useParticipants"; + +import SubeventCheckin from "./components/SubeventCheckin"; + +function Events() { + const [event, setEvent] = useState(null); + + const { + events, + loading: loadingEvents, + checkInParticipantSubevent, + } = useEvents(); + const { participants, loading: loadingParticipants } = useParticipants(); + + const options = events.map(({ name, _id }) => ({ label: name, value: _id })); + + const onConfirm = async (participant: Participant): Promise => { + if (event !== null && event.value !== undefined) { + return await checkInParticipantSubevent(event.value, participant._id); + } + return false; + }; + + return ( + + setBadgeNumber(detail.value)} + value={badgeNumber} + /> + + + ); +} + +export default SubeventCheckin; diff --git a/apps/site/src/app/admin/events/page.tsx b/apps/site/src/app/admin/events/page.tsx new file mode 100644 index 00000000..624f7d72 --- /dev/null +++ b/apps/site/src/app/admin/events/page.tsx @@ -0,0 +1 @@ +export { default as default } from "./Events"; diff --git a/apps/site/src/app/admin/layout/AdminSidebar.tsx b/apps/site/src/app/admin/layout/AdminSidebar.tsx index e18c1909..c5688ae6 100644 --- a/apps/site/src/app/admin/layout/AdminSidebar.tsx +++ b/apps/site/src/app/admin/layout/AdminSidebar.tsx @@ -20,6 +20,7 @@ function AdminSidebar() { const navigationItems: SideNavigationProps.Item[] = [ { type: "link", text: "Dashboard", href: "/admin/dashboard" }, { type: "link", text: "Participants", href: "/admin/participants" }, + { type: "link", text: "Events", href: "/admin/events" }, { type: "divider" }, { type: "link", text: "Back to main site", href: "/" }, ]; diff --git a/apps/site/src/app/admin/layout/Breadcrumbs.tsx b/apps/site/src/app/admin/layout/Breadcrumbs.tsx index ebf026de..e02d75a0 100644 --- a/apps/site/src/app/admin/layout/Breadcrumbs.tsx +++ b/apps/site/src/app/admin/layout/Breadcrumbs.tsx @@ -13,6 +13,7 @@ interface PathTitles { const pathTitles: PathTitles = { applicants: "Applicants", participants: "Participants", + events: "Events", }; const DEFAULT_ITEMS = [{ text: "IrvineHacks 2024", href: BASE_PATH }]; diff --git a/apps/site/src/lib/admin/BadgeScanner.tsx b/apps/site/src/lib/admin/BadgeScanner.tsx index d9fc8c76..82954803 100644 --- a/apps/site/src/lib/admin/BadgeScanner.tsx +++ b/apps/site/src/lib/admin/BadgeScanner.tsx @@ -22,7 +22,7 @@ function BadgeScanner(props: BadgeScannerProps) { useEffect(() => { const { fps, - qrbox, + // qrbox, aspectRatio, disableFlip, verbose, @@ -34,7 +34,7 @@ function BadgeScanner(props: BadgeScannerProps) { scannerRegionId, { fps: fps ?? 5, - qrbox: qrbox ?? 300, + // qrbox: qrbox ?? 300, aspectRatio: aspectRatio ?? 1, disableFlip: disableFlip ?? true, }, diff --git a/apps/site/src/lib/admin/useEvents.ts b/apps/site/src/lib/admin/useEvents.ts new file mode 100644 index 00000000..8ad4ad55 --- /dev/null +++ b/apps/site/src/lib/admin/useEvents.ts @@ -0,0 +1,37 @@ +import axios from "axios"; +import useSWR from "swr"; + +export interface Event { + name: string; + _id: string; +} + +const fetcher = async (url: string) => { + const res = await axios.get(url); + return res.data; +}; + +function useEvents() { + const { data, error, isLoading } = useSWR( + "/api/admin/events", + fetcher, + ); + + const checkInParticipantSubevent = async ( + event: string, + uid: string, + ): Promise => { + const res = await axios.post(`/api/admin/event-checkin/${event}`, uid); + console.log(`Checked in ${uid} for ${event}`); + return res.status === 200; + }; + + return { + events: data || [], + loading: isLoading, + error, + checkInParticipantSubevent, + }; +} + +export default useEvents;