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