Skip to content

Commit

Permalink
Implement interim subevent check-in system (#377)
Browse files Browse the repository at this point in the history
* Temp: progress on subevent check-in

* Finish interim subevent check-in system

* fix: export Role

---------

Co-authored-by: Sam Der <[email protected]>
  • Loading branch information
taesungh and samderanova authored Jan 27, 2024
1 parent f4407d3 commit 2263491
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 2 deletions.
11 changes: 11 additions & 0 deletions apps/api/src/admin/participant_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand All @@ -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}")
15 changes: 15 additions & 0 deletions apps/api/src/routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/services/mongodb_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Collection(str, Enum):
USERS = "users"
TESTING = "testing"
SETTINGS = "settings"
EVENTS = "events"


async def insert(
Expand Down
47 changes: 47 additions & 0 deletions apps/site/src/app/admin/events/Events.tsx
Original file line number Diff line number Diff line change
@@ -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<SelectProps.Option | null>(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<boolean> => {
if (event !== null && event.value !== undefined) {
return await checkInParticipantSubevent(event.value, participant._id);
}
return false;
};

return (
<ContentLayout>
<Select
selectedOption={event}
onChange={({ detail }) => setEvent(detail.selectedOption)}
options={options}
statusType={loadingEvents ? "loading" : undefined}
/>
{event && !loadingParticipants && (
<SubeventCheckin participants={participants} onConfirm={onConfirm} />
)}
</ContentLayout>
);
}

export default Events;
80 changes: 80 additions & 0 deletions apps/site/src/app/admin/events/components/SubeventCheckin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useCallback, useMemo, useState } from "react";

import Button from "@cloudscape-design/components/button";
import Container from "@cloudscape-design/components/container";
import Input from "@cloudscape-design/components/input";
import SpaceBetween from "@cloudscape-design/components/space-between";

import BadgeScanner from "@/lib/admin/BadgeScanner";
import { Participant } from "@/lib/admin/useParticipants";

interface SubeventCheckinProps {
participants: Participant[];
onConfirm: (participant: Participant) => Promise<boolean>;
}

function SubeventCheckin({ participants, onConfirm }: SubeventCheckinProps) {
const [badgeNumber, setBadgeNumber] = useState("");
const [showScanner, setShowScanner] = useState(true);
const [error, setError] = useState("");

const onScanSuccess = useCallback((decodedText: string) => {
console.log("Scanner found");
setBadgeNumber(decodedText);
setShowScanner(false);
}, []);

const participant = participants.filter(
(participant) => participant.badge_number === badgeNumber,
)[0];
const notFoundMessage = (
<p>
Participant could not be found, please note down the name of the
participant manually
</p>
);

const badgeScanner = useMemo(
() => <BadgeScanner onSuccess={onScanSuccess} onError={() => null} />,
[onScanSuccess],
);

const handleConfirm = async () => {
const okay = await onConfirm(participant);
if (okay) {
console.log("clearing badge number");
setBadgeNumber("");
setError("");
} else {
setError("checkin failed");
}
};

return (
<Container>
<SpaceBetween direction="horizontal" size="xs">
<Input
onChange={({ detail }) => setBadgeNumber(detail.value)}
value={badgeNumber}
/>
<Button
iconName="video-on"
variant="icon"
onClick={() => setShowScanner(true)}
iconAlt="Scan with camera"
/>
</SpaceBetween>
{showScanner && badgeScanner}
{badgeNumber !== "" && !participant && notFoundMessage}
{participant && (
<p>
Participant: {`${participant.first_name} ${participant.last_name}`}
</p>
)}
{error}
<Button onClick={handleConfirm}>Confirm</Button>
</Container>
);
}

export default SubeventCheckin;
1 change: 1 addition & 0 deletions apps/site/src/app/admin/events/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as default } from "./Events";
1 change: 1 addition & 0 deletions apps/site/src/app/admin/layout/AdminSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: "/" },
];
Expand Down
1 change: 1 addition & 0 deletions apps/site/src/app/admin/layout/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface PathTitles {
const pathTitles: PathTitles = {
applicants: "Applicants",
participants: "Participants",
events: "Events",
};

const DEFAULT_ITEMS = [{ text: "IrvineHacks 2024", href: BASE_PATH }];
Expand Down
4 changes: 2 additions & 2 deletions apps/site/src/lib/admin/BadgeScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function BadgeScanner(props: BadgeScannerProps) {
useEffect(() => {
const {
fps,
qrbox,
// qrbox,
aspectRatio,
disableFlip,
verbose,
Expand All @@ -34,7 +34,7 @@ function BadgeScanner(props: BadgeScannerProps) {
scannerRegionId,
{
fps: fps ?? 5,
qrbox: qrbox ?? 300,
// qrbox: qrbox ?? 300,
aspectRatio: aspectRatio ?? 1,
disableFlip: disableFlip ?? true,
},
Expand Down
37 changes: 37 additions & 0 deletions apps/site/src/lib/admin/useEvents.ts
Original file line number Diff line number Diff line change
@@ -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<Event[]>(url);
return res.data;
};

function useEvents() {
const { data, error, isLoading } = useSWR<Event[]>(
"/api/admin/events",
fetcher,
);

const checkInParticipantSubevent = async (
event: string,
uid: string,
): Promise<boolean> => {
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;

0 comments on commit 2263491

Please sign in to comment.