From 365683d366ca69335b61f8ff96fb688f8447d0c1 Mon Sep 17 00:00:00 2001 From: Taesung Hwang <44419552+taesungh@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:54:37 -0800 Subject: [PATCH] Show check-ins per day in Participants table (#363) - Provide checkins list in participants endpoint - Add columns for each day of the event with an icon on check-in status - Disable resizable columns --- apps/api/src/admin/participant_manager.py | 11 +++++- .../components/CheckinDayIcon.tsx | 38 +++++++++++++++++++ .../components/ParticipantsTable.tsx | 36 +++++++++++++++++- apps/site/src/lib/admin/useParticipants.ts | 3 ++ 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 apps/site/src/app/admin/participants/components/CheckinDayIcon.tsx diff --git a/apps/api/src/admin/participant_manager.py b/apps/api/src/admin/participant_manager.py index 3495b8d6..2362fb9b 100644 --- a/apps/api/src/admin/participant_manager.py +++ b/apps/api/src/admin/participant_manager.py @@ -1,6 +1,9 @@ +from datetime import datetime from logging import getLogger from typing import Any, Optional, Union +from typing_extensions import TypeAlias + from auth.user_identity import User, utc_now from models.ApplicationData import Decision from services import mongodb_handler @@ -9,6 +12,8 @@ log = getLogger(__name__) +Checkin: TypeAlias = tuple[datetime, str] + NON_HACKER_ROLES = ( Role.MENTOR, Role.VOLUNTEER, @@ -21,6 +26,7 @@ class Participant(UserRecord): """Participants attending the event.""" + checkins: list[Checkin] = [] first_name: str last_name: str status: Union[Status, Decision] = Status.REVIEWED @@ -47,6 +53,7 @@ async def get_hackers() -> list[Participant]: "_id", "status", "role", + "checkins", "application_data.first_name", "application_data.last_name", ], @@ -60,7 +67,7 @@ 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", "first_name", "last_name"], + ["_id", "status", "role", "checkins", "first_name", "last_name"], ) return [Participant(**user) for user in records] @@ -77,7 +84,7 @@ async def check_in_participant(uid: str, associate: User) -> None: ): raise ValueError - new_checkin_entry = (utc_now(), associate.uid) + new_checkin_entry: Checkin = (utc_now(), associate.uid) update_status = await mongodb_handler.raw_update_one( Collection.USERS, diff --git a/apps/site/src/app/admin/participants/components/CheckinDayIcon.tsx b/apps/site/src/app/admin/participants/components/CheckinDayIcon.tsx new file mode 100644 index 00000000..1e7f2ba3 --- /dev/null +++ b/apps/site/src/app/admin/participants/components/CheckinDayIcon.tsx @@ -0,0 +1,38 @@ +import Icon from "@cloudscape-design/components/icon"; +import dayjs from "dayjs"; +import timezone from "dayjs/plugin/timezone"; +import utc from "dayjs/plugin/utc"; + +import { Checkin } from "@/lib/admin/useParticipants"; + +dayjs.extend(utc); +dayjs.extend(timezone); +const EVENT_TIMEZONE = "America/Los_Angeles"; + +interface CheckinDayProps { + checkins: Checkin[]; + date: Date; +} + +const today = dayjs().tz(EVENT_TIMEZONE); + +function CheckinDayIcon({ checkins, date }: CheckinDayProps) { + // Timezones are weird, but comparing the days of the check-ins + const day = dayjs(date).tz(EVENT_TIMEZONE); + const checkinTimes = checkins.map(([datetime]) => + dayjs(datetime).tz(EVENT_TIMEZONE), + ); + + const checkedIn = checkinTimes.some((checkin) => day.isSame(checkin, "date")); + const past = day.isBefore(today, "date"); + + if (checkedIn) { + return ; + } + if (past) { + return ; + } + return ; +} + +export default CheckinDayIcon; diff --git a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx index 4e7e55a1..e90f4155 100644 --- a/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx +++ b/apps/site/src/app/admin/participants/components/ParticipantsTable.tsx @@ -9,9 +9,14 @@ import TextFilter from "@cloudscape-design/components/text-filter"; import ApplicantStatus from "@/app/admin/applicants/components/ApplicantStatus"; import { Participant } from "@/lib/admin/useParticipants"; +import CheckinDayIcon from "./CheckinDayIcon"; import ParticipantAction from "./ParticipantAction"; import RoleBadge from "./RoleBadge"; +const FRIDAY = new Date("2024-01-26T12:00:00"); +const SATURDAY = new Date("2024-01-27T12:00:00"); +const SUNDAY = new Date("2024-01-28T12:00:00"); + interface ParticipantsTableProps { participants: Participant[]; loading: boolean; @@ -83,6 +88,24 @@ function ParticipantsTable({ cell: ApplicantStatus, sortingField: "status", }, + { + id: "friday", + header: "Fri", + cell: FridayCheckin, + sortingField: "friday", + }, + { + id: "saturday", + header: "Sat", + cell: SaturdayCheckin, + sortingField: "saturday", + }, + { + id: "sunday", + header: "Sun", + cell: SundayCheckin, + sortingField: "sunday", + }, { id: "action", header: "Action", @@ -95,7 +118,6 @@ function ParticipantsTable({ items={participants} loading={loading} loadingText="Loading participants" - resizableColumns variant="full-page" stickyColumns={{ first: 1, last: 0 }} trackBy="_id" @@ -109,4 +131,16 @@ function ParticipantsTable({ ); } +const FridayCheckin = ({ checkins }: Participant) => ( + +); + +const SaturdayCheckin = ({ checkins }: Participant) => ( + +); + +const SundayCheckin = ({ checkins }: Participant) => ( + +); + export default ParticipantsTable; diff --git a/apps/site/src/lib/admin/useParticipants.ts b/apps/site/src/lib/admin/useParticipants.ts index 7c0e2537..7376524b 100644 --- a/apps/site/src/lib/admin/useParticipants.ts +++ b/apps/site/src/lib/admin/useParticipants.ts @@ -15,11 +15,14 @@ const enum Role { WorkshopLead = "workshop_lead", } +export type Checkin = [string, Uid]; + export interface Participant { _id: Uid; first_name: string; last_name: string; role: Role; + checkins: Checkin[]; status: Status; }