Skip to content

Commit

Permalink
Finish interim subevent check-in system
Browse files Browse the repository at this point in the history
  • Loading branch information
taesungh committed Jan 27, 2024
1 parent dec0600 commit 69a4c65
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 8 deletions.
5 changes: 3 additions & 2 deletions apps/api/src/admin/participant_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ async def confirm_attendance_non_hacker(uid: str, director: User) -> None:


async def subevent_checkin(event_id: str, uid: str, organizer: User) -> None:
res = await mongodb_handler.update_one(
Collection.EVENTS, {"_id": event_id}, {"checkins": {"$push": uid}}
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}")
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@ 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,
Expand Down
25 changes: 21 additions & 4 deletions apps/site/src/app/admin/events/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,40 @@ 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 } = useEvents();
const {
events,
loading: loadingEvents,
checkInParticipantSubevent,
} = useEvents();
const { participants, loading: loadingParticipants } = useParticipants();

const options = events.map(({ name, _id }) => ({ label: name, value: _id }));

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={loading ? "loading" : undefined}
statusType={loadingEvents ? "loading" : undefined}
/>
{/* TODO: use badge scanner and POST request to events endpoint */}
{event && !loadingParticipants && (
<SubeventCheckin participants={participants} onConfirm={onConfirm} />
)}
</ContentLayout>
);
}
Expand Down
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;
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 69a4c65

Please sign in to comment.