Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement interim subevent check-in system #377

Merged
merged 3 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/api/src/admin/participant_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,13 @@ async def confirm_attendance_non_hacker(uid: str, director: User) -> None:
raise RuntimeError(f"Could not update status to ATTENDING for {uid}.")
samderanova marked this conversation as resolved.
Show resolved Hide resolved

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring, but can leave it for later.

checkin = (uid, utc_now())
res = await mongodb_handler.raw_update_one(
Collection.EVENTS, {"_id": event_id}, {"$push": {"checkins": checkin}}
taesungh marked this conversation as resolved.
Show resolved Hide resolved
)
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, {})
taesungh marked this conversation as resolved.
Show resolved Hide resolved


@router.post("/event-checkin/{event}")
async def subevent_checkin(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring

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;
};
samderanova marked this conversation as resolved.
Show resolved Hide resolved

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;
Loading