Skip to content

Commit 46fcfeb

Browse files
authored
Add non-interactive Participants table for check-ins (#335)
* Add `Participant` model for check-in information * Design non-interactive Participants table - Include non-interactive version of Participants table showing check-in information including UID, name, role, status, and action - Still need to add filtering, sorting, pagination, and preferences * Refactoring `ParticipantsTable` for upcoming Modal
1 parent 2cbc5ca commit 46fcfeb

File tree

7 files changed

+188
-9
lines changed

7 files changed

+188
-9
lines changed

apps/api/src/admin/participant_manager.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
from logging import getLogger
2-
from typing import Optional
2+
from typing import Any, Optional
33

44
from auth.user_identity import User, utc_now
55
from services import mongodb_handler
66
from services.mongodb_handler import Collection
7-
from utils.user_record import Role, Status
7+
from utils.user_record import Role, Status, UserRecord
88

99
log = getLogger(__name__)
1010

1111

12-
async def get_attending_applicants() -> list[dict[str, object]]:
12+
class Participant(UserRecord):
13+
"""Participants attending the event."""
14+
15+
first_name: str
16+
last_name: str
17+
status: Status
18+
19+
20+
async def get_attending_applicants() -> list[Participant]:
1321
"""Fetch all applicants who have a status of ATTENDING"""
14-
records = await mongodb_handler.retrieve(
22+
records: list[dict[str, Any]] = await mongodb_handler.retrieve(
1523
Collection.USERS,
1624
{"role": Role.APPLICANT, "status": Status.ATTENDING},
1725
[
@@ -23,7 +31,7 @@ async def get_attending_applicants() -> list[dict[str, object]]:
2331
],
2432
)
2533

26-
return records
34+
return [Participant(**user, **user["application_data"]) for user in records]
2735

2836

2937
async def check_in_applicant(uid: str, associate: User) -> None:

apps/api/src/routers/admin.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pydantic import BaseModel, EmailStr, Field, TypeAdapter, ValidationError
88

99
from admin import participant_manager, summary_handler
10+
from admin.participant_manager import Participant
1011
from auth.authorization import require_role
1112
from auth.user_identity import User, utc_now
1213
from models.ApplicationData import Decision, Review
@@ -244,9 +245,9 @@ async def waitlist_release(uid: str) -> None:
244245
log.info(f"Accepted {uid} off the waitlist and sent email.")
245246

246247

247-
@router.get("/attending", dependencies=[Depends(require_checkin_associate)])
248-
async def attending() -> list[dict[str, object]]:
249-
"""Get list of attending participants."""
248+
@router.get("/participants", dependencies=[Depends(require_checkin_associate)])
249+
async def participants() -> list[Participant]:
250+
"""Get list of participants."""
250251
# TODO: non-hackers
251252
return await participant_manager.get_attending_applicants()
252253

Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1+
"use client";
2+
3+
import useParticipants from "@/lib/admin/useParticipants";
4+
5+
import ParticipantsTable from "./components/ParticipantsTable";
6+
17
function Participants() {
2-
return <></>;
8+
const { participants, loading } = useParticipants();
9+
10+
return (
11+
<>
12+
<ParticipantsTable participants={participants} loading={loading} />;
13+
{/* TODO: modal */}
14+
</>
15+
);
316
}
417

518
export default Participants;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Button from "@cloudscape-design/components/button";
2+
3+
import { Participant } from "@/lib/admin/useParticipants";
4+
5+
function ParticipantAction({ _id }: Participant) {
6+
// TODO: waitlist promotion
7+
return (
8+
<Button variant="inline-link" ariaLabel={`Check in ${_id}`}>
9+
Check In
10+
</Button>
11+
);
12+
}
13+
14+
export default ParticipantAction;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import Box from "@cloudscape-design/components/box";
2+
import Header from "@cloudscape-design/components/header";
3+
import SpaceBetween from "@cloudscape-design/components/space-between";
4+
import Table from "@cloudscape-design/components/table";
5+
import TextFilter from "@cloudscape-design/components/text-filter";
6+
7+
import ApplicantStatus from "@/app/admin/applicants/components/ApplicantStatus";
8+
import { Participant } from "@/lib/admin/useParticipants";
9+
10+
import ParticipantAction from "./ParticipantAction";
11+
import RoleBadge from "./RoleBadge";
12+
13+
interface ParticipantsTableProps {
14+
participants: Participant[];
15+
loading: boolean;
16+
}
17+
18+
function ParticipantsTable({ participants, loading }: ParticipantsTableProps) {
19+
// TODO: sorting
20+
// TODO: search functionality
21+
// TODO: role and status filters
22+
23+
const emptyMessage = (
24+
<Box margin={{ vertical: "xs" }} textAlign="center" color="inherit">
25+
<SpaceBetween size="m">
26+
<b>No participants</b>
27+
</SpaceBetween>
28+
</Box>
29+
);
30+
31+
return (
32+
<Table
33+
// TODO: aria labels
34+
columnDefinitions={[
35+
{
36+
id: "uid",
37+
header: "UID",
38+
cell: (item) => item._id,
39+
sortingField: "uid",
40+
isRowHeader: true,
41+
},
42+
{
43+
id: "firstName",
44+
header: "First name",
45+
cell: (item) => item.first_name,
46+
sortingField: "firstName",
47+
},
48+
{
49+
id: "lastName",
50+
header: "Last name",
51+
cell: (item) => item.last_name,
52+
sortingField: "lastName",
53+
},
54+
{
55+
id: "role",
56+
header: "Role",
57+
cell: RoleBadge,
58+
sortingField: "role",
59+
},
60+
{
61+
id: "status",
62+
header: "Status",
63+
cell: ApplicantStatus,
64+
sortingField: "status",
65+
},
66+
{
67+
id: "action",
68+
header: "Action",
69+
cell: ParticipantAction,
70+
},
71+
]}
72+
header={
73+
<Header counter={`(${participants.length})`}>Participants</Header>
74+
}
75+
items={participants}
76+
loading={loading}
77+
loadingText="Loading participants"
78+
resizableColumns
79+
variant="full-page"
80+
stickyColumns={{ first: 1, last: 0 }}
81+
trackBy="_id"
82+
empty={emptyMessage}
83+
filter={
84+
<TextFilter filteringPlaceholder="Find participants" filteringText="" />
85+
}
86+
// TODO: pagination
87+
// TODO: collection preferences
88+
/>
89+
);
90+
}
91+
92+
export default ParticipantsTable;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Badge from "@cloudscape-design/components/badge";
2+
3+
import { Participant } from "@/lib/admin/useParticipants";
4+
5+
function RoleBadge({ role }: Participant) {
6+
// TODO: custom colors
7+
return <Badge>{role}</Badge>;
8+
}
9+
10+
export default RoleBadge;
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import axios from "axios";
2+
import useSWR from "swr";
3+
4+
import { Status, Uid } from "@/lib/admin/useApplicant";
5+
6+
const enum Role {
7+
Director = "director",
8+
Organizer = "organizer",
9+
CheckInLead = "checkin_lead",
10+
Applicant = "applicant",
11+
Mentor = "mentor",
12+
Volunteer = "volunteer",
13+
Sponsor = "sponsor",
14+
Judge = "judge",
15+
WorkshopLead = "workshop_lead",
16+
}
17+
18+
export interface Participant {
19+
_id: Uid;
20+
first_name: string;
21+
last_name: string;
22+
role: Role;
23+
status: Status;
24+
}
25+
26+
const fetcher = async (url: string) => {
27+
const res = await axios.get<Participant[]>(url);
28+
return res.data;
29+
};
30+
31+
function useParticipants() {
32+
const { data, error, isLoading } = useSWR<Participant[]>(
33+
"/api/admin/participants",
34+
fetcher,
35+
);
36+
37+
// TODO: implement check-in mutation
38+
return { participants: data ?? [], loading: isLoading, error };
39+
}
40+
41+
export default useParticipants;

0 commit comments

Comments
 (0)