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

Registrar historial mentorias #102

Merged
merged 3 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions prisma/migrations/20241104012943_mentorias_history/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- CreateEnum
CREATE TYPE "MentoriaStatus" AS ENUM ('SCHEDULED', 'CANCELLED', 'NO_SHOW', 'COMPLETED');

-- CreateTable
CREATE TABLE "Mentoria" (
"id" TEXT NOT NULL,
"volunteerParticipationId" TEXT NOT NULL,
"contestantParticipantId" TEXT NOT NULL,
"meetingTime" TIMESTAMP(3) NOT NULL,
"status" "MentoriaStatus" NOT NULL,
"metadata" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "Mentoria_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Mentoria_volunteerParticipationId_contestantParticipantId_m_key" ON "Mentoria"("volunteerParticipationId", "contestantParticipantId", "meetingTime");

-- AddForeignKey
ALTER TABLE "Mentoria" ADD CONSTRAINT "Mentoria_volunteerParticipationId_fkey" FOREIGN KEY ("volunteerParticipationId") REFERENCES "VolunteerParticipation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Mentoria" ADD CONSTRAINT "Mentoria_contestantParticipantId_fkey" FOREIGN KEY ("contestantParticipantId") REFERENCES "ContestantParticipation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
24 changes: 24 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ model ContestantParticipation {
omegaupUser OmegaupUser? @relation(fields: [omegaupUserId], references: [id])
problemResults ProblemResult[]
Participation Participation[]
Mentorias Mentoria[]
File File[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Expand All @@ -175,10 +176,33 @@ model VolunteerParticipation {
problemSetterOptIn Boolean
mentorOptIn Boolean
Participation Participation[]
Mentorias Mentoria[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

enum MentoriaStatus {
SCHEDULED
CANCELLED
NO_SHOW
COMPLETED
}

model Mentoria {
id String @id @default(uuid())
volunteerParticipationId String
volunteerParticipation VolunteerParticipation @relation(fields: [volunteerParticipationId], references: [id])
contestantParticipantId String
contestantParticipant ContestantParticipation @relation(fields: [contestantParticipantId], references: [id])
meetingTime DateTime
status MentoriaStatus
metadata Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@unique([volunteerParticipationId, contestantParticipantId, meetingTime])
}

model ProblemResult {
omegaupAlias String @id
score Int
Expand Down
13 changes: 13 additions & 0 deletions src/components/mentorias/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ScheduleMentoriaRequest } from "@/types/mentorias.schema";

export async function registerMentoria(
payload: ScheduleMentoriaRequest,
): Promise<void> {
await fetch("/api/mentoria/schedule", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
}
73 changes: 61 additions & 12 deletions src/components/mentorias/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import {
} from "@/types/mentor.schema";
import Calendar from "react-calendar";
import { useState } from "react";
import { InlineWidget } from "react-calendly";
import { useCalendlyEventListener, InlineWidget } from "react-calendly";
import { Link } from "../link";
import { getLocalDateTimeWithOffset, nextHalfHour } from "@/utils/time";
import { Value } from "@sinclair/typebox/value";
import { registerMentoria } from "./client";

const fetcher = (args: RequestInfo): Promise<unknown> =>
fetch(args).then((res) => res.json());
Expand Down Expand Up @@ -64,11 +65,46 @@ function Loading(): JSX.Element {
);
}

function CalendlyInlineWidget({
url,
contestantParticipantId,
volunteerAuthId,
volunteerParticipationId,
meetingTimeOpt,
}: {
volunteerAuthId: string;
volunteerParticipationId: string;
contestantParticipantId: string | null;
meetingTimeOpt: Date | undefined;
url: string;
}): JSX.Element {
useCalendlyEventListener({
onEventScheduled: async (e) => {
if (contestantParticipantId) {
await registerMentoria({
volunteerAuthId,
contestantParticipantId,
volunteerParticipationId,
meetingTimeOpt: meetingTimeOpt && meetingTimeOpt.toISOString(),
calendlyPayload: e.data.payload,
});
}
},
});
return (
<div className="group relative z-0 mb-5 w-full">
<InlineWidget url={url}></InlineWidget>
</div>
);
}

// Receives a list of connected providers
export default function Mentorias({
ofmiEdition,
contestantParticipantId,
}: {
ofmiEdition: number;
contestantParticipantId: string | null;
}): JSX.Element {
const startTime = nextHalfHour(new Date(Date.now()));
const endTime = new Date(startTime.getTime() + 7 * 24 * 60 * 60 * 1000);
Expand All @@ -79,7 +115,11 @@ export default function Mentorias({
});
const [selectedDay, setSelectedDay] = useState<Date>();
const [selectedStartTime, setSelectedStartTime] = useState<Date>();
const [schedulingUrlToShow, setSchedulingUrlToShow] = useState<string>();
const [schedulingUrlToShow, setSchedulingUrlToShow] = useState<{
url: string;
volunteerAuthId: string;
volunteerParticipationId: string;
}>();

const showFilterCalendar = schedulingUrlToShow === undefined;
const fullSchedulingUrlToShow = (schedulingUrlToShow: string): string => {
Expand Down Expand Up @@ -220,6 +260,8 @@ export default function Mentorias({
{availabilities &&
availabilities.map(
({
volunteerAuthId,
volunteerParticipationId,
calendlySchedulingUrl,
firstName,
lastName,
Expand Down Expand Up @@ -259,17 +301,20 @@ export default function Mentorias({
ev.preventDefault();
if (
calendlySchedulingUrl ===
schedulingUrlToShow
schedulingUrlToShow?.url
) {
setSchedulingUrlToShow(undefined);
} else {
setSchedulingUrlToShow(
calendlySchedulingUrl,
);
setSchedulingUrlToShow({
url: calendlySchedulingUrl,
volunteerParticipationId,
volunteerAuthId,
});
}
}}
>
{calendlySchedulingUrl === schedulingUrlToShow
{calendlySchedulingUrl ===
schedulingUrlToShow?.url
? "Ocultar"
: "Mostrar"}
</Link>
Expand All @@ -285,11 +330,15 @@ export default function Mentorias({
{availabilities === null && <Loading />}
</div>
{schedulingUrlToShow && (
<div className="group relative z-0 mb-5 w-full">
<InlineWidget
url={fullSchedulingUrlToShow(schedulingUrlToShow)}
></InlineWidget>
</div>
<CalendlyInlineWidget
contestantParticipantId={contestantParticipantId}
volunteerAuthId={schedulingUrlToShow.volunteerAuthId}
volunteerParticipationId={
schedulingUrlToShow.volunteerParticipationId
}
meetingTimeOpt={selectedStartTime}
url={fullSchedulingUrlToShow(schedulingUrlToShow.url)}
/>
)}
</div>
</div>
Expand Down
38 changes: 34 additions & 4 deletions src/lib/calendly.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { UserAvailability } from "@/types/mentor.schema";
import { TTLCache } from "./cache";
import type { JSONValue } from "@/types/json";

const CALENDLY_API_BASE_URL = "https://api.calendly.com";

Expand Down Expand Up @@ -28,7 +29,7 @@ async function getUserUri(token: string): Promise<string> {
};
const response = await fetch(`${CALENDLY_API_BASE_URL}/users/me`, options);
if (response.status === 429) {
throw Error("Calendly RareLimit");
throw Error("Calendly RateLimit");
}
const json = await response.json();
if (response.status !== 200) {
Expand Down Expand Up @@ -72,7 +73,7 @@ async function getEventUri(
options,
);
if (response.status === 429) {
throw Error("Calendly RareLimit");
throw Error("Calendly RateLimit");
}
const json = await response.json();
if (response.status !== 200) {
Expand Down Expand Up @@ -146,7 +147,7 @@ async function getAvailableStartTimes({
options,
);
if (response.status === 429) {
throw Error("Calendly RareLimit");
throw Error("Calendly RateLimit");
}
const json = await response.json();
if (response.status !== 200) {
Expand Down Expand Up @@ -176,7 +177,10 @@ export async function getAvailabilities({
token: string;
startTime: Date;
endTime: Date;
}): Promise<Omit<UserAvailability, "firstName" | "lastName"> | null> {
}): Promise<Omit<
UserAvailability,
"volunteerAuthId" | "volunteerParticipationId" | "firstName" | "lastName"
> | null> {
try {
// Get the user uri
const userUri = await getUserUri(token);
Expand All @@ -198,3 +202,29 @@ export async function getAvailabilities({
return null;
}
}

export async function getEventOrInvitee({
token,
url,
}: {
token: string;
url: string;
}): Promise<JSONValue> {
const options = {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await fetch(url, options);
if (response.status === 429) {
throw Error("Calendly RateLimit");
}
const json = await response.json();
if (response.status !== 200) {
console.error("calendly.getEventOrInvitee", json);
throw Error("calendly.getEventOrInvitee");
}
aaron-diaz marked this conversation as resolved.
Show resolved Hide resolved

return json.resource;
}
43 changes: 24 additions & 19 deletions src/lib/ofmi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { prisma } from "@/lib/prisma";
import {
ParticipationOutput,
ParticipationRequestInput,
ParticipationRequestInputSchema,
ParticipationOutputSchema,
UserParticipation,
} from "@/types/participation.schema";
import { Pronoun, PronounsOfString } from "@/types/pronouns";
Expand Down Expand Up @@ -131,7 +133,7 @@ export async function findParticipants(
export async function findParticipation(
ofmi: Ofmi,
email: string,
): Promise<ParticipationRequestInput | null> {
): Promise<ParticipationOutput | null> {
const participation = await prisma.participation.findFirst({
where: { ofmiId: ofmi.id, user: { UserAuth: { email: email } } },
include: {
Expand Down Expand Up @@ -185,26 +187,29 @@ export async function findParticipation(
return null;
}

const payload: ParticipationRequestInput = {
ofmiEdition: ofmi.edition,
user: {
...user,
email: user.UserAuth.email,
birthDate: user.birthDate.toISOString(),
pronouns: PronounsOfString(user.pronouns) as Pronoun,
shirtStyle: ShirtStyleOfString(user.shirtStyle) as ShirtStyle,
mailingAddress: {
...mailingAddress,
recipient: mailingAddress.name,
internalNumber: mailingAddress.internalNumber ?? undefined,
municipality: mailingAddress.county,
locality: mailingAddress.neighborhood,
references: mailingAddress.references ?? undefined,
const payload: ParticipationOutput = {
input: {
ofmiEdition: ofmi.edition,
user: {
...user,
email: user.UserAuth.email,
birthDate: user.birthDate.toISOString(),
pronouns: PronounsOfString(user.pronouns) as Pronoun,
shirtStyle: ShirtStyleOfString(user.shirtStyle) as ShirtStyle,
mailingAddress: {
...mailingAddress,
recipient: mailingAddress.name,
internalNumber: mailingAddress.internalNumber ?? undefined,
municipality: mailingAddress.county,
locality: mailingAddress.neighborhood,
references: mailingAddress.references ?? undefined,
},
},
registeredAt: participation.createdAt.toISOString(),
userParticipation: userParticipation as UserParticipation,
},
registeredAt: participation.createdAt.toISOString(),
userParticipation: userParticipation as UserParticipation,
contestantParticipantId: participation.contestantParticipationId,
};

return Value.Cast(ParticipationRequestInputSchema, payload);
return Value.Cast(ParticipationOutputSchema, payload);
}
2 changes: 2 additions & 0 deletions src/lib/volunteer/mentor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export async function getAllAvailabilities({
continue;
}
mentors.push({
volunteerAuthId: userAuthId,
volunteerParticipationId: participation.volunteerParticipationId!,
firstName: participation.user.firstName,
lastName: participation.user.lastName,
...availabilities,
Expand Down
2 changes: 1 addition & 1 deletion src/pages/api/admin/participant/[email].ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async function exportParticipantsHandler(
message: `Participation for the ${ofmi.edition}-ofmi not found.`,
});
}
return res.status(200).json(participation);
return res.status(200).json(participation.input);
}

export default async function handle(
Expand Down
Loading