Skip to content

Commit

Permalink
Registrar historial mentorias (#102)
Browse files Browse the repository at this point in the history
* Registrar historial mentorias

* store invitee too
  • Loading branch information
Juanito98 authored Nov 5, 2024
1 parent 503ffc2 commit 72487bd
Show file tree
Hide file tree
Showing 15 changed files with 349 additions and 41 deletions.
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");
}

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

0 comments on commit 72487bd

Please sign in to comment.