Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate waitlist promotion modal and action #346

Merged
merged 10 commits into from
Jan 24, 2024
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);
setCheckinParticipant(null);
samderanova marked this conversation as resolved.
Show resolved Hide resolved
// 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,68 @@
import { useContext } from "react";

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

import UserContext from "@/lib/admin/UserContext";
import { isCheckinLead } from "@/lib/admin/authorization";
import { Decision, PostAcceptedStatus } from "@/lib/admin/useApplicant";
import { Participant } from "@/lib/admin/useParticipants";
import ParticipantActionPopover from "./ParticipantActionPopver";
samderanova marked this conversation as resolved.
Show resolved Hide resolved

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 === PostAcceptedStatus.signed;

const promoteButton = (
<Button
variant="inline-link"
ariaLabel={`Promote ${participant._id} off waitlist`}
onClick={() => initiatePromotion(participant)}
disabled={isCheckin}
samderanova marked this conversation as resolved.
Show resolved Hide resolved
>
Promote
</Button>
);

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

if (participant.status === Decision.waitlisted) {
if (isCheckin) {
return (
<ParticipantActionPopover content="Only check-in leads are allowed to promote walk-ins.">
{promoteButton}
</ParticipantActionPopover>
);
}
return promoteButton;
} else if (isWaiverSigned) {
return (
<ParticipantActionPopover content="Must confirm attendance in portal first">
{checkinButton}
</ParticipantActionPopover>
);
}
return checkinButton;
samderanova marked this conversation as resolved.
Show resolved Hide resolved
}

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

interface ParticipantActionPopoverProps {
content: string;
children?: ReactNode;
}
samderanova marked this conversation as resolved.
Show resolved Hide resolved

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);
}
9 changes: 8 additions & 1 deletion apps/site/src/lib/admin/useParticipants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import useSWR from "swr";

import { Status, Uid } from "@/lib/admin/useApplicant";

const enum Role {
export const enum Role {
samderanova marked this conversation as resolved.
Show resolved Hide resolved
Director = "director",
Organizer = "organizer",
CheckInLead = "checkin_lead",
Expand Down 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