Skip to content

Commit

Permalink
Integrate waitlist promotion modal and action (#346)
Browse files Browse the repository at this point in the history
* fix: get_hackers function call

* Waitlist Promotion System

- Create new promote action that shows modal with confirmation button
  and instructions to participant
- Show promote action instead of check in for waitlisted participants
- Disable promote action for organizers who are not check-in leads,
  showing PopOver message
- Disable check in action for participants with `WAIVER_SIGNED` status,
  showing PopOver message

* Update apps/site/src/app/admin/participants/Participants.tsx

Co-authored-by: Taesung Hwang <[email protected]>

* grant directors promotion, along with refactoring

* Update apps/site/src/app/admin/participants/Participants.tsx

Co-authored-by: Taesung Hwang <[email protected]>

* fix: remove export, PropsWithChildren usage

* fix: typo in filename

* fix: rename import, isCheckin logic

* fix: disable check in button for ACCEPTED status

---------

Co-authored-by: Taesung Hwang <[email protected]>
  • Loading branch information
samderanova and taesungh authored Jan 24, 2024
1 parent 8256afd commit b3784fe
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 11 deletions.
2 changes: 1 addition & 1 deletion apps/api/src/routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ async def waitlist_release(uid: str) -> None:
async def participants() -> list[Participant]:
"""Get list of participants."""
# TODO: non-hackers
return await participant_manager.get_attending_applicants()
return await participant_manager.get_hackers()


@router.post("/checkin/{uid}")
Expand Down
38 changes: 32 additions & 6 deletions apps/site/src/app/admin/participants/Participants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,39 @@ import useParticipants, { Participant } from "@/lib/admin/useParticipants";

import CheckInModal from "./components/CheckInModal";
import ParticipantsTable from "./components/ParticipantsTable";
import WaitlistPromotionModal from "./components/WaitlistPromotionModal";

function Participants() {
const { participants, loading, checkInParticipant } = useParticipants();
const [currentParticipant, setCurrentParticipant] =
const {
participants,
loading,
checkInParticipant,
releaseParticipantFromWaitlist,
} = useParticipants();
const [checkinParticipant, setCheckinParticipant] =
useState<Participant | null>(null);
const [promoteParticipant, setPromoteParticipant] =
useState<Participant | null>(null);

const initiateCheckIn = (participant: Participant): void => {
setCurrentParticipant(participant);
setCheckinParticipant(participant);
};

const sendCheckIn = async (participant: Participant): Promise<void> => {
await checkInParticipant(participant);
setCurrentParticipant(null);
setCheckinParticipant(null);
// TODO: Flashbar notification
};

const initiatePromotion = (participant: Participant): void => {
setPromoteParticipant(participant);
};

const sendWaitlistPromote = async (
participant: Participant,
): Promise<void> => {
await releaseParticipantFromWaitlist(participant);
setPromoteParticipant(null);
// TODO: Flashbar notification
};

Expand All @@ -28,11 +48,17 @@ function Participants() {
participants={participants}
loading={loading}
initiateCheckIn={initiateCheckIn}
initiatePromotion={initiatePromotion}
/>
<CheckInModal
onDismiss={() => setCurrentParticipant(null)}
onDismiss={() => setCheckinParticipant(null)}
onConfirm={sendCheckIn}
participant={currentParticipant}
participant={checkinParticipant}
/>
<WaitlistPromotionModal
onDismiss={() => setPromoteParticipant(null)}
onConfirm={sendWaitlistPromote}
participant={promoteParticipant}
/>
{/* TODO: walk-in promotion modal */}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import TextContent from "@cloudscape-design/components/text-content";

import { Participant } from "@/lib/admin/useParticipants";

interface ActionModalProps {
export interface ActionModalProps {
onDismiss: () => void;
onConfirm: (participant: Participant) => void;
participant: Participant | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,72 @@
import { useContext } from "react";

import Button from "@cloudscape-design/components/button";

import UserContext from "@/lib/admin/UserContext";
import { isCheckinLead } from "@/lib/admin/authorization";
import { Status } from "@/lib/admin/useApplicant";
import { Participant } from "@/lib/admin/useParticipants";
import ParticipantActionPopover from "./ParticipantActionPopover";

interface ParticipantActionProps {
participant: Participant;
initiateCheckIn: (participant: Participant) => void;
initiatePromotion: (participant: Participant) => void;
}

function ParticipantAction({
participant,
initiateCheckIn,
initiatePromotion,
}: ParticipantActionProps) {
// TODO: waitlist promotion
const { role } = useContext(UserContext);

const isCheckin = isCheckinLead(role);
const isWaiverSigned = participant.status === Status.signed;
const isAccepted = participant.status === Status.accepted;

const promoteButton = (
<Button
variant="inline-link"
ariaLabel={`Promote ${participant._id} off waitlist`}
onClick={() => initiatePromotion(participant)}
disabled={!isCheckin}
>
Promote
</Button>
);

return (
const checkinButton = (
<Button
variant="inline-link"
ariaLabel={`Check in ${participant._id}`}
onClick={() => initiateCheckIn(participant)}
disabled={isWaiverSigned || isAccepted}
>
Check In
</Button>
);

if (participant.status === Status.waitlisted) {
if (!isCheckin) {
return (
<ParticipantActionPopover content="Only check-in leads are allowed to promote walk-ins.">
{promoteButton}
</ParticipantActionPopover>
);
}
return promoteButton;
} else if (isWaiverSigned || isAccepted) {
const content = isWaiverSigned
? "Must confirm attendance in portal first"
: "Must sign waiver and confirm attendance in portal";
return (
<ParticipantActionPopover content={content}>
{checkinButton}
</ParticipantActionPopover>
);
}
return checkinButton;
}

export default ParticipantAction;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Popover from "@cloudscape-design/components/popover";
import { PropsWithChildren } from "react";

interface ParticipantActionPopoverProps {
content: string;
}

function ParticipantActionPopover({
content,
children,
}: PropsWithChildren<ParticipantActionPopoverProps>) {
return (
<Popover
dismissButton={false}
position="top"
size="medium"
triggerType="custom"
content={content}
>
{children}
</Popover>
);
}

export default ParticipantActionPopover;
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ interface ParticipantsTableProps {
participants: Participant[];
loading: boolean;
initiateCheckIn: (participant: Participant) => void;
initiatePromotion: (participant: Participant) => void;
}

function ParticipantsTable({
participants,
loading,
initiateCheckIn,
initiatePromotion,
}: ParticipantsTableProps) {
// TODO: sorting
// TODO: search functionality
Expand All @@ -32,9 +34,10 @@ function ParticipantsTable({
<ParticipantAction
participant={participant}
initiateCheckIn={initiateCheckIn}
initiatePromotion={initiatePromotion}
/>
),
[initiateCheckIn],
[initiateCheckIn, initiatePromotion],
);

const emptyMessage = (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Box from "@cloudscape-design/components/box";
import Button from "@cloudscape-design/components/button";
import Modal from "@cloudscape-design/components/modal";
import SpaceBetween from "@cloudscape-design/components/space-between";
import TextContent from "@cloudscape-design/components/text-content";

import { ActionModalProps } from "./CheckInModal";

function WaitlistPromotionModal({
onDismiss,
onConfirm,
participant,
}: ActionModalProps) {
if (participant === null) {
return <Modal visible={false} />;
}

return (
<Modal
onDismiss={onDismiss}
visible={true}
footer={
<Box float="right">
<SpaceBetween direction="horizontal" size="xs">
<Button variant="link" onClick={onDismiss}>
Cancel
</Button>
<Button variant="primary" onClick={() => onConfirm(participant)}>
Promote
</Button>
</SpaceBetween>
</Box>
}
header={`Promote ${participant?.first_name} ${participant?.last_name} Off Waitlist`}
>
<SpaceBetween size="m">
<TextContent>
<ul>
{/* TODO: actual instructions for check-in leads */}
<li>Log into the portal</li>
<li>Sign waiver</li>
<li>Confirm attendance</li>
</ul>
</TextContent>
{/* TODO: badge barcode input */}
</SpaceBetween>
</Modal>
);
}
export default WaitlistPromotionModal;
5 changes: 5 additions & 0 deletions apps/site/src/lib/admin/authorization.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const ADMIN_ROLES = ["director", "reviewer", "checkin_lead"];
const CHECKIN_ROLES = ["director", "checkin_lead"];
const ORGANIZER_ROLES = ["organizer"];

export function isApplicationManager(role: string | null) {
Expand All @@ -11,3 +12,7 @@ export function isAdminRole(role: string | null) {
(ADMIN_ROLES.includes(role) || ORGANIZER_ROLES.includes(role))
);
}

export function isCheckinLead(role: string | null) {
return role !== null && CHECKIN_ROLES.includes(role);
}
7 changes: 7 additions & 0 deletions apps/site/src/lib/admin/useParticipants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,18 @@ function useParticipants() {
await axios.post(`/api/admin/checkin/${participant._id}`);
};

const releaseParticipantFromWaitlist = async (participant: Participant) => {
console.log(`Promoted to waitlist`, participant);
// TODO: implement mutation for showing checked in on each day
await axios.post(`/api/admin/waitlist-release/${participant._id}`);
};

return {
participants: data ?? [],
loading: isLoading,
error,
checkInParticipant,
releaseParticipantFromWaitlist,
};
}

Expand Down

0 comments on commit b3784fe

Please sign in to comment.