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);
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;
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,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
Loading