From 95eb46d0652cb8a408a290a7d83d81b2fc5d2eb4 Mon Sep 17 00:00:00 2001 From: Taesung Hwang <44419552+taesungh@users.noreply.github.com> Date: Fri, 26 Jan 2024 15:32:14 -0800 Subject: [PATCH] Implement badge barcode scanning (#374) * Install `html5-qrcode` for scanning barcodes * Store and provide badge number for participants - Update check-in POST request to send badge number - Update check-in endpoint to store badge number - Update participants endpoint to provide badge numbers * Implement badge scanner with html5-qrcode - Still some weird `useEffect` issues with scanner opening twice --- apps/api/src/admin/participant_manager.py | 22 ++++---- apps/api/src/routers/admin.py | 6 +- apps/site/package.json | 1 + .../participants/components/CheckInModal.tsx | 52 ++++++++++++++++- apps/site/src/lib/admin/BadgeScanner.tsx | 56 +++++++++++++++++++ apps/site/src/lib/admin/useParticipants.ts | 8 ++- pnpm-lock.yaml | 7 +++ 7 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 apps/site/src/lib/admin/BadgeScanner.tsx diff --git a/apps/api/src/admin/participant_manager.py b/apps/api/src/admin/participant_manager.py index f2e6078b..7ebcf1cc 100644 --- a/apps/api/src/admin/participant_manager.py +++ b/apps/api/src/admin/participant_manager.py @@ -30,6 +30,10 @@ class Participant(UserRecord): first_name: str last_name: str status: Union[Status, Decision] = Status.REVIEWED + badge_number: Union[str, None] = None + + +PARTICIPANT_FIELDS = ["_id", "status", "role", "checkins", "badge_number"] async def get_hackers() -> list[Participant]: @@ -49,14 +53,8 @@ async def get_hackers() -> list[Participant]: ] }, }, - [ - "_id", - "status", - "role", - "checkins", - "application_data.first_name", - "application_data.last_name", - ], + PARTICIPANT_FIELDS + + ["application_data.first_name", "application_data.last_name"], ) return [Participant(**user, **user["application_data"]) for user in records] @@ -67,12 +65,12 @@ async def get_non_hackers() -> list[Participant]: records: list[dict[str, Any]] = await mongodb_handler.retrieve( Collection.USERS, {"role": {"$in": NON_HACKER_ROLES}}, - ["_id", "status", "role", "checkins", "first_name", "last_name"], + PARTICIPANT_FIELDS + ["first_name", "last_name"], ) return [Participant(**user) for user in records] -async def check_in_participant(uid: str, associate: User) -> None: +async def check_in_participant(uid: str, badge_number: str, associate: User) -> None: """Check in participant at IrvineHacks""" record: Optional[dict[str, object]] = await mongodb_handler.retrieve_one( Collection.USERS, {"_id": uid, "role": {"$exists": True}} @@ -91,13 +89,13 @@ async def check_in_participant(uid: str, associate: User) -> None: {"_id": uid}, { "$push": {"checkins": new_checkin_entry}, + "$set": {"badge_number": badge_number}, }, ) if not update_status: raise RuntimeError(f"Could not update check-in record for {uid}.") - log.info(f"Applicant {uid} checked in by {associate.uid}") - + 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.""" diff --git a/apps/api/src/routers/admin.py b/apps/api/src/routers/admin.py index f37469f4..0746b9f0 100644 --- a/apps/api/src/routers/admin.py +++ b/apps/api/src/routers/admin.py @@ -255,11 +255,13 @@ async def participants() -> list[Participant]: @router.post("/checkin/{uid}") async def check_in_participant( - uid: str, associate: Annotated[User, Depends(require_checkin_associate)] + uid: str, + badge_number: Annotated[str, Body()], + associate: Annotated[User, Depends(require_checkin_associate)], ) -> None: """Check in participant at IrvineHacks.""" try: - await participant_manager.check_in_participant(uid, associate) + await participant_manager.check_in_participant(uid, badge_number, associate) except ValueError: raise HTTPException(status.HTTP_404_NOT_FOUND) except RuntimeError as err: diff --git a/apps/site/package.json b/apps/site/package.json index d3d37fb6..7b8e0750 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -28,6 +28,7 @@ "date-fns-tz": "^2.0.0", "dayjs": "^1.11.10", "framer-motion": "^10.16.14", + "html5-qrcode": "^2.3.8", "lucide-react": "^0.292.0", "next": "13.5.6", "next-sanity": "^5.5.4", diff --git a/apps/site/src/app/admin/participants/components/CheckInModal.tsx b/apps/site/src/app/admin/participants/components/CheckInModal.tsx index a2221bc5..ae41c56b 100644 --- a/apps/site/src/app/admin/participants/components/CheckInModal.tsx +++ b/apps/site/src/app/admin/participants/components/CheckInModal.tsx @@ -1,9 +1,13 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; + import Box from "@cloudscape-design/components/box"; import Button from "@cloudscape-design/components/button"; +import Input from "@cloudscape-design/components/input"; import Modal from "@cloudscape-design/components/modal"; import SpaceBetween from "@cloudscape-design/components/space-between"; import TextContent from "@cloudscape-design/components/text-content"; +import BadgeScanner from "@/lib/admin/BadgeScanner"; import { Participant } from "@/lib/admin/useParticipants"; export interface ActionModalProps { @@ -13,7 +17,31 @@ export interface ActionModalProps { } function CheckInModal({ onDismiss, onConfirm, participant }: ActionModalProps) { + const [badgeNumber, setBadgeNumber] = useState( + participant?.badge_number ?? "", + ); + const [showScanner, setShowScanner] = useState(true); + + const onScanSuccess = useCallback((decodedText: string) => { + setBadgeNumber(decodedText); + setShowScanner(false); + }, []); + + useEffect(() => { + console.log("new participant", participant); + setBadgeNumber(participant?.badge_number ?? ""); + setShowScanner(participant?.badge_number === null); + }, [participant]); + + const badgeScanner = useMemo( + () => null} />, + [onScanSuccess], + ); + if (participant === null) { + if (showScanner) { + setShowScanner(false); + } return ; } @@ -27,7 +55,15 @@ function CheckInModal({ onDismiss, onConfirm, participant }: ActionModalProps) { - @@ -45,7 +81,19 @@ function CheckInModal({ onDismiss, onConfirm, participant }: ActionModalProps) {
  • Fill in badge and give to participant.
  • - {/* TODO: badge barcode input */} + {showScanner && badgeScanner} + + setBadgeNumber(detail.value)} + value={badgeNumber} + /> +