From fdb6b02f8ea7567235e32363f4a02df72ba5dec0 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 21 Jul 2024 15:13:06 +0200 Subject: [PATCH 001/169] setup script that sends out interviewTimes --- lib/types/types.ts | 24 ++++++ lib/utils/sendInterviewTimes.ts | 60 ++++++++++++++ lib/utils/tempEmailTestData.ts | 142 ++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 lib/utils/sendInterviewTimes.ts create mode 100644 lib/utils/tempEmailTestData.ts diff --git a/lib/types/types.ts b/lib/types/types.ts index f1e28104..38ed7610 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -95,3 +95,27 @@ export type committeeInterviewType = { timeslot: string; message: string; }; + +export type interviewType = { + name: string; + interviewTimes: { + start: string; + end: string; + }; +}; + +export type emailCommitteeInterviewType = { + periodId: string; + period_name: string; + committeeName: string; + committeeEmail: string; + applicants: interviewType[]; +}; + +export type emailApplicantInterviewType = { + periodId: string; + period_name: string; + applicantName: string; + applicantEmail: string; + committees: interviewType[]; +}; diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts new file mode 100644 index 00000000..53332e94 --- /dev/null +++ b/lib/utils/sendInterviewTimes.ts @@ -0,0 +1,60 @@ +import { SESClient } from "@aws-sdk/client-ses"; +import sendEmail from "../../utils/sendEmail"; +import { + emailApplicantInterviewType, + emailCommitteeInterviewType, + interviewType, +} from "../types/types"; +const { applicantTestData, committeeTestData } = require("./tempEmailTestData"); + +export const sendInterviewTimes = async () => { + const sesClient = new SESClient({ region: "eu-north-1" }); + + // Send email to each applicant + for (const applicant of applicantTestData) { + const typedApplicant: emailApplicantInterviewType = applicant; + const applicantEmail = [typedApplicant.applicantEmail]; + const subject = `Hei, ${typedApplicant.period_name}. Her er dine intervjutider!`; + let body = `Intervjutider:\n\n`; + + typedApplicant.committees.forEach((committee: interviewType) => { + body += `Komite: ${committee.name}\n`; + body += `Start: ${committee.interviewTimes.start}\n`; + body += `Slutt: ${committee.interviewTimes.end}\n\n`; + }); + + body += `Med vennlig hilsen,\nAppkom <3`; + + await sendEmail({ + sesClient: sesClient, + fromEmail: "opptak@online.ntnu.no", + toEmails: applicantEmail, + subject: subject, + htmlContent: body, + }); + } + + // Send email to each committee + for (const committee of committeeTestData) { + const typedCommittee: emailCommitteeInterviewType = committee; + const committeeEmail = [typedCommittee.committeeEmail]; + const subject = `${typedCommittee.period_name}s sine intervjutider`; + let body = `Her er intervjutidene for søkerene deres:\n\n`; + + typedCommittee.applicants.forEach((applicant: interviewType) => { + body += `Navn: ${applicant.name}\n`; + body += `Start: ${applicant.interviewTimes.start}\n`; + body += `Slutt: ${applicant.interviewTimes.end}\n\n`; + }); + + body += `Med vennlig hilsen, Appkom <3`; + + await sendEmail({ + sesClient: sesClient, + fromEmail: "opptak@online.ntnu.no", + toEmails: committeeEmail, + subject: subject, + htmlContent: body, + }); + } +}; diff --git a/lib/utils/tempEmailTestData.ts b/lib/utils/tempEmailTestData.ts new file mode 100644 index 00000000..3eeb9fc1 --- /dev/null +++ b/lib/utils/tempEmailTestData.ts @@ -0,0 +1,142 @@ +import { + emailApplicantInterviewType, + emailCommitteeInterviewType, +} from "../types/types"; + +export const applicantTestData: emailApplicantInterviewType[] = [ + { + periodId: "1", + period_name: "Fall 2023 Recruitment", + applicantName: "Emily Johnson", + applicantEmail: "emily.johnson@example.com", + committees: [ + { + name: "Arrkom", + interviewTimes: { + start: "2023-09-15T10:00:00Z", + end: "2023-09-15T10:30:00Z", + }, + }, + { + name: "Appkom", + interviewTimes: { + start: "2023-09-16T14:00:00Z", + end: "2023-09-16T14:30:00Z", + }, + }, + ], + }, + { + periodId: "2", + period_name: "Spring 2024 Recruitment", + applicantName: "Michael Brown", + applicantEmail: "michael.brown@example.com", + committees: [ + { + name: "Tech Committee", + interviewTimes: { + start: "2024-03-10T09:00:00Z", + end: "2024-03-10T09:30:00Z", + }, + }, + { + name: "Cultural Committee", + interviewTimes: { + start: "2024-03-11T11:00:00Z", + end: "2024-03-11T11:30:00Z", + }, + }, + ], + }, + { + periodId: "3", + period_name: "Summer 2024 Recruitment", + applicantName: "Sarah Lee", + applicantEmail: "sarah.lee@example.com", + committees: [ + { + name: "Finance Committee", + interviewTimes: { + start: "2024-06-20T13:00:00Z", + end: "2024-06-20T13:30:00Z", + }, + }, + { + name: "Events Committee", + interviewTimes: { + start: "2024-06-21T15:00:00Z", + end: "2024-06-21T15:30:00Z", + }, + }, + ], + }, +]; + +export const committeeTestData: emailCommitteeInterviewType[] = [ + { + periodId: "1", + period_name: "Fall 2023 Recruitment", + committeeName: "Admissions Committee", + committeeEmail: "admissions@university.edu", + applicants: [ + { + name: "John Doe", + interviewTimes: { + start: "2023-09-15T10:00:00Z", + end: "2023-09-15T10:30:00Z", + }, + }, + { + name: "Jane Smith", + interviewTimes: { + start: "2023-09-15T11:00:00Z", + end: "2023-09-15T11:30:00Z", + }, + }, + ], + }, + { + periodId: "2", + period_name: "Spring 2024 Recruitment", + committeeName: "Tech Committee", + committeeEmail: "tech@university.edu", + applicants: [ + { + name: "Michael Brown", + interviewTimes: { + start: "2024-03-10T09:00:00Z", + end: "2024-03-10T09:30:00Z", + }, + }, + { + name: "Anna Davis", + interviewTimes: { + start: "2024-03-10T10:00:00Z", + end: "2024-03-10T10:30:00Z", + }, + }, + ], + }, + { + periodId: "3", + period_name: "Summer 2024 Recruitment", + committeeName: "Events Committee", + committeeEmail: "events@university.edu", + applicants: [ + { + name: "Sarah Lee", + interviewTimes: { + start: "2024-06-21T15:00:00Z", + end: "2024-06-21T15:30:00Z", + }, + }, + { + name: "John Williams", + interviewTimes: { + start: "2024-06-21T16:00:00Z", + end: "2024-06-21T16:30:00Z", + }, + }, + ], + }, +]; From 383b72299d7505d424881e90d230062e12b575f6 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 22 Jul 2024 00:33:07 +0200 Subject: [PATCH 002/169] refactoring --- lib/types/types.ts | 29 +++- lib/utils/sendInterviewTimes.ts | 35 +++- lib/utils/tempEmailTestData.ts | 296 +++++++++++++++++--------------- 3 files changed, 210 insertions(+), 150 deletions(-) diff --git a/lib/types/types.ts b/lib/types/types.ts index 57992772..9b145fdb 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -92,20 +92,30 @@ export type committeeInterviewType = { message: string; }; -export type interviewType = { - name: string; +export type algorithmType = { + applicantName: string; + committeeName: string; interviewTimes: { start: string; end: string; }; -}; +}[]; export type emailCommitteeInterviewType = { periodId: string; period_name: string; committeeName: string; committeeEmail: string; - applicants: interviewType[]; + applicants: [ + { + committeeName: string; + interviewTimes: { + start: string; + end: string; + room: string; + }; + }, + ]; }; export type emailApplicantInterviewType = { @@ -113,5 +123,14 @@ export type emailApplicantInterviewType = { period_name: string; applicantName: string; applicantEmail: string; - committees: interviewType[]; + committees: [ + { + applicantName: string; + InterviewTimes: { + start: string; + end: string; + room: string; + }; + }, + ]; }; diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts index 53332e94..eb4a5933 100644 --- a/lib/utils/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes.ts @@ -1,23 +1,42 @@ import { SESClient } from "@aws-sdk/client-ses"; -import sendEmail from "../../utils/sendEmail"; +import sendEmail from "./sendEmail"; import { emailApplicantInterviewType, emailCommitteeInterviewType, - interviewType, } from "../types/types"; -const { applicantTestData, committeeTestData } = require("./tempEmailTestData"); -export const sendInterviewTimes = async () => { +import { algorithmTestData } from "./tempEmailTestData"; + +interface Props { + periodId: string; +} + +export const fetchInterviewTimes = async ({ periodId }: Props) => { + //TODO + //Hente data fra algoritmen + //Hente data fra databasen + //Slå sammen date fra algoritmen og databasen +}; + +interface sendInterviewTimesProps { + committeesToEmail: emailCommitteeInterviewType[]; + applicantsToEmail: emailApplicantInterviewType[]; +} + +const sendInterviewTimes = async ({ + committeesToEmail, + applicantsToEmail, +}: sendInterviewTimesProps) => { const sesClient = new SESClient({ region: "eu-north-1" }); // Send email to each applicant - for (const applicant of applicantTestData) { + for (const applicant of applicantsToEmail) { const typedApplicant: emailApplicantInterviewType = applicant; const applicantEmail = [typedApplicant.applicantEmail]; const subject = `Hei, ${typedApplicant.period_name}. Her er dine intervjutider!`; let body = `Intervjutider:\n\n`; - typedApplicant.committees.forEach((committee: interviewType) => { + typedApplicant.committees.forEach((committee: any) => { body += `Komite: ${committee.name}\n`; body += `Start: ${committee.interviewTimes.start}\n`; body += `Slutt: ${committee.interviewTimes.end}\n\n`; @@ -35,13 +54,13 @@ export const sendInterviewTimes = async () => { } // Send email to each committee - for (const committee of committeeTestData) { + for (const committee of committeesToEmail) { const typedCommittee: emailCommitteeInterviewType = committee; const committeeEmail = [typedCommittee.committeeEmail]; const subject = `${typedCommittee.period_name}s sine intervjutider`; let body = `Her er intervjutidene for søkerene deres:\n\n`; - typedCommittee.applicants.forEach((applicant: interviewType) => { + typedCommittee.applicants.forEach((applicant: any) => { body += `Navn: ${applicant.name}\n`; body += `Start: ${applicant.interviewTimes.start}\n`; body += `Slutt: ${applicant.interviewTimes.end}\n\n`; diff --git a/lib/utils/tempEmailTestData.ts b/lib/utils/tempEmailTestData.ts index 3eeb9fc1..3349f16c 100644 --- a/lib/utils/tempEmailTestData.ts +++ b/lib/utils/tempEmailTestData.ts @@ -1,142 +1,164 @@ -import { - emailApplicantInterviewType, - emailCommitteeInterviewType, -} from "../types/types"; +import { algorithmType } from "../types/types"; -export const applicantTestData: emailApplicantInterviewType[] = [ - { - periodId: "1", - period_name: "Fall 2023 Recruitment", - applicantName: "Emily Johnson", - applicantEmail: "emily.johnson@example.com", - committees: [ - { - name: "Arrkom", - interviewTimes: { - start: "2023-09-15T10:00:00Z", - end: "2023-09-15T10:30:00Z", - }, - }, - { - name: "Appkom", - interviewTimes: { - start: "2023-09-16T14:00:00Z", - end: "2023-09-16T14:30:00Z", - }, - }, - ], - }, - { - periodId: "2", - period_name: "Spring 2024 Recruitment", - applicantName: "Michael Brown", - applicantEmail: "michael.brown@example.com", - committees: [ - { - name: "Tech Committee", - interviewTimes: { - start: "2024-03-10T09:00:00Z", - end: "2024-03-10T09:30:00Z", - }, - }, - { - name: "Cultural Committee", - interviewTimes: { - start: "2024-03-11T11:00:00Z", - end: "2024-03-11T11:30:00Z", - }, - }, - ], - }, - { - periodId: "3", - period_name: "Summer 2024 Recruitment", - applicantName: "Sarah Lee", - applicantEmail: "sarah.lee@example.com", - committees: [ - { - name: "Finance Committee", - interviewTimes: { - start: "2024-06-20T13:00:00Z", - end: "2024-06-20T13:30:00Z", - }, - }, - { - name: "Events Committee", - interviewTimes: { - start: "2024-06-21T15:00:00Z", - end: "2024-06-21T15:30:00Z", - }, - }, - ], +export const algorithmTestData: algorithmType = [ + { + applicantName: "Alice Johnson", + committeeName: "Tech Committee", + interviewTimes: { + start: "2024-07-22T09:00:00Z", + end: "2024-07-22T10:00:00Z", + }, }, -]; - -export const committeeTestData: emailCommitteeInterviewType[] = [ - { - periodId: "1", - period_name: "Fall 2023 Recruitment", - committeeName: "Admissions Committee", - committeeEmail: "admissions@university.edu", - applicants: [ - { - name: "John Doe", - interviewTimes: { - start: "2023-09-15T10:00:00Z", - end: "2023-09-15T10:30:00Z", - }, - }, - { - name: "Jane Smith", - interviewTimes: { - start: "2023-09-15T11:00:00Z", - end: "2023-09-15T11:30:00Z", - }, - }, - ], - }, - { - periodId: "2", - period_name: "Spring 2024 Recruitment", + { + applicantName: "Bob Smith", + committeeName: "HR Committee", + interviewTimes: { + start: "2024-07-22T11:00:00Z", + end: "2024-07-22T12:00:00Z", + }, + }, + { + applicantName: "Charlie Brown", + committeeName: "Finance Committee", + interviewTimes: { + start: "2024-07-23T13:00:00Z", + end: "2024-07-23T14:00:00Z", + }, + }, + { + applicantName: "Daisy Ridley", + committeeName: "Marketing Committee", + interviewTimes: { + start: "2024-07-24T15:00:00Z", + end: "2024-07-24T16:00:00Z", + }, + }, + { + applicantName: "Ethan Hunt", + committeeName: "Operations Committee", + interviewTimes: { + start: "2024-07-25T17:00:00Z", + end: "2024-07-25T18:00:00Z", + }, + }, + { + applicantName: "Fiona Apple", committeeName: "Tech Committee", - committeeEmail: "tech@university.edu", - applicants: [ - { - name: "Michael Brown", - interviewTimes: { - start: "2024-03-10T09:00:00Z", - end: "2024-03-10T09:30:00Z", - }, - }, - { - name: "Anna Davis", - interviewTimes: { - start: "2024-03-10T10:00:00Z", - end: "2024-03-10T10:30:00Z", - }, - }, - ], - }, - { - periodId: "3", - period_name: "Summer 2024 Recruitment", - committeeName: "Events Committee", - committeeEmail: "events@university.edu", - applicants: [ - { - name: "Sarah Lee", - interviewTimes: { - start: "2024-06-21T15:00:00Z", - end: "2024-06-21T15:30:00Z", - }, - }, - { - name: "John Williams", - interviewTimes: { - start: "2024-06-21T16:00:00Z", - end: "2024-06-21T16:30:00Z", - }, - }, - ], + interviewTimes: { + start: "2024-07-26T09:00:00Z", + end: "2024-07-26T10:00:00Z", + }, + }, + { + applicantName: "George Clooney", + committeeName: "HR Committee", + interviewTimes: { + start: "2024-07-27T11:00:00Z", + end: "2024-07-27T12:00:00Z", + }, + }, + { + applicantName: "Hannah Montana", + committeeName: "Finance Committee", + interviewTimes: { + start: "2024-07-28T13:00:00Z", + end: "2024-07-28T14:00:00Z", + }, + }, + { + applicantName: "Ian Somerhalder", + committeeName: "Marketing Committee", + interviewTimes: { + start: "2024-07-29T15:00:00Z", + end: "2024-07-29T16:00:00Z", + }, + }, + { + applicantName: "Jennifer Lawrence", + committeeName: "Operations Committee", + interviewTimes: { + start: "2024-07-30T17:00:00Z", + end: "2024-07-30T18:00:00Z", + }, + }, + { + applicantName: "Kyle Reese", + committeeName: "Tech Committee", + interviewTimes: { + start: "2024-07-31T09:00:00Z", + end: "2024-07-31T10:00:00Z", + }, + }, + { + applicantName: "Laura Palmer", + committeeName: "HR Committee", + interviewTimes: { + start: "2024-08-01T11:00:00Z", + end: "2024-08-01T12:00:00Z", + }, + }, + { + applicantName: "Michael Scott", + committeeName: "Finance Committee", + interviewTimes: { + start: "2024-08-02T13:00:00Z", + end: "2024-08-02T14:00:00Z", + }, + }, + { + applicantName: "Nancy Drew", + committeeName: "Marketing Committee", + interviewTimes: { + start: "2024-08-03T15:00:00Z", + end: "2024-08-03T16:00:00Z", + }, + }, + { + applicantName: "Oscar Wilde", + committeeName: "Operations Committee", + interviewTimes: { + start: "2024-08-04T17:00:00Z", + end: "2024-08-04T18:00:00Z", + }, + }, + { + applicantName: "Pam Beesly", + committeeName: "Tech Committee", + interviewTimes: { + start: "2024-08-05T09:00:00Z", + end: "2024-08-05T10:00:00Z", + }, + }, + { + applicantName: "Quentin Tarantino", + committeeName: "HR Committee", + interviewTimes: { + start: "2024-08-06T11:00:00Z", + end: "2024-08-06T12:00:00Z", + }, + }, + { + applicantName: "Rachel Green", + committeeName: "Finance Committee", + interviewTimes: { + start: "2024-08-07T13:00:00Z", + end: "2024-08-07T14:00:00Z", + }, + }, + { + applicantName: "Steve Rogers", + committeeName: "Marketing Committee", + interviewTimes: { + start: "2024-08-08T15:00:00Z", + end: "2024-08-08T16:00:00Z", + }, + }, + { + applicantName: "Tina Fey", + committeeName: "Operations Committee", + interviewTimes: { + start: "2024-08-09T17:00:00Z", + end: "2024-08-09T18:00:00Z", + }, }, ]; From 9076638bd6476c7b5b51673bee5e5c90c98b89bb Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 22 Jul 2024 00:33:16 +0200 Subject: [PATCH 003/169] . --- lib/utils/sendInterviewTimes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts index eb4a5933..3612a81a 100644 --- a/lib/utils/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes.ts @@ -15,7 +15,7 @@ export const fetchInterviewTimes = async ({ periodId }: Props) => { //TODO //Hente data fra algoritmen //Hente data fra databasen - //Slå sammen date fra algoritmen og databasen + //Slå sammen dataen fra algoritmen og databasen }; interface sendInterviewTimesProps { From 9b2525396befbd5cf12fb24bdfdb8312b4d75bca Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 22 Jul 2024 00:35:02 +0200 Subject: [PATCH 004/169] spelling --- lib/types/types.ts | 2 +- lib/utils/tempEmailTestData.ts | 40 +++++++++++++++++----------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/types/types.ts b/lib/types/types.ts index 9b145fdb..dbf7bda4 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -95,7 +95,7 @@ export type committeeInterviewType = { export type algorithmType = { applicantName: string; committeeName: string; - interviewTimes: { + interviewTime: { start: string; end: string; }; diff --git a/lib/utils/tempEmailTestData.ts b/lib/utils/tempEmailTestData.ts index 3349f16c..b84bec9b 100644 --- a/lib/utils/tempEmailTestData.ts +++ b/lib/utils/tempEmailTestData.ts @@ -4,7 +4,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Alice Johnson", committeeName: "Tech Committee", - interviewTimes: { + interviewTime: { start: "2024-07-22T09:00:00Z", end: "2024-07-22T10:00:00Z", }, @@ -12,7 +12,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Bob Smith", committeeName: "HR Committee", - interviewTimes: { + interviewTime: { start: "2024-07-22T11:00:00Z", end: "2024-07-22T12:00:00Z", }, @@ -20,7 +20,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Charlie Brown", committeeName: "Finance Committee", - interviewTimes: { + interviewTime: { start: "2024-07-23T13:00:00Z", end: "2024-07-23T14:00:00Z", }, @@ -28,7 +28,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Daisy Ridley", committeeName: "Marketing Committee", - interviewTimes: { + interviewTime: { start: "2024-07-24T15:00:00Z", end: "2024-07-24T16:00:00Z", }, @@ -36,7 +36,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Ethan Hunt", committeeName: "Operations Committee", - interviewTimes: { + interviewTime: { start: "2024-07-25T17:00:00Z", end: "2024-07-25T18:00:00Z", }, @@ -44,7 +44,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Fiona Apple", committeeName: "Tech Committee", - interviewTimes: { + interviewTime: { start: "2024-07-26T09:00:00Z", end: "2024-07-26T10:00:00Z", }, @@ -52,7 +52,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "George Clooney", committeeName: "HR Committee", - interviewTimes: { + interviewTime: { start: "2024-07-27T11:00:00Z", end: "2024-07-27T12:00:00Z", }, @@ -60,7 +60,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Hannah Montana", committeeName: "Finance Committee", - interviewTimes: { + interviewTime: { start: "2024-07-28T13:00:00Z", end: "2024-07-28T14:00:00Z", }, @@ -68,7 +68,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Ian Somerhalder", committeeName: "Marketing Committee", - interviewTimes: { + interviewTime: { start: "2024-07-29T15:00:00Z", end: "2024-07-29T16:00:00Z", }, @@ -76,7 +76,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Jennifer Lawrence", committeeName: "Operations Committee", - interviewTimes: { + interviewTime: { start: "2024-07-30T17:00:00Z", end: "2024-07-30T18:00:00Z", }, @@ -84,7 +84,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Kyle Reese", committeeName: "Tech Committee", - interviewTimes: { + interviewTime: { start: "2024-07-31T09:00:00Z", end: "2024-07-31T10:00:00Z", }, @@ -92,7 +92,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Laura Palmer", committeeName: "HR Committee", - interviewTimes: { + interviewTime: { start: "2024-08-01T11:00:00Z", end: "2024-08-01T12:00:00Z", }, @@ -100,7 +100,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Michael Scott", committeeName: "Finance Committee", - interviewTimes: { + interviewTime: { start: "2024-08-02T13:00:00Z", end: "2024-08-02T14:00:00Z", }, @@ -108,7 +108,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Nancy Drew", committeeName: "Marketing Committee", - interviewTimes: { + interviewTime: { start: "2024-08-03T15:00:00Z", end: "2024-08-03T16:00:00Z", }, @@ -116,7 +116,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Oscar Wilde", committeeName: "Operations Committee", - interviewTimes: { + interviewTime: { start: "2024-08-04T17:00:00Z", end: "2024-08-04T18:00:00Z", }, @@ -124,7 +124,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Pam Beesly", committeeName: "Tech Committee", - interviewTimes: { + interviewTime: { start: "2024-08-05T09:00:00Z", end: "2024-08-05T10:00:00Z", }, @@ -132,7 +132,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Quentin Tarantino", committeeName: "HR Committee", - interviewTimes: { + interviewTime: { start: "2024-08-06T11:00:00Z", end: "2024-08-06T12:00:00Z", }, @@ -140,7 +140,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Rachel Green", committeeName: "Finance Committee", - interviewTimes: { + interviewTime: { start: "2024-08-07T13:00:00Z", end: "2024-08-07T14:00:00Z", }, @@ -148,7 +148,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Steve Rogers", committeeName: "Marketing Committee", - interviewTimes: { + interviewTime: { start: "2024-08-08T15:00:00Z", end: "2024-08-08T16:00:00Z", }, @@ -156,7 +156,7 @@ export const algorithmTestData: algorithmType = [ { applicantName: "Tina Fey", committeeName: "Operations Committee", - interviewTimes: { + interviewTime: { start: "2024-08-09T17:00:00Z", end: "2024-08-09T18:00:00Z", }, From 5bf1a3c49542c2d3dde7cbdd39f8661bbceaad43 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 22 Jul 2024 10:50:23 +0200 Subject: [PATCH 005/169] change func names --- lib/utils/sendInterviewTimes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts index 3612a81a..6b3d2b0f 100644 --- a/lib/utils/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes.ts @@ -11,7 +11,7 @@ interface Props { periodId: string; } -export const fetchInterviewTimes = async ({ periodId }: Props) => { +export const sendOutInterviewTimes = async ({ periodId }: Props) => { //TODO //Hente data fra algoritmen //Hente data fra databasen @@ -23,7 +23,7 @@ interface sendInterviewTimesProps { applicantsToEmail: emailApplicantInterviewType[]; } -const sendInterviewTimes = async ({ +const formatAndSendEmails = async ({ committeesToEmail, applicantsToEmail, }: sendInterviewTimesProps) => { From ac616843a7b5209bd08fe0cde3273a896ceeaf83 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 22 Jul 2024 11:23:40 +0200 Subject: [PATCH 006/169] readjust types --- lib/mongo/committees.ts | 9 + lib/types/types.ts | 45 ++--- lib/utils/sendInterviewTimes.ts | 170 +++++++++++++++--- .../api/committees/times/[period-id]/index.ts | 20 ++- 4 files changed, 199 insertions(+), 45 deletions(-) diff --git a/lib/mongo/committees.ts b/lib/mongo/committees.ts index 3aece6ec..0b86d7e4 100644 --- a/lib/mongo/committees.ts +++ b/lib/mongo/committees.ts @@ -69,6 +69,15 @@ export const updateCommitteeMessage = async ( } }; +export const getCommitteesByPeriod = async (periodId: string) => { + try { + const result = await committees.find({ periodId: periodId }).toArray(); + return { committees: result }; + } catch (error) { + return { error: "Failed to fetch committees" }; + } +}; + export const getCommittees = async ( periodId: string, selectedCommittee: string, diff --git a/lib/types/types.ts b/lib/types/types.ts index dbf7bda4..5c565e58 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -81,12 +81,14 @@ export type periodType = { export type AvailableTime = { start: string; end: string; + room: string; }; export type committeeInterviewType = { periodId: string; period_name: string; committee: string; + committeeEmail: string; availabletimes: AvailableTime[]; timeslot: string; message: string; @@ -101,21 +103,25 @@ export type algorithmType = { }; }[]; +export type committeeEmails = { + committeeName: string; + committeeEmail: string; +}; + export type emailCommitteeInterviewType = { periodId: string; period_name: string; committeeName: string; committeeEmail: string; - applicants: [ - { - committeeName: string; - interviewTimes: { - start: string; - end: string; - room: string; - }; - }, - ]; + applicants: { + committeeName: string; + committeeEmail: string; + interviewTimes: { + start: string; + end: string; + room: string; + }; + }[]; }; export type emailApplicantInterviewType = { @@ -123,14 +129,13 @@ export type emailApplicantInterviewType = { period_name: string; applicantName: string; applicantEmail: string; - committees: [ - { - applicantName: string; - InterviewTimes: { - start: string; - end: string; - room: string; - }; - }, - ]; + committees: { + committeeName: string; + committeeEmail: string; + interviewTimes: { + start: string; + end: string; + room: string; + }; + }[]; }; diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts index 6b3d2b0f..51519952 100644 --- a/lib/utils/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes.ts @@ -1,8 +1,12 @@ import { SESClient } from "@aws-sdk/client-ses"; import sendEmail from "./sendEmail"; import { + committeeEmails, + committeeInterviewType, emailApplicantInterviewType, emailCommitteeInterviewType, + periodType, + algorithmType, } from "../types/types"; import { algorithmTestData } from "./tempEmailTestData"; @@ -12,10 +16,104 @@ interface Props { } export const sendOutInterviewTimes = async ({ periodId }: Props) => { - //TODO - //Hente data fra algoritmen - //Hente data fra databasen - //Slå sammen dataen fra algoritmen og databasen + const period: periodType = await fetchPeriod(periodId); + const committeeInterviewTimes: committeeInterviewType[] = + await fetchCommitteeInterviewTimes(periodId); + const committeeEmails: committeeEmails[] = await fetchCommitteeEmails(); + + //TODO hente fra algoritmen + const algorithmData: algorithmType = algorithmTestData; + + const committeesToEmail: emailCommitteeInterviewType[] = []; + const applicantsToEmailMap: { [key: string]: emailApplicantInterviewType } = + {}; + + // Merge data from algorithm and database + for (const committeeTime of committeeInterviewTimes) { + const committeeEmail = committeeEmails.find( + (email) => email.committeeName === committeeTime.committee + ); + + if (!committeeEmail) continue; + + const applicants = algorithmData + .filter((app) => app.committeeName === committeeTime.committee) + .map((app) => ({ + committeeName: app.committeeName, + committeeEmail: committeeEmail.committeeEmail, + interviewTimes: { + start: app.interviewTime.start, + end: app.interviewTime.end, + room: + committeeTime.availabletimes.find( + (time) => + time.start === app.interviewTime.start && + time.end === app.interviewTime.end + )?.room || "", + }, + })); + + const emailCommittee: emailCommitteeInterviewType = { + periodId: period._id.toString(), + period_name: period.name, + committeeName: committeeTime.committee, + committeeEmail: committeeEmail.committeeEmail, + applicants, + }; + + committeesToEmail.push(emailCommittee); + + // Collect applicants to send email + for (const applicant of applicants) { + if (!applicantsToEmailMap[applicant.committeeEmail]) { + applicantsToEmailMap[applicant.committeeEmail] = { + periodId: period._id.toString(), + period_name: period.name, + applicantName: applicant.committeeName, + applicantEmail: applicant.committeeEmail, + committees: [], + }; + } + applicantsToEmailMap[applicant.committeeEmail].committees.push(applicant); + } + } + + const applicantsToEmail = Object.values(applicantsToEmailMap); + + // Send out the emails + await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); +}; + +const fetchPeriod = async (periodId: string): Promise => { + try { + const response = await fetch(`api/periods/${periodId}`); + return await response.json(); + } catch (error) { + console.error(error); + throw new Error("Failed to fetch period"); + } +}; + +const fetchCommitteeInterviewTimes = async ( + periodId: string +): Promise => { + try { + const response = await fetch(`api/committees/times/${periodId}`); + return await response.json(); + } catch (error) { + console.error(error); + throw new Error("Failed to fetch committee interview times"); + } +}; + +const fetchCommitteeEmails = async (): Promise => { + try { + const response = await fetch(`api/periods/ow-committees`); + return await response.json(); + } catch (error) { + console.error(error); + throw new Error("Failed to fetch committee emails"); + } }; interface sendInterviewTimesProps { @@ -37,20 +135,34 @@ const formatAndSendEmails = async ({ let body = `Intervjutider:\n\n`; typedApplicant.committees.forEach((committee: any) => { - body += `Komite: ${committee.name}\n`; + body += `Komite: ${committee.committeeName}\n`; body += `Start: ${committee.interviewTimes.start}\n`; - body += `Slutt: ${committee.interviewTimes.end}\n\n`; + body += `Slutt: ${committee.interviewTimes.end}\n`; + body += `Rom: ${committee.interviewTimes.room}\n\n`; }); body += `Med vennlig hilsen,\nAppkom <3`; - await sendEmail({ - sesClient: sesClient, - fromEmail: "opptak@online.ntnu.no", - toEmails: applicantEmail, - subject: subject, - htmlContent: body, - }); + // await sendEmail({ + // sesClient: sesClient, + // fromEmail: "opptak@online.ntnu.no", + // toEmails: applicantEmail, + // subject: subject, + // htmlContent: body, + // }); + + console.log( + "sesClient", + sesClient, + "fromEmail", + "opptak@online.ntnu.no", + "toEmails", + applicantEmail, + "subject", + subject, + "htmlContent", + body + ); } // Send email to each committee @@ -61,19 +173,33 @@ const formatAndSendEmails = async ({ let body = `Her er intervjutidene for søkerene deres:\n\n`; typedCommittee.applicants.forEach((applicant: any) => { - body += `Navn: ${applicant.name}\n`; + body += `Navn: ${applicant.committeeName}\n`; body += `Start: ${applicant.interviewTimes.start}\n`; - body += `Slutt: ${applicant.interviewTimes.end}\n\n`; + body += `Slutt: ${applicant.interviewTimes.end}\n`; + body += `Rom: ${applicant.interviewTimes.room}\n\n`; }); body += `Med vennlig hilsen, Appkom <3`; - await sendEmail({ - sesClient: sesClient, - fromEmail: "opptak@online.ntnu.no", - toEmails: committeeEmail, - subject: subject, - htmlContent: body, - }); + // await sendEmail({ + // sesClient: sesClient, + // fromEmail: "opptak@online.ntnu.no", + // toEmails: committeeEmail, + // subject: subject, + // htmlContent: body, + // }); + + console.log( + "sesClient", + sesClient, + "fromEmail", + "opptak@online.ntnu.no", + "toEmails", + committeeEmail, + "subject", + subject, + "htmlContent", + body + ); } }; diff --git a/pages/api/committees/times/[period-id]/index.ts b/pages/api/committees/times/[period-id]/index.ts index ba8f91da..da15ccc9 100644 --- a/pages/api/committees/times/[period-id]/index.ts +++ b/pages/api/committees/times/[period-id]/index.ts @@ -2,12 +2,15 @@ import { NextApiRequest, NextApiResponse } from "next"; import { getCommittees, createCommittee, - deleteCommittee, - updateCommitteeMessage, + getCommitteesByPeriod, } from "../../../../../lib/mongo/committees"; import { getServerSession } from "next-auth"; import { authOptions } from "../../../auth/[...nextauth]"; -import { hasSession, isInCommitee } from "../../../../../lib/utils/apiChecks"; +import { + hasSession, + isAdmin, + isInCommitee, +} from "../../../../../lib/utils/apiChecks"; import { isCommitteeType } from "../../../../../lib/utils/validators"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { @@ -23,6 +26,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (!hasSession(res, session)) return; if (!isInCommitee(res, session)) return; + if (req.method === "GET") { + if (!isAdmin(res, session)) return; + + try { + const committees = await getCommitteesByPeriod(periodId); + return res.status(200).json({ committees }); + } catch (error: any) { + return res.status(500).json({ error: error.message }); + } + } + if (req.method === "POST") { const committeeData = req.body; From f84b4baff3ba8b178ce7ce163d1f330ca58f6f79 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 22 Jul 2024 11:40:09 +0200 Subject: [PATCH 007/169] button to test sendInterviewTimes --- lib/mongo/committees.ts | 1 + lib/utils/sendInterviewTimes.ts | 42 +++++++++++++++--- pages/admin/[period-id]/index.tsx | 44 ++++++++++++++++--- .../api/committees/times/[period-id]/index.ts | 2 +- 4 files changed, 76 insertions(+), 13 deletions(-) diff --git a/lib/mongo/committees.ts b/lib/mongo/committees.ts index 0b86d7e4..7018cee6 100644 --- a/lib/mongo/committees.ts +++ b/lib/mongo/committees.ts @@ -71,6 +71,7 @@ export const updateCommitteeMessage = async ( export const getCommitteesByPeriod = async (periodId: string) => { try { + if (!committees) await init(); const result = await committees.find({ periodId: periodId }).toArray(); return { committees: result }; } catch (error) { diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts index 51519952..6f2aa7ae 100644 --- a/lib/utils/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes.ts @@ -21,13 +21,22 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { await fetchCommitteeInterviewTimes(periodId); const committeeEmails: committeeEmails[] = await fetchCommitteeEmails(); - //TODO hente fra algoritmen + console.log("Period:", period); + console.log("Committee Interview Times:", committeeInterviewTimes); + console.log("Committee Emails:", committeeEmails); + + // TODO hente fra algoritmen const algorithmData: algorithmType = algorithmTestData; const committeesToEmail: emailCommitteeInterviewType[] = []; const applicantsToEmailMap: { [key: string]: emailApplicantInterviewType } = {}; + // Ensure committeeInterviewTimes is an array + if (!Array.isArray(committeeInterviewTimes)) { + throw new Error("committeeInterviewTimes is not an array"); + } + // Merge data from algorithm and database for (const committeeTime of committeeInterviewTimes) { const committeeEmail = committeeEmails.find( @@ -80,14 +89,21 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { const applicantsToEmail = Object.values(applicantsToEmailMap); + console.log("Committees To Email:", committeesToEmail); + console.log("Applicants To Email:", applicantsToEmail); + // Send out the emails await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); }; const fetchPeriod = async (periodId: string): Promise => { try { - const response = await fetch(`api/periods/${periodId}`); - return await response.json(); + const response = await fetch(`/api/periods/${periodId}`); + const data = await response.json(); + if (!data || !data.period) { + throw new Error("Invalid response from fetchPeriod"); + } + return data.period; } catch (error) { console.error(error); throw new Error("Failed to fetch period"); @@ -98,8 +114,14 @@ const fetchCommitteeInterviewTimes = async ( periodId: string ): Promise => { try { - const response = await fetch(`api/committees/times/${periodId}`); - return await response.json(); + const response = await fetch(`/api/committees/times/${periodId}`); + const data = await response.json(); + if (!Array.isArray(data)) { + throw new Error( + "Expected an array from the fetchCommitteeInterviewTimes API response" + ); + } + return data; } catch (error) { console.error(error); throw new Error("Failed to fetch committee interview times"); @@ -108,8 +130,14 @@ const fetchCommitteeInterviewTimes = async ( const fetchCommitteeEmails = async (): Promise => { try { - const response = await fetch(`api/periods/ow-committees`); - return await response.json(); + const response = await fetch(`/api/periods/ow-committees`); + const data = await response.json(); + if (!Array.isArray(data)) { + throw new Error( + "Expected an array from the fetchCommitteeEmails API response" + ); + } + return data; } catch (error) { console.error(error); throw new Error("Failed to fetch committee emails"); diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index 63901159..0cdff8e2 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -4,12 +4,18 @@ import router from "next/router"; import { applicantType, periodType } from "../../../lib/types/types"; import NotFound from "../../404"; import ApplicantsOverview from "../../../components/applicantoverview/ApplicantsOverview"; +import { Tabs } from "../../../components/Tabs"; +import { CalendarIcon, InboxIcon } from "@heroicons/react/24/solid"; +import Button from "../../../components/Button"; +import { sendOutInterviewTimes } from "../../../lib/utils/sendInterviewTimes"; const Admin = () => { const { data: session } = useSession(); - const periodId = router.query["period-id"]; + const periodId = router.query["period-id"] as string; const [period, setPeriod] = useState(null); const [committees, setCommittees] = useState(null); + const [activeTab, setActiveTab] = useState(0); + const [tabClicked, setTabClicked] = useState(0); useEffect(() => { const fetchPeriod = async () => { @@ -41,10 +47,38 @@ const Admin = () => { } return ( - { + setActiveTab(index); + setTabClicked(index); + }} + content={[ + { + title: "Søkere", + icon: , + content: ( + + ), + }, + { + title: "Send ut", + icon: , + content: ( +
+
+ ), + }, + ]} /> ); }; diff --git a/pages/api/committees/times/[period-id]/index.ts b/pages/api/committees/times/[period-id]/index.ts index da15ccc9..3a7ed326 100644 --- a/pages/api/committees/times/[period-id]/index.ts +++ b/pages/api/committees/times/[period-id]/index.ts @@ -58,7 +58,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } } - res.setHeader("Allow", ["POST"]); + res.setHeader("Allow", ["POST", "GET"]); res.status(405).end(`Method ${req.method} is not allowed.`); }; From c7205f5d02b067a3177e9d2b1f65e0f14d4f1b95 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 22 Jul 2024 17:11:35 +0200 Subject: [PATCH 008/169] adjust API --- lib/utils/sendInterviewTimes.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts index 6f2aa7ae..d861df15 100644 --- a/lib/utils/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes.ts @@ -116,12 +116,9 @@ const fetchCommitteeInterviewTimes = async ( try { const response = await fetch(`/api/committees/times/${periodId}`); const data = await response.json(); - if (!Array.isArray(data)) { - throw new Error( - "Expected an array from the fetchCommitteeInterviewTimes API response" - ); - } - return data; + console.log(data.committees.committees); + + return data.committees.committees; } catch (error) { console.error(error); throw new Error("Failed to fetch committee interview times"); From 7845a308a25d182f2ed180d638767b4fde4b9cb5 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 22 Jul 2024 17:20:43 +0200 Subject: [PATCH 009/169] change type and update test data --- lib/types/types.ts | 27 +-- lib/utils/tempEmailTestData.ts | 379 +++++++++++++++++++++------------ 2 files changed, 252 insertions(+), 154 deletions(-) diff --git a/lib/types/types.ts b/lib/types/types.ts index 5c565e58..f470c73d 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -96,11 +96,12 @@ export type committeeInterviewType = { export type algorithmType = { applicantName: string; - committeeName: string; - interviewTime: { + + interviews: { start: string; end: string; - }; + committeeName: string; + }[]; }[]; export type committeeEmails = { @@ -113,15 +114,17 @@ export type emailCommitteeInterviewType = { period_name: string; committeeName: string; committeeEmail: string; - applicants: { - committeeName: string; - committeeEmail: string; - interviewTimes: { - start: string; - end: string; - room: string; - }; - }[]; + applicants: [ + { + committeeName: string; + committeeEmail: string; + interviewTimes: { + start: string; + end: string; + room: string; + }; + }, + ]; }; export type emailApplicantInterviewType = { diff --git a/lib/utils/tempEmailTestData.ts b/lib/utils/tempEmailTestData.ts index b84bec9b..b0ed1658 100644 --- a/lib/utils/tempEmailTestData.ts +++ b/lib/utils/tempEmailTestData.ts @@ -2,163 +2,258 @@ import { algorithmType } from "../types/types"; export const algorithmTestData: algorithmType = [ { - applicantName: "Alice Johnson", - committeeName: "Tech Committee", - interviewTime: { - start: "2024-07-22T09:00:00Z", - end: "2024-07-22T10:00:00Z", - }, + applicantName: "Ian Somerhalder", + interviews: [ + { + start: "2024-07-29T15:00:00Z", + end: "2024-07-29T16:00:00Z", + committeeName: "Bedkom", + }, + { + start: "2024-07-29T15:00:00Z", + end: "2024-07-29T16:00:00Z", + committeeName: "Appkom", + }, + ], }, { - applicantName: "Bob Smith", - committeeName: "HR Committee", - interviewTime: { - start: "2024-07-22T11:00:00Z", - end: "2024-07-22T12:00:00Z", - }, + applicantName: "Fredrik Hansemann", + interviews: [ + { + start: "2024-07-29T08:30:00Z", + end: "2024-07-29T09:00:00Z", + committeeName: "Bedkom", + }, + { + start: "2024-07-29T15:00:00Z", + end: "2024-07-29T16:00:00Z", + committeeName: "Appkom", + }, + ], }, { - applicantName: "Charlie Brown", - committeeName: "Finance Committee", - interviewTime: { - start: "2024-07-23T13:00:00Z", - end: "2024-07-23T14:00:00Z", - }, + applicantName: "Elena Gilbert", + interviews: [ + { + start: "2024-07-30T10:00:00Z", + end: "2024-07-30T11:00:00Z", + committeeName: "Dotkom", + }, + { + start: "2024-07-30T12:00:00Z", + end: "2024-07-30T13:00:00Z", + committeeName: "Fagkom", + }, + ], }, { - applicantName: "Daisy Ridley", - committeeName: "Marketing Committee", - interviewTime: { - start: "2024-07-24T15:00:00Z", - end: "2024-07-24T16:00:00Z", - }, + applicantName: "Damon Salvatore", + interviews: [ + { + start: "2024-07-31T09:00:00Z", + end: "2024-07-31T10:00:00Z", + committeeName: "Ekskom", + }, + { + start: "2024-07-31T11:00:00Z", + end: "2024-07-31T12:00:00Z", + committeeName: "Bedkom", + }, + ], }, { - applicantName: "Ethan Hunt", - committeeName: "Operations Committee", - interviewTime: { - start: "2024-07-25T17:00:00Z", - end: "2024-07-25T18:00:00Z", - }, + applicantName: "Bonnie Bennett", + interviews: [ + { + start: "2024-08-01T14:00:00Z", + end: "2024-08-01T15:00:00Z", + committeeName: "Appkom", + }, + { + start: "2024-08-01T16:00:00Z", + end: "2024-08-01T17:00:00Z", + committeeName: "Dotkom", + }, + ], }, { - applicantName: "Fiona Apple", - committeeName: "Tech Committee", - interviewTime: { - start: "2024-07-26T09:00:00Z", - end: "2024-07-26T10:00:00Z", - }, + applicantName: "Caroline Forbes", + interviews: [ + { + start: "2024-08-02T10:30:00Z", + end: "2024-08-02T11:30:00Z", + committeeName: "Fagkom", + }, + { + start: "2024-08-02T12:30:00Z", + end: "2024-08-02T13:30:00Z", + committeeName: "Ekskom", + }, + ], }, { - applicantName: "George Clooney", - committeeName: "HR Committee", - interviewTime: { - start: "2024-07-27T11:00:00Z", - end: "2024-07-27T12:00:00Z", - }, + applicantName: "Stefan Salvatore", + interviews: [ + { + start: "2024-08-03T08:00:00Z", + end: "2024-08-03T09:00:00Z", + committeeName: "Bedkom", + }, + { + start: "2024-08-03T10:00:00Z", + end: "2024-08-03T11:00:00Z", + committeeName: "Appkom", + }, + ], }, { - applicantName: "Hannah Montana", - committeeName: "Finance Committee", - interviewTime: { - start: "2024-07-28T13:00:00Z", - end: "2024-07-28T14:00:00Z", - }, + applicantName: "Matt Donovan", + interviews: [ + { + start: "2024-08-04T09:00:00Z", + end: "2024-08-04T10:00:00Z", + committeeName: "Dotkom", + }, + { + start: "2024-08-04T11:00:00Z", + end: "2024-08-04T12:00:00Z", + committeeName: "Fagkom", + }, + ], }, { - applicantName: "Ian Somerhalder", - committeeName: "Marketing Committee", - interviewTime: { - start: "2024-07-29T15:00:00Z", - end: "2024-07-29T16:00:00Z", - }, - }, - { - applicantName: "Jennifer Lawrence", - committeeName: "Operations Committee", - interviewTime: { - start: "2024-07-30T17:00:00Z", - end: "2024-07-30T18:00:00Z", - }, - }, - { - applicantName: "Kyle Reese", - committeeName: "Tech Committee", - interviewTime: { - start: "2024-07-31T09:00:00Z", - end: "2024-07-31T10:00:00Z", - }, - }, - { - applicantName: "Laura Palmer", - committeeName: "HR Committee", - interviewTime: { - start: "2024-08-01T11:00:00Z", - end: "2024-08-01T12:00:00Z", - }, - }, - { - applicantName: "Michael Scott", - committeeName: "Finance Committee", - interviewTime: { - start: "2024-08-02T13:00:00Z", - end: "2024-08-02T14:00:00Z", - }, - }, - { - applicantName: "Nancy Drew", - committeeName: "Marketing Committee", - interviewTime: { - start: "2024-08-03T15:00:00Z", - end: "2024-08-03T16:00:00Z", - }, - }, - { - applicantName: "Oscar Wilde", - committeeName: "Operations Committee", - interviewTime: { - start: "2024-08-04T17:00:00Z", - end: "2024-08-04T18:00:00Z", - }, - }, - { - applicantName: "Pam Beesly", - committeeName: "Tech Committee", - interviewTime: { - start: "2024-08-05T09:00:00Z", - end: "2024-08-05T10:00:00Z", - }, - }, - { - applicantName: "Quentin Tarantino", - committeeName: "HR Committee", - interviewTime: { - start: "2024-08-06T11:00:00Z", - end: "2024-08-06T12:00:00Z", - }, - }, - { - applicantName: "Rachel Green", - committeeName: "Finance Committee", - interviewTime: { - start: "2024-08-07T13:00:00Z", - end: "2024-08-07T14:00:00Z", - }, - }, - { - applicantName: "Steve Rogers", - committeeName: "Marketing Committee", - interviewTime: { - start: "2024-08-08T15:00:00Z", - end: "2024-08-08T16:00:00Z", - }, - }, - { - applicantName: "Tina Fey", - committeeName: "Operations Committee", - interviewTime: { - start: "2024-08-09T17:00:00Z", - end: "2024-08-09T18:00:00Z", - }, + applicantName: "Tyler Lockwood", + interviews: [ + { + start: "2024-08-05T13:00:00Z", + end: "2024-08-05T14:00:00Z", + committeeName: "Ekskom", + }, + { + start: "2024-08-05T15:00:00Z", + end: "2024-08-05T16:00:00Z", + committeeName: "Bedkom", + }, + ], + }, + { + applicantName: "Alaric Saltzman", + interviews: [ + { + start: "2024-08-06T09:30:00Z", + end: "2024-08-06T10:30:00Z", + committeeName: "Appkom", + }, + { + start: "2024-08-06T11:30:00Z", + end: "2024-08-06T12:30:00Z", + committeeName: "Dotkom", + }, + ], + }, + { + applicantName: "Jenna Sommers", + interviews: [ + { + start: "2024-08-07T08:30:00Z", + end: "2024-08-07T09:30:00Z", + committeeName: "Fagkom", + }, + { + start: "2024-08-07T10:30:00Z", + end: "2024-08-07T11:30:00Z", + committeeName: "Ekskom", + }, + ], + }, + { + applicantName: "Jeremy Gilbert", + interviews: [ + { + start: "2024-08-08T10:00:00Z", + end: "2024-08-08T11:00:00Z", + committeeName: "Bedkom", + }, + { + start: "2024-08-08T12:00:00Z", + end: "2024-08-08T13:00:00Z", + committeeName: "Appkom", + }, + ], + }, + { + applicantName: "Katherine Pierce", + interviews: [ + { + start: "2024-08-09T14:30:00Z", + end: "2024-08-09T15:30:00Z", + committeeName: "Dotkom", + }, + { + start: "2024-08-09T16:30:00Z", + end: "2024-08-09T17:30:00Z", + committeeName: "Fagkom", + }, + ], + }, + { + applicantName: "Rebekah Mikaelson", + interviews: [ + { + start: "2024-08-10T09:00:00Z", + end: "2024-08-10T10:00:00Z", + committeeName: "Ekskom", + }, + { + start: "2024-08-10T11:00:00Z", + end: "2024-08-10T12:00:00Z", + committeeName: "Bedkom", + }, + ], + }, + { + applicantName: "Klaus Mikaelson", + interviews: [ + { + start: "2024-08-11T13:00:00Z", + end: "2024-08-11T14:00:00Z", + committeeName: "Appkom", + }, + { + start: "2024-08-11T15:00:00Z", + end: "2024-08-11T16:00:00Z", + committeeName: "Dotkom", + }, + ], + }, + { + applicantName: "Hayley Marshall", + interviews: [ + { + start: "2024-08-12T08:30:00Z", + end: "2024-08-12T09:30:00Z", + committeeName: "Fagkom", + }, + { + start: "2024-08-12T10:30:00Z", + end: "2024-08-12T11:30:00Z", + committeeName: "Ekskom", + }, + ], + }, + { + applicantName: "Marcel Gerard", + interviews: [ + { + start: "2024-08-13T11:00:00Z", + end: "2024-08-13T12:00:00Z", + committeeName: "Bedkom", + }, + { + start: "2024-08-13T13:00:00Z", + end: "2024-08-13T14:00:00Z", + committeeName: "Appkom", + }, + ], }, ]; From e300e7438e8387c958da758232a55b3be18ffb2d Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 22 Jul 2024 17:57:48 +0200 Subject: [PATCH 010/169] fix committees to email --- lib/types/types.ts | 24 +++++------ lib/utils/sendInterviewTimes.ts | 75 ++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 46 deletions(-) diff --git a/lib/types/types.ts b/lib/types/types.ts index f470c73d..f8563f71 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -105,8 +105,8 @@ export type algorithmType = { }[]; export type committeeEmails = { - committeeName: string; - committeeEmail: string; + name_short: string; + email: string; }; export type emailCommitteeInterviewType = { @@ -114,17 +114,15 @@ export type emailCommitteeInterviewType = { period_name: string; committeeName: string; committeeEmail: string; - applicants: [ - { - committeeName: string; - committeeEmail: string; - interviewTimes: { - start: string; - end: string; - room: string; - }; - }, - ]; + applicants: { + committeeName: string; + committeeEmail: string; + interviewTimes: { + start: string; + end: string; + room: string; + }; + }[]; }; export type emailApplicantInterviewType = { diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts index d861df15..925409cc 100644 --- a/lib/utils/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes.ts @@ -25,65 +25,81 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { console.log("Committee Interview Times:", committeeInterviewTimes); console.log("Committee Emails:", committeeEmails); - // TODO hente fra algoritmen const algorithmData: algorithmType = algorithmTestData; const committeesToEmail: emailCommitteeInterviewType[] = []; const applicantsToEmailMap: { [key: string]: emailApplicantInterviewType } = {}; - // Ensure committeeInterviewTimes is an array - if (!Array.isArray(committeeInterviewTimes)) { - throw new Error("committeeInterviewTimes is not an array"); - } - - // Merge data from algorithm and database for (const committeeTime of committeeInterviewTimes) { + // Find the email details for the current committee const committeeEmail = committeeEmails.find( - (email) => email.committeeName === committeeTime.committee + (email) => + email.name_short.toLowerCase() === committeeTime.committee.toLowerCase() ); + console.log("her 4"); + console.log(`Committee email ${committeeEmail}`); + if (!committeeEmail) continue; + console.log("her 5"); const applicants = algorithmData - .filter((app) => app.committeeName === committeeTime.committee) + .filter((app) => + app.interviews.some( + (interview) => interview.committeeName === committeeTime.committee + ) + ) .map((app) => ({ - committeeName: app.committeeName, - committeeEmail: committeeEmail.committeeEmail, - interviewTimes: { - start: app.interviewTime.start, - end: app.interviewTime.end, + committeeName: committeeTime.committee, + committeeEmail: committeeEmail.email, + interviewTimes: app.interviews.map((interview) => ({ + start: interview.start, + end: interview.end, room: committeeTime.availabletimes.find( (time) => - time.start === app.interviewTime.start && - time.end === app.interviewTime.end + time.start === interview.start && time.end === interview.end )?.room || "", - }, + })), })); const emailCommittee: emailCommitteeInterviewType = { periodId: period._id.toString(), period_name: period.name, committeeName: committeeTime.committee, - committeeEmail: committeeEmail.committeeEmail, - applicants, + committeeEmail: committeeEmail.email, + applicants: applicants.flatMap((applicant) => + applicant.interviewTimes.map((interviewTime) => ({ + committeeName: applicant.committeeName, + committeeEmail: applicant.committeeEmail, + interviewTimes: interviewTime, + })) + ), }; committeesToEmail.push(emailCommittee); - // Collect applicants to send email - for (const applicant of applicants) { - if (!applicantsToEmailMap[applicant.committeeEmail]) { - applicantsToEmailMap[applicant.committeeEmail] = { + console.log("her 3"); + for (const app of applicants) { + console.log("her 1"); + if (!applicantsToEmailMap[app.committeeEmail]) { + console.log("her 2"); + applicantsToEmailMap[app.committeeEmail] = { periodId: period._id.toString(), period_name: period.name, - applicantName: applicant.committeeName, - applicantEmail: applicant.committeeEmail, + applicantName: app.committeeName, + applicantEmail: app.committeeEmail, committees: [], }; } - applicantsToEmailMap[applicant.committeeEmail].committees.push(applicant); + applicantsToEmailMap[app.committeeEmail].committees.push( + ...app.interviewTimes.map((interviewTime) => ({ + committeeName: app.committeeName, + committeeEmail: app.committeeEmail, + interviewTimes: interviewTime, + })) + ); } } @@ -92,7 +108,6 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { console.log("Committees To Email:", committeesToEmail); console.log("Applicants To Email:", applicantsToEmail); - // Send out the emails await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); }; @@ -116,8 +131,6 @@ const fetchCommitteeInterviewTimes = async ( try { const response = await fetch(`/api/committees/times/${periodId}`); const data = await response.json(); - console.log(data.committees.committees); - return data.committees.committees; } catch (error) { console.error(error); @@ -177,8 +190,6 @@ const formatAndSendEmails = async ({ // }); console.log( - "sesClient", - sesClient, "fromEmail", "opptak@online.ntnu.no", "toEmails", @@ -215,8 +226,6 @@ const formatAndSendEmails = async ({ // }); console.log( - "sesClient", - sesClient, "fromEmail", "opptak@online.ntnu.no", "toEmails", From f0b938734ff435f94178b804edb8dd3d425ca834 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 22 Jul 2024 22:01:29 +0200 Subject: [PATCH 011/169] . --- lib/utils/sendInterviewTimes.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts index 925409cc..f8333b03 100644 --- a/lib/utils/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes.ts @@ -38,11 +38,7 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { email.name_short.toLowerCase() === committeeTime.committee.toLowerCase() ); - console.log("her 4"); - console.log(`Committee email ${committeeEmail}`); - if (!committeeEmail) continue; - console.log("her 5"); const applicants = algorithmData .filter((app) => @@ -80,11 +76,8 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { committeesToEmail.push(emailCommittee); - console.log("her 3"); for (const app of applicants) { - console.log("her 1"); if (!applicantsToEmailMap[app.committeeEmail]) { - console.log("her 2"); applicantsToEmailMap[app.committeeEmail] = { periodId: period._id.toString(), period_name: period.name, From 20e4687fa24ab4bbc1134b327fb6dbb31616f6ed Mon Sep 17 00:00:00 2001 From: fredrir Date: Tue, 23 Jul 2024 12:05:27 +0200 Subject: [PATCH 012/169] fix applicants for committees --- lib/utils/sendInterviewTimes.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts index f8333b03..3754af48 100644 --- a/lib/utils/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes.ts @@ -32,7 +32,7 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { {}; for (const committeeTime of committeeInterviewTimes) { - // Find the email details for the current committee + console.log("Committee Time:", committeeTime); const committeeEmail = committeeEmails.find( (email) => email.name_short.toLowerCase() === committeeTime.committee.toLowerCase() @@ -41,11 +41,13 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { if (!committeeEmail) continue; const applicants = algorithmData - .filter((app) => - app.interviews.some( - (interview) => interview.committeeName === committeeTime.committee - ) - ) + .filter((app) => { + return app.interviews.some( + (interview) => + interview.committeeName.toLowerCase() === + committeeTime.committee.toLowerCase() + ); + }) .map((app) => ({ committeeName: committeeTime.committee, committeeEmail: committeeEmail.email, From 66eea5e709bb7d8b1e8d7594af1927620150a01e Mon Sep 17 00:00:00 2001 From: fredrir Date: Tue, 23 Jul 2024 14:08:04 +0200 Subject: [PATCH 013/169] redefined types --- lib/types/types.ts | 10 +++---- lib/utils/sendInterviewTimes.ts | 47 ++++++++++++++++++++------------- lib/utils/tempEmailTestData.ts | 17 ++++++++++++ 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/lib/types/types.ts b/lib/types/types.ts index f8563f71..6a7aed34 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -96,7 +96,7 @@ export type committeeInterviewType = { export type algorithmType = { applicantName: string; - + applicantEmail: string; interviews: { start: string; end: string; @@ -115,13 +115,13 @@ export type emailCommitteeInterviewType = { committeeName: string; committeeEmail: string; applicants: { - committeeName: string; - committeeEmail: string; + applicantName: string; + applicantEmail: string; interviewTimes: { start: string; end: string; room: string; - }; + }[]; }[]; }; @@ -137,6 +137,6 @@ export type emailApplicantInterviewType = { start: string; end: string; room: string; - }; + }[]; }[]; }; diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts index 3754af48..e6a50d0a 100644 --- a/lib/utils/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes.ts @@ -10,6 +10,7 @@ import { } from "../types/types"; import { algorithmTestData } from "./tempEmailTestData"; +import { changeDisplayName } from "./toString"; interface Props { periodId: string; @@ -49,6 +50,8 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { ); }) .map((app) => ({ + applicantName: app.applicantName, + applicantEmail: app.applicantEmail, committeeName: committeeTime.committee, committeeEmail: committeeEmail.email, interviewTimes: app.interviews.map((interview) => ({ @@ -67,34 +70,38 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { period_name: period.name, committeeName: committeeTime.committee, committeeEmail: committeeEmail.email, - applicants: applicants.flatMap((applicant) => - applicant.interviewTimes.map((interviewTime) => ({ - committeeName: applicant.committeeName, - committeeEmail: applicant.committeeEmail, - interviewTimes: interviewTime, - })) - ), + applicants: applicants.map((app) => ({ + applicantName: app.applicantName, + applicantEmail: app.applicantEmail, + interviewTimes: app.interviewTimes.map((interview) => ({ + start: interview.start, + end: interview.end, + room: "", + })), + })), }; committeesToEmail.push(emailCommittee); for (const app of applicants) { - if (!applicantsToEmailMap[app.committeeEmail]) { - applicantsToEmailMap[app.committeeEmail] = { + if (!applicantsToEmailMap[app.applicantEmail]) { + applicantsToEmailMap[app.applicantEmail] = { periodId: period._id.toString(), period_name: period.name, - applicantName: app.committeeName, - applicantEmail: app.committeeEmail, + applicantName: app.applicantName, + applicantEmail: app.applicantEmail, committees: [], }; } - applicantsToEmailMap[app.committeeEmail].committees.push( - ...app.interviewTimes.map((interviewTime) => ({ - committeeName: app.committeeName, - committeeEmail: app.committeeEmail, - interviewTimes: interviewTime, - })) - ); + applicantsToEmailMap[app.applicantEmail].committees.push({ + committeeName: app.committeeName, + committeeEmail: app.committeeEmail, + interviewTimes: app.interviewTimes.map((interview) => ({ + start: interview.start, + end: interview.end, + room: "", + })), + }); } } @@ -200,7 +207,9 @@ const formatAndSendEmails = async ({ for (const committee of committeesToEmail) { const typedCommittee: emailCommitteeInterviewType = committee; const committeeEmail = [typedCommittee.committeeEmail]; - const subject = `${typedCommittee.period_name}s sine intervjutider`; + const subject = `${changeDisplayName( + typedCommittee.committeeName + )}s sine intervjutider for ${typedCommittee.period_name}`; let body = `Her er intervjutidene for søkerene deres:\n\n`; typedCommittee.applicants.forEach((applicant: any) => { diff --git a/lib/utils/tempEmailTestData.ts b/lib/utils/tempEmailTestData.ts index b0ed1658..5855f8f9 100644 --- a/lib/utils/tempEmailTestData.ts +++ b/lib/utils/tempEmailTestData.ts @@ -3,6 +3,7 @@ import { algorithmType } from "../types/types"; export const algorithmTestData: algorithmType = [ { applicantName: "Ian Somerhalder", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-07-29T15:00:00Z", @@ -18,6 +19,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Fredrik Hansemann", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-07-29T08:30:00Z", @@ -33,6 +35,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Elena Gilbert", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-07-30T10:00:00Z", @@ -48,6 +51,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Damon Salvatore", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-07-31T09:00:00Z", @@ -63,6 +67,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Bonnie Bennett", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-01T14:00:00Z", @@ -78,6 +83,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Caroline Forbes", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-02T10:30:00Z", @@ -93,6 +99,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Stefan Salvatore", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-03T08:00:00Z", @@ -108,6 +115,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Matt Donovan", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-04T09:00:00Z", @@ -123,6 +131,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Tyler Lockwood", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-05T13:00:00Z", @@ -138,6 +147,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Alaric Saltzman", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-06T09:30:00Z", @@ -153,6 +163,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Jenna Sommers", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-07T08:30:00Z", @@ -168,6 +179,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Jeremy Gilbert", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-08T10:00:00Z", @@ -183,6 +195,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Katherine Pierce", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-09T14:30:00Z", @@ -198,6 +211,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Rebekah Mikaelson", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-10T09:00:00Z", @@ -213,6 +227,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Klaus Mikaelson", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-11T13:00:00Z", @@ -228,6 +243,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Hayley Marshall", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-12T08:30:00Z", @@ -243,6 +259,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Marcel Gerard", + applicantEmail: "fhansteen@gmail.com", interviews: [ { start: "2024-08-13T11:00:00Z", From 1f70bc1f63f99d17aca5ad0926d511bf9d21aeed Mon Sep 17 00:00:00 2001 From: fredrir Date: Tue, 23 Jul 2024 14:35:49 +0200 Subject: [PATCH 014/169] correct formatting of applicants and committees --- lib/types/types.ts | 8 +- lib/utils/sendInterviewTimes.ts | 243 ------------------ .../sendInterviewTimes/fetchFunctions.ts | 48 ++++ .../sendInterviewTimes/sendInterviewTimes.ts | 218 ++++++++++++++++ .../tempEmailTestData.ts | 2 +- pages/admin/[period-id]/index.tsx | 2 +- 6 files changed, 272 insertions(+), 249 deletions(-) delete mode 100644 lib/utils/sendInterviewTimes.ts create mode 100644 lib/utils/sendInterviewTimes/fetchFunctions.ts create mode 100644 lib/utils/sendInterviewTimes/sendInterviewTimes.ts rename lib/utils/{ => sendInterviewTimes}/tempEmailTestData.ts (99%) diff --git a/lib/types/types.ts b/lib/types/types.ts index 6a7aed34..f8e143b5 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -117,11 +117,11 @@ export type emailCommitteeInterviewType = { applicants: { applicantName: string; applicantEmail: string; - interviewTimes: { + interviewTime: { start: string; end: string; room: string; - }[]; + }; }[]; }; @@ -133,10 +133,10 @@ export type emailApplicantInterviewType = { committees: { committeeName: string; committeeEmail: string; - interviewTimes: { + interviewTime: { start: string; end: string; room: string; - }[]; + }; }[]; }; diff --git a/lib/utils/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes.ts deleted file mode 100644 index e6a50d0a..00000000 --- a/lib/utils/sendInterviewTimes.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { SESClient } from "@aws-sdk/client-ses"; -import sendEmail from "./sendEmail"; -import { - committeeEmails, - committeeInterviewType, - emailApplicantInterviewType, - emailCommitteeInterviewType, - periodType, - algorithmType, -} from "../types/types"; - -import { algorithmTestData } from "./tempEmailTestData"; -import { changeDisplayName } from "./toString"; - -interface Props { - periodId: string; -} - -export const sendOutInterviewTimes = async ({ periodId }: Props) => { - const period: periodType = await fetchPeriod(periodId); - const committeeInterviewTimes: committeeInterviewType[] = - await fetchCommitteeInterviewTimes(periodId); - const committeeEmails: committeeEmails[] = await fetchCommitteeEmails(); - - console.log("Period:", period); - console.log("Committee Interview Times:", committeeInterviewTimes); - console.log("Committee Emails:", committeeEmails); - - const algorithmData: algorithmType = algorithmTestData; - - const committeesToEmail: emailCommitteeInterviewType[] = []; - const applicantsToEmailMap: { [key: string]: emailApplicantInterviewType } = - {}; - - for (const committeeTime of committeeInterviewTimes) { - console.log("Committee Time:", committeeTime); - const committeeEmail = committeeEmails.find( - (email) => - email.name_short.toLowerCase() === committeeTime.committee.toLowerCase() - ); - - if (!committeeEmail) continue; - - const applicants = algorithmData - .filter((app) => { - return app.interviews.some( - (interview) => - interview.committeeName.toLowerCase() === - committeeTime.committee.toLowerCase() - ); - }) - .map((app) => ({ - applicantName: app.applicantName, - applicantEmail: app.applicantEmail, - committeeName: committeeTime.committee, - committeeEmail: committeeEmail.email, - interviewTimes: app.interviews.map((interview) => ({ - start: interview.start, - end: interview.end, - room: - committeeTime.availabletimes.find( - (time) => - time.start === interview.start && time.end === interview.end - )?.room || "", - })), - })); - - const emailCommittee: emailCommitteeInterviewType = { - periodId: period._id.toString(), - period_name: period.name, - committeeName: committeeTime.committee, - committeeEmail: committeeEmail.email, - applicants: applicants.map((app) => ({ - applicantName: app.applicantName, - applicantEmail: app.applicantEmail, - interviewTimes: app.interviewTimes.map((interview) => ({ - start: interview.start, - end: interview.end, - room: "", - })), - })), - }; - - committeesToEmail.push(emailCommittee); - - for (const app of applicants) { - if (!applicantsToEmailMap[app.applicantEmail]) { - applicantsToEmailMap[app.applicantEmail] = { - periodId: period._id.toString(), - period_name: period.name, - applicantName: app.applicantName, - applicantEmail: app.applicantEmail, - committees: [], - }; - } - applicantsToEmailMap[app.applicantEmail].committees.push({ - committeeName: app.committeeName, - committeeEmail: app.committeeEmail, - interviewTimes: app.interviewTimes.map((interview) => ({ - start: interview.start, - end: interview.end, - room: "", - })), - }); - } - } - - const applicantsToEmail = Object.values(applicantsToEmailMap); - - console.log("Committees To Email:", committeesToEmail); - console.log("Applicants To Email:", applicantsToEmail); - - await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); -}; - -const fetchPeriod = async (periodId: string): Promise => { - try { - const response = await fetch(`/api/periods/${periodId}`); - const data = await response.json(); - if (!data || !data.period) { - throw new Error("Invalid response from fetchPeriod"); - } - return data.period; - } catch (error) { - console.error(error); - throw new Error("Failed to fetch period"); - } -}; - -const fetchCommitteeInterviewTimes = async ( - periodId: string -): Promise => { - try { - const response = await fetch(`/api/committees/times/${periodId}`); - const data = await response.json(); - return data.committees.committees; - } catch (error) { - console.error(error); - throw new Error("Failed to fetch committee interview times"); - } -}; - -const fetchCommitteeEmails = async (): Promise => { - try { - const response = await fetch(`/api/periods/ow-committees`); - const data = await response.json(); - if (!Array.isArray(data)) { - throw new Error( - "Expected an array from the fetchCommitteeEmails API response" - ); - } - return data; - } catch (error) { - console.error(error); - throw new Error("Failed to fetch committee emails"); - } -}; - -interface sendInterviewTimesProps { - committeesToEmail: emailCommitteeInterviewType[]; - applicantsToEmail: emailApplicantInterviewType[]; -} - -const formatAndSendEmails = async ({ - committeesToEmail, - applicantsToEmail, -}: sendInterviewTimesProps) => { - const sesClient = new SESClient({ region: "eu-north-1" }); - - // Send email to each applicant - for (const applicant of applicantsToEmail) { - const typedApplicant: emailApplicantInterviewType = applicant; - const applicantEmail = [typedApplicant.applicantEmail]; - const subject = `Hei, ${typedApplicant.period_name}. Her er dine intervjutider!`; - let body = `Intervjutider:\n\n`; - - typedApplicant.committees.forEach((committee: any) => { - body += `Komite: ${committee.committeeName}\n`; - body += `Start: ${committee.interviewTimes.start}\n`; - body += `Slutt: ${committee.interviewTimes.end}\n`; - body += `Rom: ${committee.interviewTimes.room}\n\n`; - }); - - body += `Med vennlig hilsen,\nAppkom <3`; - - // await sendEmail({ - // sesClient: sesClient, - // fromEmail: "opptak@online.ntnu.no", - // toEmails: applicantEmail, - // subject: subject, - // htmlContent: body, - // }); - - console.log( - "fromEmail", - "opptak@online.ntnu.no", - "toEmails", - applicantEmail, - "subject", - subject, - "htmlContent", - body - ); - } - - // Send email to each committee - for (const committee of committeesToEmail) { - const typedCommittee: emailCommitteeInterviewType = committee; - const committeeEmail = [typedCommittee.committeeEmail]; - const subject = `${changeDisplayName( - typedCommittee.committeeName - )}s sine intervjutider for ${typedCommittee.period_name}`; - let body = `Her er intervjutidene for søkerene deres:\n\n`; - - typedCommittee.applicants.forEach((applicant: any) => { - body += `Navn: ${applicant.committeeName}\n`; - body += `Start: ${applicant.interviewTimes.start}\n`; - body += `Slutt: ${applicant.interviewTimes.end}\n`; - body += `Rom: ${applicant.interviewTimes.room}\n\n`; - }); - - body += `Med vennlig hilsen, Appkom <3`; - - // await sendEmail({ - // sesClient: sesClient, - // fromEmail: "opptak@online.ntnu.no", - // toEmails: committeeEmail, - // subject: subject, - // htmlContent: body, - // }); - - console.log( - "fromEmail", - "opptak@online.ntnu.no", - "toEmails", - committeeEmail, - "subject", - subject, - "htmlContent", - body - ); - } -}; diff --git a/lib/utils/sendInterviewTimes/fetchFunctions.ts b/lib/utils/sendInterviewTimes/fetchFunctions.ts new file mode 100644 index 00000000..b88c473a --- /dev/null +++ b/lib/utils/sendInterviewTimes/fetchFunctions.ts @@ -0,0 +1,48 @@ +import { + committeeEmails, + committeeInterviewType, + periodType, +} from "../../types/types"; + +export const fetchPeriod = async (periodId: string): Promise => { + try { + const response = await fetch(`/api/periods/${periodId}`); + const data = await response.json(); + if (!data || !data.period) { + throw new Error("Invalid response from fetchPeriod"); + } + return data.period; + } catch (error) { + console.error(error); + throw new Error("Failed to fetch period"); + } +}; + +export const fetchCommitteeInterviewTimes = async ( + periodId: string +): Promise => { + try { + const response = await fetch(`/api/committees/times/${periodId}`); + const data = await response.json(); + return data.committees.committees; + } catch (error) { + console.error(error); + throw new Error("Failed to fetch committee interview times"); + } +}; + +export const fetchCommitteeEmails = async (): Promise => { + try { + const response = await fetch(`/api/periods/ow-committees`); + const data = await response.json(); + if (!Array.isArray(data)) { + throw new Error( + "Expected an array from the fetchCommitteeEmails API response" + ); + } + return data; + } catch (error) { + console.error(error); + throw new Error("Failed to fetch committee emails"); + } +}; diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts new file mode 100644 index 00000000..491bbbd3 --- /dev/null +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -0,0 +1,218 @@ +import { SESClient } from "@aws-sdk/client-ses"; +import sendEmail from "../sendEmail"; +import { + committeeEmails, + committeeInterviewType, + emailApplicantInterviewType, + emailCommitteeInterviewType, + periodType, + algorithmType, +} from "../../types/types"; + +import { algorithmTestData } from "./tempEmailTestData"; +import { changeDisplayName } from "../toString"; +import { + fetchCommitteeEmails, + fetchCommitteeInterviewTimes, + fetchPeriod, +} from "./fetchFunctions"; + +interface Props { + periodId: string; +} + +export const sendOutInterviewTimes = async ({ periodId }: Props) => { + const period: periodType = await fetchPeriod(periodId); + const committeeInterviewTimes: committeeInterviewType[] = + await fetchCommitteeInterviewTimes(periodId); + const committeeEmails: committeeEmails[] = await fetchCommitteeEmails(); + + const algorithmData: algorithmType = algorithmTestData; + + const applicantsToEmailMap = formatApplicants( + algorithmData, + periodId, + period, + committeeEmails, + committeeInterviewTimes + ); + + const committeesToEmail = formatCommittees(applicantsToEmailMap); + + console.log(applicantsToEmailMap); + console.log(committeesToEmail); + + // await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); +}; + +const formatApplicants = ( + algorithmData: algorithmType, + periodId: string, + period: periodType, + committeeEmails: committeeEmails[], + committeeInterviewTimes: committeeInterviewType[] +): emailApplicantInterviewType[] => { + const applicantsToEmailMap: emailApplicantInterviewType[] = []; + + for (const app of algorithmData) { + const committees = app.interviews.map((interview) => { + const committeeEmail = committeeEmails.find( + (email) => + email.name_short.toLowerCase() === + interview.committeeName.toLowerCase() + ); + + const committeeTime = committeeInterviewTimes.find( + (time) => + time.committee.toLowerCase() === + interview.committeeName.toLowerCase() && + time.availabletimes.some( + (available) => + available.start === interview.start && + available.end === interview.end + ) + ); + + return { + committeeName: interview.committeeName, + committeeEmail: committeeEmail?.email || "", + interviewTime: { + start: interview.start, + end: interview.end, + room: + committeeTime?.availabletimes.find( + (available) => + available.start === interview.start && + available.end === interview.end + )?.room || "", + }, + }; + }); + + const applicantToEmail: emailApplicantInterviewType = { + periodId: periodId, + period_name: period.name, + applicantName: app.applicantName, + applicantEmail: app.applicantEmail, + committees: committees, + }; + + applicantsToEmailMap.push(applicantToEmail); + } + + return applicantsToEmailMap; +}; + +const formatCommittees = ( + applicantsToEmailMap: emailApplicantInterviewType[] +): emailCommitteeInterviewType[] => { + const committeesToEmail: { [key: string]: emailCommitteeInterviewType } = {}; + + for (const applicant of applicantsToEmailMap) { + for (const committee of applicant.committees) { + if (!committeesToEmail[committee.committeeName]) { + committeesToEmail[committee.committeeName] = { + periodId: applicant.periodId, + period_name: applicant.period_name, + committeeName: committee.committeeName, + committeeEmail: committee.committeeEmail, + applicants: [], + }; + } + + committeesToEmail[committee.committeeName].applicants.push({ + applicantName: applicant.applicantName, + applicantEmail: applicant.applicantEmail, + interviewTime: committee.interviewTime, + }); + } + } + + return Object.values(committeesToEmail); +}; + +interface sendInterviewTimesProps { + committeesToEmail: emailCommitteeInterviewType[]; + applicantsToEmail: emailApplicantInterviewType[]; +} + +const formatAndSendEmails = async ({ + committeesToEmail, + applicantsToEmail, +}: sendInterviewTimesProps) => { + const sesClient = new SESClient({ region: "eu-north-1" }); + + // Send email to each applicant + // for (const applicant of applicantsToEmail) { + // const typedApplicant: emailApplicantInterviewType = applicant; + // const applicantEmail = [typedApplicant.applicantEmail]; + // const subject = `Hei, ${typedApplicant.period_name}. Her er dine intervjutider!`; + // let body = `Intervjutider:\n\n`; + + // typedApplicant.committees.forEach((committee: any) => { + // body += `Komite: ${committee.committeeName}\n`; + // body += `Start: ${committee.interviewTimes.start}\n`; + // body += `Slutt: ${committee.interviewTimes.end}\n`; + // body += `Rom: ${committee.interviewTimes.room}\n\n`; + // }); + + // body += `Med vennlig hilsen,\nAppkom <3`; + + // // await sendEmail({ + // // sesClient: sesClient, + // // fromEmail: "opptak@online.ntnu.no", + // // toEmails: applicantEmail, + // // subject: subject, + // // htmlContent: body, + // // }); + + // console.log( + // "fromEmail", + // "opptak@online.ntnu.no", + // "toEmails", + // applicantEmail, + // "subject", + // subject, + // "htmlContent", + // body + // ); + // } + + // Send email to each committee + for (const committee of committeesToEmail) { + const typedCommittee: emailCommitteeInterviewType = committee; + const committeeEmail = [typedCommittee.committeeEmail]; + const subject = `${changeDisplayName( + typedCommittee.committeeName + )}s sine intervjutider for ${typedCommittee.period_name}`; + let body = `Her er intervjutidene for søkerene deres:\n\n`; + + typedCommittee.applicants.forEach((applicant: any) => { + body += `Navn: ${applicant.committeeName}\n`; + body += `Start: ${applicant.interviewTimes.start}\n`; + body += `Slutt: ${applicant.interviewTimes.end}\n`; + body += `Rom: ${applicant.interviewTimes.room}\n\n`; + }); + + body += `Med vennlig hilsen, Appkom <3`; + + // await sendEmail({ + // sesClient: sesClient, + // fromEmail: "opptak@online.ntnu.no", + // toEmails: committeeEmail, + // subject: subject, + // htmlContent: body, + // }); + + console.log( + "fromEmail", + "opptak@online.ntnu.no", + "toEmails", + committeeEmail, + "subject", + subject, + "htmlContent", + body + ); + } +}; diff --git a/lib/utils/tempEmailTestData.ts b/lib/utils/sendInterviewTimes/tempEmailTestData.ts similarity index 99% rename from lib/utils/tempEmailTestData.ts rename to lib/utils/sendInterviewTimes/tempEmailTestData.ts index 5855f8f9..c1080e2b 100644 --- a/lib/utils/tempEmailTestData.ts +++ b/lib/utils/sendInterviewTimes/tempEmailTestData.ts @@ -1,4 +1,4 @@ -import { algorithmType } from "../types/types"; +import { algorithmType } from "../../types/types"; export const algorithmTestData: algorithmType = [ { diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index 0cdff8e2..3d897dbc 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -7,7 +7,7 @@ import ApplicantsOverview from "../../../components/applicantoverview/Applicants import { Tabs } from "../../../components/Tabs"; import { CalendarIcon, InboxIcon } from "@heroicons/react/24/solid"; import Button from "../../../components/Button"; -import { sendOutInterviewTimes } from "../../../lib/utils/sendInterviewTimes"; +import { sendOutInterviewTimes } from "../../../lib/utils/sendInterviewTimes/sendInterviewTimes"; const Admin = () => { const { data: session } = useSession(); From 7c721a54dfe46c0afc92199d8765b8a2e7e60401 Mon Sep 17 00:00:00 2001 From: fredrir Date: Tue, 23 Jul 2024 14:52:26 +0200 Subject: [PATCH 015/169] fix formatting --- lib/utils/sendInterviewTimes/formatAndSend.ts | 74 ++++++++++++++ .../sendInterviewTimes/sendInterviewTimes.ts | 98 ++----------------- .../sendInterviewTimes/tempEmailTestData.ts | 32 +++--- 3 files changed, 97 insertions(+), 107 deletions(-) create mode 100644 lib/utils/sendInterviewTimes/formatAndSend.ts diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts new file mode 100644 index 00000000..9bb35899 --- /dev/null +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -0,0 +1,74 @@ +import { SESClient } from "@aws-sdk/client-ses"; +import { + emailCommitteeInterviewType, + emailApplicantInterviewType, +} from "../../types/types"; +import { changeDisplayName } from "../toString"; + +interface sendInterviewTimesProps { + committeesToEmail: emailCommitteeInterviewType[]; + applicantsToEmail: emailApplicantInterviewType[]; +} + +export const formatAndSendEmails = async ({ + committeesToEmail, + applicantsToEmail, +}: sendInterviewTimesProps) => { + const sesClient = new SESClient({ region: "eu-north-1" }); + + // Send email to each applicant + for (const applicant of applicantsToEmail) { + const typedApplicant: emailApplicantInterviewType = applicant; + const applicantEmail = [typedApplicant.applicantEmail]; + const subject = `Hei, ${typedApplicant.applicantName}. Her er dine intervjutider:`; + let body = `\n\n`; + + typedApplicant.committees.forEach((committee) => { + body += `Komite: ${committee.committeeName}\n`; + body += `Start: ${committee.interviewTime.start}\n`; + body += `Slutt: ${committee.interviewTime.end}\n`; + body += `Rom: ${committee.interviewTime.room}\n\n`; + }); + + body += `Med vennlig hilsen,\nAppkom <3`; + + // // await sendEmail({ + // // sesClient: sesClient, + // // fromEmail: "opptak@online.ntnu.no", + // // toEmails: applicantEmail, + // // subject: subject, + // // htmlContent: body, + // // }); + + console.log(applicantEmail[0], "\n", subject, "\n", body); + } + + // Send email to each committee + for (const committee of committeesToEmail) { + const typedCommittee: emailCommitteeInterviewType = committee; + const committeeEmail = [typedCommittee.committeeEmail]; + const subject = `${changeDisplayName( + typedCommittee.committeeName + )}s sine intervjutider for ${typedCommittee.period_name}`; + let body = `Her er intervjutidene for søkerene deres:\n\n`; + + typedCommittee.applicants.forEach((applicant) => { + body += `Navn: ${applicant.applicantName}\n`; + body += `Start: ${applicant.interviewTime.start}\n`; + body += `Slutt: ${applicant.interviewTime.end}\n`; + body += `Rom: ${applicant.interviewTime.room}\n\n`; + }); + + body += `Med vennlig hilsen, Appkom <3`; + + // await sendEmail({ + // sesClient: sesClient, + // fromEmail: "opptak@online.ntnu.no", + // toEmails: committeeEmail, + // subject: subject, + // htmlContent: body, + // }); + + console.log(committeeEmail[0], "\n", subject, "\n", body); + } +}; diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index 491bbbd3..cf7a3b3d 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -16,6 +16,7 @@ import { fetchCommitteeInterviewTimes, fetchPeriod, } from "./fetchFunctions"; +import { formatAndSendEmails } from "./formatAndSend"; interface Props { periodId: string; @@ -29,7 +30,7 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { const algorithmData: algorithmType = algorithmTestData; - const applicantsToEmailMap = formatApplicants( + const applicantsToEmail: emailApplicantInterviewType[] = formatApplicants( algorithmData, periodId, period, @@ -37,12 +38,13 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { committeeInterviewTimes ); - const committeesToEmail = formatCommittees(applicantsToEmailMap); + const committeesToEmail: emailCommitteeInterviewType[] = + formatCommittees(applicantsToEmail); - console.log(applicantsToEmailMap); - console.log(committeesToEmail); + // console.log(applicantsToEmailMap); + // console.log(committeesToEmail); - // await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); + await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); }; const formatApplicants = ( @@ -130,89 +132,3 @@ const formatCommittees = ( return Object.values(committeesToEmail); }; - -interface sendInterviewTimesProps { - committeesToEmail: emailCommitteeInterviewType[]; - applicantsToEmail: emailApplicantInterviewType[]; -} - -const formatAndSendEmails = async ({ - committeesToEmail, - applicantsToEmail, -}: sendInterviewTimesProps) => { - const sesClient = new SESClient({ region: "eu-north-1" }); - - // Send email to each applicant - // for (const applicant of applicantsToEmail) { - // const typedApplicant: emailApplicantInterviewType = applicant; - // const applicantEmail = [typedApplicant.applicantEmail]; - // const subject = `Hei, ${typedApplicant.period_name}. Her er dine intervjutider!`; - // let body = `Intervjutider:\n\n`; - - // typedApplicant.committees.forEach((committee: any) => { - // body += `Komite: ${committee.committeeName}\n`; - // body += `Start: ${committee.interviewTimes.start}\n`; - // body += `Slutt: ${committee.interviewTimes.end}\n`; - // body += `Rom: ${committee.interviewTimes.room}\n\n`; - // }); - - // body += `Med vennlig hilsen,\nAppkom <3`; - - // // await sendEmail({ - // // sesClient: sesClient, - // // fromEmail: "opptak@online.ntnu.no", - // // toEmails: applicantEmail, - // // subject: subject, - // // htmlContent: body, - // // }); - - // console.log( - // "fromEmail", - // "opptak@online.ntnu.no", - // "toEmails", - // applicantEmail, - // "subject", - // subject, - // "htmlContent", - // body - // ); - // } - - // Send email to each committee - for (const committee of committeesToEmail) { - const typedCommittee: emailCommitteeInterviewType = committee; - const committeeEmail = [typedCommittee.committeeEmail]; - const subject = `${changeDisplayName( - typedCommittee.committeeName - )}s sine intervjutider for ${typedCommittee.period_name}`; - let body = `Her er intervjutidene for søkerene deres:\n\n`; - - typedCommittee.applicants.forEach((applicant: any) => { - body += `Navn: ${applicant.committeeName}\n`; - body += `Start: ${applicant.interviewTimes.start}\n`; - body += `Slutt: ${applicant.interviewTimes.end}\n`; - body += `Rom: ${applicant.interviewTimes.room}\n\n`; - }); - - body += `Med vennlig hilsen, Appkom <3`; - - // await sendEmail({ - // sesClient: sesClient, - // fromEmail: "opptak@online.ntnu.no", - // toEmails: committeeEmail, - // subject: subject, - // htmlContent: body, - // }); - - console.log( - "fromEmail", - "opptak@online.ntnu.no", - "toEmails", - committeeEmail, - "subject", - subject, - "htmlContent", - body - ); - } -}; diff --git a/lib/utils/sendInterviewTimes/tempEmailTestData.ts b/lib/utils/sendInterviewTimes/tempEmailTestData.ts index c1080e2b..d1d77a2c 100644 --- a/lib/utils/sendInterviewTimes/tempEmailTestData.ts +++ b/lib/utils/sendInterviewTimes/tempEmailTestData.ts @@ -3,7 +3,7 @@ import { algorithmType } from "../../types/types"; export const algorithmTestData: algorithmType = [ { applicantName: "Ian Somerhalder", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "IanSommerhalder@gmail.com", interviews: [ { start: "2024-07-29T15:00:00Z", @@ -35,7 +35,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Elena Gilbert", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Elena@outlook.com", interviews: [ { start: "2024-07-30T10:00:00Z", @@ -51,7 +51,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Damon Salvatore", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Damon@gmail.com", interviews: [ { start: "2024-07-31T09:00:00Z", @@ -67,7 +67,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Bonnie Bennett", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Bonnie@bennet.no", interviews: [ { start: "2024-08-01T14:00:00Z", @@ -83,7 +83,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Caroline Forbes", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Caroline@gmail.com", interviews: [ { start: "2024-08-02T10:30:00Z", @@ -99,7 +99,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Stefan Salvatore", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "stefan@salvatore.no", interviews: [ { start: "2024-08-03T08:00:00Z", @@ -115,7 +115,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Matt Donovan", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Matt@donavan.com", interviews: [ { start: "2024-08-04T09:00:00Z", @@ -131,7 +131,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Tyler Lockwood", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Tyler@lockwood.com", interviews: [ { start: "2024-08-05T13:00:00Z", @@ -147,7 +147,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Alaric Saltzman", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Alaric@yahoo.com", interviews: [ { start: "2024-08-06T09:30:00Z", @@ -163,7 +163,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Jenna Sommers", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Jenna@outlook.com", interviews: [ { start: "2024-08-07T08:30:00Z", @@ -179,7 +179,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Jeremy Gilbert", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Jeremy@yahoo.com", interviews: [ { start: "2024-08-08T10:00:00Z", @@ -195,7 +195,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Katherine Pierce", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Katherine@gmail.com", interviews: [ { start: "2024-08-09T14:30:00Z", @@ -211,7 +211,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Rebekah Mikaelson", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Rebakah@gmail.com", interviews: [ { start: "2024-08-10T09:00:00Z", @@ -227,7 +227,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Klaus Mikaelson", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Klaus@outlook.com", interviews: [ { start: "2024-08-11T13:00:00Z", @@ -243,7 +243,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Hayley Marshall", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Hayley@gmail.com", interviews: [ { start: "2024-08-12T08:30:00Z", @@ -259,7 +259,7 @@ export const algorithmTestData: algorithmType = [ }, { applicantName: "Marcel Gerard", - applicantEmail: "fhansteen@gmail.com", + applicantEmail: "Marcel@Gerard.com", interviews: [ { start: "2024-08-13T11:00:00Z", From fa55d985ee9bca5ab92b47268f1f193bc4231456 Mon Sep 17 00:00:00 2001 From: fredrir Date: Tue, 23 Jul 2024 14:56:39 +0200 Subject: [PATCH 016/169] Add room if within time period --- .../sendInterviewTimes/sendInterviewTimes.ts | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index cf7a3b3d..d7318013 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -66,32 +66,33 @@ const formatApplicants = ( const committeeTime = committeeInterviewTimes.find( (time) => - time.committee.toLowerCase() === - interview.committeeName.toLowerCase() && - time.availabletimes.some( - (available) => - available.start === interview.start && - available.end === interview.end - ) + time.committee.toLowerCase() === interview.committeeName.toLowerCase() ); + let room = ""; + + if (committeeTime) { + const availableTime = committeeTime.availabletimes.find( + (available) => + available.start <= interview.start && available.end >= interview.end + ); + if (availableTime) { + room = availableTime.room; + } + } + return { committeeName: interview.committeeName, committeeEmail: committeeEmail?.email || "", interviewTime: { start: interview.start, end: interview.end, - room: - committeeTime?.availabletimes.find( - (available) => - available.start === interview.start && - available.end === interview.end - )?.room || "", + room: room, }, }; }); - const applicantToEmail: emailApplicantInterviewType = { + const applicantToEmail = { periodId: periodId, period_name: period.name, applicantName: app.applicantName, From 449ce760217487db44f8b026375a9830ded52be9 Mon Sep 17 00:00:00 2001 From: fredrir Date: Tue, 23 Jul 2024 15:27:41 +0200 Subject: [PATCH 017/169] padding --- pages/admin/[period-id]/index.tsx | 66 ++++++++++++++++--------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index 3d897dbc..67abd199 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -47,39 +47,41 @@ const Admin = () => { } return ( - { - setActiveTab(index); - setTabClicked(index); - }} - content={[ - { - title: "Søkere", - icon: , - content: ( - - ), - }, - { - title: "Send ut", - icon: , - content: ( -
-
+ ), + }, + ]} + /> + ); }; From d616beb51a0eed0fdc6952d5b7741e5711cf931e Mon Sep 17 00:00:00 2001 From: fredrir Date: Tue, 23 Jul 2024 15:35:16 +0200 Subject: [PATCH 018/169] date formatting --- lib/utils/dateUtils.ts | 12 ++++++++++++ lib/utils/sendInterviewTimes/formatAndSend.ts | 17 +++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/utils/dateUtils.ts b/lib/utils/dateUtils.ts index d6837583..c2ddfe7b 100644 --- a/lib/utils/dateUtils.ts +++ b/lib/utils/dateUtils.ts @@ -10,6 +10,18 @@ export const formatDate = (inputDate: undefined | Date) => { return `${day}.${month}.${year}`; // - ${hours}:${minutes} }; +export const formatDateHours = (inputDate: undefined | Date) => { + const date = new Date(inputDate || ""); + + const day = date.getDate().toString().padStart(2, "0"); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const year = date.getFullYear(); + const hours = date.getHours().toString().padStart(2, "0"); + const minutes = date.getMinutes().toString().padStart(2, "0"); + + return `${formatDateNorwegian(inputDate)}, ${hours}:${minutes}`; // - ${hours}:${minutes} +}; + export const formatDateNorwegian = (inputDate?: Date): string => { const date = new Date(inputDate || ""); diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 9bb35899..db967e1e 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -4,6 +4,7 @@ import { emailApplicantInterviewType, } from "../../types/types"; import { changeDisplayName } from "../toString"; +import { formatDateHours } from "../dateUtils"; interface sendInterviewTimesProps { committeesToEmail: emailCommitteeInterviewType[]; @@ -25,8 +26,12 @@ export const formatAndSendEmails = async ({ typedApplicant.committees.forEach((committee) => { body += `Komite: ${committee.committeeName}\n`; - body += `Start: ${committee.interviewTime.start}\n`; - body += `Slutt: ${committee.interviewTime.end}\n`; + body += `Start: ${formatDateHours( + new Date(committee.interviewTime.start) + )}\n`; + body += `Slutt: ${formatDateHours( + new Date(committee.interviewTime.end) + )}\n`; body += `Rom: ${committee.interviewTime.room}\n\n`; }); @@ -54,8 +59,12 @@ export const formatAndSendEmails = async ({ typedCommittee.applicants.forEach((applicant) => { body += `Navn: ${applicant.applicantName}\n`; - body += `Start: ${applicant.interviewTime.start}\n`; - body += `Slutt: ${applicant.interviewTime.end}\n`; + body += `Start: ${formatDateHours( + new Date(applicant.interviewTime.start) + )}\n`; + body += `Slutt: ${formatDateHours( + new Date(applicant.interviewTime.end) + )}\n`; body += `Rom: ${applicant.interviewTime.room}\n\n`; }); From 0d3bcfedde4b02d7ad1a4cef615833c9a41d8fff Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 12:09:57 +0200 Subject: [PATCH 019/169] . --- lib/utils/sendInterviewTimes/tempEmailTestData.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/utils/sendInterviewTimes/tempEmailTestData.ts b/lib/utils/sendInterviewTimes/tempEmailTestData.ts index d1d77a2c..0c09cba1 100644 --- a/lib/utils/sendInterviewTimes/tempEmailTestData.ts +++ b/lib/utils/sendInterviewTimes/tempEmailTestData.ts @@ -12,7 +12,7 @@ export const algorithmTestData: algorithmType = [ }, { start: "2024-07-29T15:00:00Z", - end: "2024-07-29T16:00:00Z", + end: "2024-07-29T12:00:00Z", committeeName: "Appkom", }, ], @@ -28,7 +28,7 @@ export const algorithmTestData: algorithmType = [ }, { start: "2024-07-29T15:00:00Z", - end: "2024-07-29T16:00:00Z", + end: "2024-07-20T12:00:00Z", committeeName: "Appkom", }, ], From 509bf4f74833622854ecbab988e058a6a42a2437 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 13:42:21 +0200 Subject: [PATCH 020/169] match meetings from DB --- .../bridge/fetch_applicants_and_committees.py | 58 +++++++++++++++++-- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index b01cf645..85056bb6 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -3,26 +3,44 @@ from datetime import datetime, timezone import os import certifi +import itertools +from typing import TypedDict, List +import mip + +from mip_matching.Committee import Committee +from mip_matching.TimeInterval import TimeInterval +from mip_matching.Applicant import Applicant +from mip_matching.match_meetings import match_meetings, MeetingMatch def main(): periods = fetch_periods() - #Sjekker om perioden er etter søknadstiden og før intervjuslutt og hasSentInterviewtimes er false, og returnerer søkere og komitétider dersom det er tilfelle for period in periods: periodId = str(period["_id"]) interview_end = datetime.fromisoformat(period["interviewPeriod"]["end"].replace("Z", "+00:00")) application_end = datetime.fromisoformat(period["applicationPeriod"]["end"].replace("Z", "+00:00")) - now = datetime.now(timezone.utc) - if application_end > now and period["hasSentInterviewTimes"] == False and interview_end < now: + if (application_end > now and period["hasSentInterviewTimes"] == False and interview_end < now) or period["name"] == "Juli Opptak": applicants = fetch_applicants(periodId) committee_times = fetch_committee_times(periodId) - print(applicants) - print(committee_times) - return applicants, committee_times + applicant_objects = create_applicant_objects(applicants) + committee_objects = create_committee_objects(committee_times) + + print(applicant_objects) + print(committee_objects) + + match_result = match_meetings(applicant_objects, committee_objects) + + send_to_db(match_result) + return match_result + + +def send_to_db(match_result: MeetingMatch): + print("Sending to db") + print(match_result) def connect_to_db(collection_name): @@ -72,5 +90,33 @@ def fetch_committee_times(periodId): return committee_times +def create_applicant_objects(applicants_data: List[dict]) -> set[Applicant]: + applicants = set() + for data in applicants_data: + applicant = Applicant(name=data['name']) + for interval_data in data['selectedTimes']: + interval = TimeInterval( + start=datetime.fromisoformat(interval_data['start'].replace("Z", "+00:00")), + end=datetime.fromisoformat(interval_data['end'].replace("Z", "+00:00")) + ) + applicant.add_interval(interval) + applicants.add(applicant) + return applicants + +def create_committee_objects(committee_data: List[dict]) -> set[Committee]: + committees = set() + for data in committee_data: + committee = Committee(name=data['committee']) + for interval_data in data['availabletimes']: + interval = TimeInterval( + start=datetime.fromisoformat(interval_data['start'].replace("Z", "+00:00")), + end=datetime.fromisoformat(interval_data['end'].replace("Z", "+00:00")) + ) + capacity = interval_data.get('capacity', 1) + committee.add_interval(interval, capacity) + committees.add(committee) + return committees + + if __name__ == "__main__": main() \ No newline at end of file From eb462709ac7b04d70e82132208efe84ce74c19a8 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 14:20:10 +0200 Subject: [PATCH 021/169] add committee object to applicants --- .../bridge/fetch_applicants_and_committees.py | 25 +++++++++++++++++-- algorithm/src/mip_matching/match_meetings.py | 3 +++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 85056bb6..22a4d7fc 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -26,7 +26,12 @@ def main(): applicants = fetch_applicants(periodId) committee_times = fetch_committee_times(periodId) - applicant_objects = create_applicant_objects(applicants) + + committee_objects = create_committee_objects(committee_times) + + all_committees = {committee.name: committee for committee in committee_objects} + + applicant_objects = create_applicant_objects(applicants, all_committees) committee_objects = create_committee_objects(committee_times) print(applicant_objects) @@ -90,19 +95,35 @@ def fetch_committee_times(periodId): return committee_times -def create_applicant_objects(applicants_data: List[dict]) -> set[Applicant]: +from typing import List +from datetime import datetime + +def create_applicant_objects(applicants_data: List[dict], all_committees: dict[str, Committee]) -> set[Applicant]: applicants = set() for data in applicants_data: applicant = Applicant(name=data['name']) + + optional_committee_names = data.get('optionalCommittees', []) + optional_committees = {all_committees[name] for name in optional_committee_names if name in all_committees} + applicant.add_committees(optional_committees) + + preferences = data.get('preferences', {}) + preference_committees = {all_committees[committee_name] for committee_name in preferences.values() if committee_name in all_committees} + applicant.add_committees(preference_committees) + for interval_data in data['selectedTimes']: interval = TimeInterval( start=datetime.fromisoformat(interval_data['start'].replace("Z", "+00:00")), end=datetime.fromisoformat(interval_data['end'].replace("Z", "+00:00")) ) applicant.add_interval(interval) + applicants.add(applicant) return applicants + + + def create_committee_objects(committee_data: List[dict]) -> set[Committee]: committees = set() for data in committee_data: diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index 9bbd512e..eb6bdf11 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -19,6 +19,9 @@ class MeetingMatch(TypedDict): def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> MeetingMatch: """Matches meetings and returns a MeetingMatch-object""" model = mip.Model(sense=mip.MAXIMIZE) + + for applicant in applicants: + print(f"Applicant: {applicant.name}, Committees: {[str(committee) for committee in applicant.get_committees()]}") m = {} From 9ea990749064be6fc42c9b11721458c82a387817 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 14:21:06 +0200 Subject: [PATCH 022/169] remove unused imports --- algorithm/bridge/fetch_applicants_and_committees.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 22a4d7fc..12c993ea 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -3,9 +3,7 @@ from datetime import datetime, timezone import os import certifi -import itertools -from typing import TypedDict, List -import mip +from typing import List from mip_matching.Committee import Committee from mip_matching.TimeInterval import TimeInterval @@ -95,8 +93,7 @@ def fetch_committee_times(periodId): return committee_times -from typing import List -from datetime import datetime + def create_applicant_objects(applicants_data: List[dict], all_committees: dict[str, Committee]) -> set[Applicant]: applicants = set() From e39508d6ebafb0219c8926dfaf619fa7693daa34 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 14:48:52 +0200 Subject: [PATCH 023/169] add email to applicants and send to DB --- .../bridge/fetch_applicants_and_committees.py | 46 +++++++++++++++++-- algorithm/src/mip_matching/Applicant.py | 3 +- algorithm/src/mip_matching/match_meetings.py | 18 ++++---- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 12c993ea..01bbb9e6 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -3,7 +3,7 @@ from datetime import datetime, timezone import os import certifi -from typing import List +from typing import List, Dict from mip_matching.Committee import Committee from mip_matching.TimeInterval import TimeInterval @@ -42,8 +42,23 @@ def main(): def send_to_db(match_result: MeetingMatch): + load_dotenv() + formatted_results = format_match_results(match_result) print("Sending to db") - print(match_result) + print(formatted_results) + + mongo_uri = os.getenv("MONGODB_URI") + db_name = os.getenv("DB_NAME") + client = MongoClient(mongo_uri, tlsCAFile=certifi.where()) + + db = client[db_name] # type: ignore + + collection = db["interviews"] + + collection.insert_many(formatted_results) + + client.close() + def connect_to_db(collection_name): @@ -93,12 +108,37 @@ def fetch_committee_times(periodId): return committee_times +def format_match_results(match_results: MeetingMatch) -> List[Dict]: + transformed_results = {} + + for result in match_results['matchings']: + email, _, committee, time_interval = result + start = time_interval.start.isoformat() + end = time_interval.end.isoformat() + + if email not in transformed_results: + transformed_results[email] = { + "applicantName": result[1].name, + "applicantEmail": email, + "interviews": [] + } + + transformed_results[email]["interviews"].append({ + "start": start, + "end": end, + "committeeName": committee.name + }) + + return list(transformed_results.values()) + + def create_applicant_objects(applicants_data: List[dict], all_committees: dict[str, Committee]) -> set[Applicant]: applicants = set() for data in applicants_data: - applicant = Applicant(name=data['name']) + applicant = Applicant(name=data['name'], email=data['email']) + optional_committee_names = data.get('optionalCommittees', []) optional_committees = {all_committees[name] for name in optional_committee_names if name in all_committees} diff --git a/algorithm/src/mip_matching/Applicant.py b/algorithm/src/mip_matching/Applicant.py index d769ebc7..5acb2cfb 100644 --- a/algorithm/src/mip_matching/Applicant.py +++ b/algorithm/src/mip_matching/Applicant.py @@ -15,10 +15,11 @@ class Applicant: komitéer hen har søkt på, og når søkeren kan ha intervjuer. """ - def __init__(self, name: str): + def __init__(self, name: str, email: str): self.committees: list[Committee] = [] self.slots: set[TimeInterval] = set() self.name = name + self.email = email def add_committee(self, committee: Committee) -> None: self.committees.append(committee) diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index eb6bdf11..b372fad9 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -1,5 +1,4 @@ from typing import TypedDict - from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee from mip_matching.Applicant import Applicant @@ -13,8 +12,7 @@ class MeetingMatch(TypedDict): solver_status: mip.OptimizationStatus matched_meetings: int total_wanted_meetings: int - matchings: list[tuple[Applicant, Committee, TimeInterval]] - + matchings: list[tuple[str, Applicant, Committee, TimeInterval]] def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> MeetingMatch: """Matches meetings and returns a MeetingMatch-object""" @@ -68,22 +66,22 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me solver_status = model.optimize() # Få de faktiske møtetidene - antall_matchede_møter: int = 0 + matched_meetings: int = 0 matchings: list = [] for name, variable in m.items(): if variable.x: - antall_matchede_møter += 1 - matchings.append(name) + matched_meetings += 1 + matchings.append((name[0].email, *name)) print(f"{name}") - antall_ønskede_møter = sum( + total_wanted_meetings = sum( len(applicant.get_committees()) for applicant in applicants) match_object: MeetingMatch = { "solver_status": solver_status, - "matched_meetings": antall_matchede_møter, - "total_wanted_meetings": antall_ønskede_møter, + "matched_meetings": matched_meetings, + "total_wanted_meetings": total_wanted_meetings, "matchings": matchings, } - return match_object + return match_object \ No newline at end of file From 06b17d59d41ad1513f1763885788c5a084ff9f25 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 15:29:15 +0200 Subject: [PATCH 024/169] use ID as unique identifier --- .../bridge/fetch_applicants_and_committees.py | 37 ++++++++++--------- algorithm/src/mip_matching/Applicant.py | 3 +- algorithm/src/mip_matching/match_meetings.py | 7 ++-- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 01bbb9e6..51b1c386 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -37,13 +37,13 @@ def main(): match_result = match_meetings(applicant_objects, committee_objects) - send_to_db(match_result) + send_to_db(match_result, applicants) return match_result -def send_to_db(match_result: MeetingMatch): +def send_to_db(match_result: MeetingMatch, applicants: List[dict]): load_dotenv() - formatted_results = format_match_results(match_result) + formatted_results = format_match_results(match_result, applicants) print("Sending to db") print(formatted_results) @@ -108,36 +108,39 @@ def fetch_committee_times(periodId): return committee_times -def format_match_results(match_results: MeetingMatch) -> List[Dict]: +def format_match_results(match_results: MeetingMatch, applicants: List[dict]) -> List[Dict]: transformed_results = {} - + applicant_dict = {str(applicant['_id']): applicant['name'] for applicant in applicants} + for result in match_results['matchings']: - email, _, committee, time_interval = result - start = time_interval.start.isoformat() - end = time_interval.end.isoformat() + applicant_id = str(result[1]) + applicant_name = applicant_dict.get(applicant_id, 'Unknown') - if email not in transformed_results: - transformed_results[email] = { - "applicantName": result[1].name, - "applicantEmail": email, + if applicant_id not in transformed_results: + transformed_results[applicant_id] = { + "applicantId": applicant_id, + "applicantName": applicant_name, "interviews": [] } - transformed_results[email]["interviews"].append({ + committee = result[2] + time_interval = result[3] + start = time_interval.start.isoformat() + end = time_interval.end.isoformat() + + transformed_results[applicant_id]["interviews"].append({ "start": start, "end": end, - "committeeName": committee.name + "committeeName": committee.name }) return list(transformed_results.values()) - - def create_applicant_objects(applicants_data: List[dict], all_committees: dict[str, Committee]) -> set[Applicant]: applicants = set() for data in applicants_data: - applicant = Applicant(name=data['name'], email=data['email']) + applicant = Applicant(name=data['name'], email=data['email'], id=data['_id']) optional_committee_names = data.get('optionalCommittees', []) diff --git a/algorithm/src/mip_matching/Applicant.py b/algorithm/src/mip_matching/Applicant.py index 5acb2cfb..5a08061c 100644 --- a/algorithm/src/mip_matching/Applicant.py +++ b/algorithm/src/mip_matching/Applicant.py @@ -15,11 +15,12 @@ class Applicant: komitéer hen har søkt på, og når søkeren kan ha intervjuer. """ - def __init__(self, name: str, email: str): + def __init__(self, name: str, email: str, id: str): self.committees: list[Committee] = [] self.slots: set[TimeInterval] = set() self.name = name self.email = email + self.id = id def add_committee(self, committee: Committee) -> None: self.committees.append(committee) diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index b372fad9..f4b9a6cd 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -68,11 +68,12 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me # Få de faktiske møtetidene matched_meetings: int = 0 matchings: list = [] - for name, variable in m.items(): + for (applicant, committee, interval), variable in m.items(): if variable.x: matched_meetings += 1 - matchings.append((name[0].email, *name)) - print(f"{name}") + matchings.append((applicant.email, applicant.id, committee, interval)) + print(f"{(applicant, committee, interval)}") + total_wanted_meetings = sum( len(applicant.get_committees()) for applicant in applicants) From 8d4c6b2c5f89b6ad7308ee0747be5785f6c253e7 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 15:29:54 +0200 Subject: [PATCH 025/169] remove special test condition --- algorithm/bridge/fetch_applicants_and_committees.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 51b1c386..640c3bf8 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -20,7 +20,7 @@ def main(): now = datetime.now(timezone.utc) - if (application_end > now and period["hasSentInterviewTimes"] == False and interview_end < now) or period["name"] == "Juli Opptak": + if (application_end > now and period["hasSentInterviewTimes"] == False and interview_end < now): applicants = fetch_applicants(periodId) committee_times = fetch_committee_times(periodId) From 124f42f961cf6d1e1b0b7a8f6a1b4266e015660e Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 16:05:42 +0200 Subject: [PATCH 026/169] get interviews from DB --- lib/mongo/interviews.ts | 33 +++ lib/types/types.ts | 2 +- .../sendInterviewTimes/fetchFunctions.ts | 18 ++ .../sendInterviewTimes/sendInterviewTimes.ts | 6 +- .../sendInterviewTimes/tempEmailTestData.ts | 276 ------------------ pages/api/interviews/[period-id].ts | 40 +++ 6 files changed, 95 insertions(+), 280 deletions(-) create mode 100644 lib/mongo/interviews.ts delete mode 100644 lib/utils/sendInterviewTimes/tempEmailTestData.ts create mode 100644 pages/api/interviews/[period-id].ts diff --git a/lib/mongo/interviews.ts b/lib/mongo/interviews.ts new file mode 100644 index 00000000..7a6f6a5a --- /dev/null +++ b/lib/mongo/interviews.ts @@ -0,0 +1,33 @@ +import { Collection, Db, MongoClient } from "mongodb"; +import clientPromise from "./mongodb"; +import { algorithmType } from "../types/types"; + +let client: MongoClient; +let db: Db; +let interviews: Collection; + +async function init() { + if (db) return; + try { + client = await clientPromise; + db = client.db(); + interviews = db.collection("interviews"); + } catch (error) { + console.error(error); + throw new Error("Failed to establish connection to database"); + } +} + +(async () => { + await init(); +})(); + +export const getInterviewsByPeriod = async (periodId: string) => { + try { + if (!interviews) await init(); + const result = await interviews.find({ periodId: periodId }).toArray(); + return { interviews: result }; + } catch (error) { + return { error: "Failed to fetch interviews" }; + } +}; diff --git a/lib/types/types.ts b/lib/types/types.ts index 1fcc0570..4318cce0 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -113,7 +113,7 @@ export type algorithmType = { end: string; committeeName: string; }[]; -}[]; +}; export type committeeEmails = { name_short: string; diff --git a/lib/utils/sendInterviewTimes/fetchFunctions.ts b/lib/utils/sendInterviewTimes/fetchFunctions.ts index b88c473a..70a15396 100644 --- a/lib/utils/sendInterviewTimes/fetchFunctions.ts +++ b/lib/utils/sendInterviewTimes/fetchFunctions.ts @@ -1,4 +1,5 @@ import { + algorithmType, committeeEmails, committeeInterviewType, periodType, @@ -46,3 +47,20 @@ export const fetchCommitteeEmails = async (): Promise => { throw new Error("Failed to fetch committee emails"); } }; + +export const fetchAlgorithmData = async ( + periodId: string +): Promise => { + try { + const reponse = await fetch(`/interviews/${periodId}`); + const data = await reponse.json(); + if (!Array.isArray(data)) { + throw new Error("Expected an array from the interviews API response"); + } + console.log(data); + return data; + } catch (error) { + console.error(error); + throw new Error("Failed to fetch algorithm data"); + } +}; diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index d7318013..104afcd4 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -9,9 +9,9 @@ import { algorithmType, } from "../../types/types"; -import { algorithmTestData } from "./tempEmailTestData"; import { changeDisplayName } from "../toString"; import { + fetchAlgorithmData, fetchCommitteeEmails, fetchCommitteeInterviewTimes, fetchPeriod, @@ -28,7 +28,7 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { await fetchCommitteeInterviewTimes(periodId); const committeeEmails: committeeEmails[] = await fetchCommitteeEmails(); - const algorithmData: algorithmType = algorithmTestData; + const algorithmData: algorithmType[] = await fetchAlgorithmData(periodId); const applicantsToEmail: emailApplicantInterviewType[] = formatApplicants( algorithmData, @@ -48,7 +48,7 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { }; const formatApplicants = ( - algorithmData: algorithmType, + algorithmData: algorithmType[], periodId: string, period: periodType, committeeEmails: committeeEmails[], diff --git a/lib/utils/sendInterviewTimes/tempEmailTestData.ts b/lib/utils/sendInterviewTimes/tempEmailTestData.ts deleted file mode 100644 index 0c09cba1..00000000 --- a/lib/utils/sendInterviewTimes/tempEmailTestData.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { algorithmType } from "../../types/types"; - -export const algorithmTestData: algorithmType = [ - { - applicantName: "Ian Somerhalder", - applicantEmail: "IanSommerhalder@gmail.com", - interviews: [ - { - start: "2024-07-29T15:00:00Z", - end: "2024-07-29T16:00:00Z", - committeeName: "Bedkom", - }, - { - start: "2024-07-29T15:00:00Z", - end: "2024-07-29T12:00:00Z", - committeeName: "Appkom", - }, - ], - }, - { - applicantName: "Fredrik Hansemann", - applicantEmail: "fhansteen@gmail.com", - interviews: [ - { - start: "2024-07-29T08:30:00Z", - end: "2024-07-29T09:00:00Z", - committeeName: "Bedkom", - }, - { - start: "2024-07-29T15:00:00Z", - end: "2024-07-20T12:00:00Z", - committeeName: "Appkom", - }, - ], - }, - { - applicantName: "Elena Gilbert", - applicantEmail: "Elena@outlook.com", - interviews: [ - { - start: "2024-07-30T10:00:00Z", - end: "2024-07-30T11:00:00Z", - committeeName: "Dotkom", - }, - { - start: "2024-07-30T12:00:00Z", - end: "2024-07-30T13:00:00Z", - committeeName: "Fagkom", - }, - ], - }, - { - applicantName: "Damon Salvatore", - applicantEmail: "Damon@gmail.com", - interviews: [ - { - start: "2024-07-31T09:00:00Z", - end: "2024-07-31T10:00:00Z", - committeeName: "Ekskom", - }, - { - start: "2024-07-31T11:00:00Z", - end: "2024-07-31T12:00:00Z", - committeeName: "Bedkom", - }, - ], - }, - { - applicantName: "Bonnie Bennett", - applicantEmail: "Bonnie@bennet.no", - interviews: [ - { - start: "2024-08-01T14:00:00Z", - end: "2024-08-01T15:00:00Z", - committeeName: "Appkom", - }, - { - start: "2024-08-01T16:00:00Z", - end: "2024-08-01T17:00:00Z", - committeeName: "Dotkom", - }, - ], - }, - { - applicantName: "Caroline Forbes", - applicantEmail: "Caroline@gmail.com", - interviews: [ - { - start: "2024-08-02T10:30:00Z", - end: "2024-08-02T11:30:00Z", - committeeName: "Fagkom", - }, - { - start: "2024-08-02T12:30:00Z", - end: "2024-08-02T13:30:00Z", - committeeName: "Ekskom", - }, - ], - }, - { - applicantName: "Stefan Salvatore", - applicantEmail: "stefan@salvatore.no", - interviews: [ - { - start: "2024-08-03T08:00:00Z", - end: "2024-08-03T09:00:00Z", - committeeName: "Bedkom", - }, - { - start: "2024-08-03T10:00:00Z", - end: "2024-08-03T11:00:00Z", - committeeName: "Appkom", - }, - ], - }, - { - applicantName: "Matt Donovan", - applicantEmail: "Matt@donavan.com", - interviews: [ - { - start: "2024-08-04T09:00:00Z", - end: "2024-08-04T10:00:00Z", - committeeName: "Dotkom", - }, - { - start: "2024-08-04T11:00:00Z", - end: "2024-08-04T12:00:00Z", - committeeName: "Fagkom", - }, - ], - }, - { - applicantName: "Tyler Lockwood", - applicantEmail: "Tyler@lockwood.com", - interviews: [ - { - start: "2024-08-05T13:00:00Z", - end: "2024-08-05T14:00:00Z", - committeeName: "Ekskom", - }, - { - start: "2024-08-05T15:00:00Z", - end: "2024-08-05T16:00:00Z", - committeeName: "Bedkom", - }, - ], - }, - { - applicantName: "Alaric Saltzman", - applicantEmail: "Alaric@yahoo.com", - interviews: [ - { - start: "2024-08-06T09:30:00Z", - end: "2024-08-06T10:30:00Z", - committeeName: "Appkom", - }, - { - start: "2024-08-06T11:30:00Z", - end: "2024-08-06T12:30:00Z", - committeeName: "Dotkom", - }, - ], - }, - { - applicantName: "Jenna Sommers", - applicantEmail: "Jenna@outlook.com", - interviews: [ - { - start: "2024-08-07T08:30:00Z", - end: "2024-08-07T09:30:00Z", - committeeName: "Fagkom", - }, - { - start: "2024-08-07T10:30:00Z", - end: "2024-08-07T11:30:00Z", - committeeName: "Ekskom", - }, - ], - }, - { - applicantName: "Jeremy Gilbert", - applicantEmail: "Jeremy@yahoo.com", - interviews: [ - { - start: "2024-08-08T10:00:00Z", - end: "2024-08-08T11:00:00Z", - committeeName: "Bedkom", - }, - { - start: "2024-08-08T12:00:00Z", - end: "2024-08-08T13:00:00Z", - committeeName: "Appkom", - }, - ], - }, - { - applicantName: "Katherine Pierce", - applicantEmail: "Katherine@gmail.com", - interviews: [ - { - start: "2024-08-09T14:30:00Z", - end: "2024-08-09T15:30:00Z", - committeeName: "Dotkom", - }, - { - start: "2024-08-09T16:30:00Z", - end: "2024-08-09T17:30:00Z", - committeeName: "Fagkom", - }, - ], - }, - { - applicantName: "Rebekah Mikaelson", - applicantEmail: "Rebakah@gmail.com", - interviews: [ - { - start: "2024-08-10T09:00:00Z", - end: "2024-08-10T10:00:00Z", - committeeName: "Ekskom", - }, - { - start: "2024-08-10T11:00:00Z", - end: "2024-08-10T12:00:00Z", - committeeName: "Bedkom", - }, - ], - }, - { - applicantName: "Klaus Mikaelson", - applicantEmail: "Klaus@outlook.com", - interviews: [ - { - start: "2024-08-11T13:00:00Z", - end: "2024-08-11T14:00:00Z", - committeeName: "Appkom", - }, - { - start: "2024-08-11T15:00:00Z", - end: "2024-08-11T16:00:00Z", - committeeName: "Dotkom", - }, - ], - }, - { - applicantName: "Hayley Marshall", - applicantEmail: "Hayley@gmail.com", - interviews: [ - { - start: "2024-08-12T08:30:00Z", - end: "2024-08-12T09:30:00Z", - committeeName: "Fagkom", - }, - { - start: "2024-08-12T10:30:00Z", - end: "2024-08-12T11:30:00Z", - committeeName: "Ekskom", - }, - ], - }, - { - applicantName: "Marcel Gerard", - applicantEmail: "Marcel@Gerard.com", - interviews: [ - { - start: "2024-08-13T11:00:00Z", - end: "2024-08-13T12:00:00Z", - committeeName: "Bedkom", - }, - { - start: "2024-08-13T13:00:00Z", - end: "2024-08-13T14:00:00Z", - committeeName: "Appkom", - }, - ], - }, -]; diff --git a/pages/api/interviews/[period-id].ts b/pages/api/interviews/[period-id].ts new file mode 100644 index 00000000..07bf5c40 --- /dev/null +++ b/pages/api/interviews/[period-id].ts @@ -0,0 +1,40 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { getServerSession } from "next-auth"; +import { authOptions } from "../auth/[...nextauth]"; +import { + hasSession, + isAdmin, + isInCommitee, +} from "../../../lib/utils/apiChecks"; +import { getInterviewsByPeriod } from "../../../lib/mongo/interviews"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getServerSession(req, res, authOptions); + const periodId = req.query["period-id"]; + + if (!periodId || typeof periodId !== "string") { + return res + .status(400) + .json({ error: "Invalid or missing periodId parameter" }); + } + + if (!hasSession(res, session)) return; + if (!isInCommitee(res, session)) return; + + if (req.method === "GET") { + if (!isAdmin(res, session)) return; + + try { + const interviews = await getInterviewsByPeriod(periodId); + return res.status(200).json({ interviews }); + } catch (error: any) { + return res.status(500).json({ error: error.message }); + } + } + + res.setHeader("Allow", ["GET"]); + res.status(405).end(`Method ${req.method} is not allowed.`); +}; + +export default handler; From b02f495e7b46789df7c85513c23c1d554961cbbc Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 16:09:04 +0200 Subject: [PATCH 027/169] add periodId --- algorithm/bridge/fetch_applicants_and_committees.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 640c3bf8..63af4cc4 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -37,13 +37,13 @@ def main(): match_result = match_meetings(applicant_objects, committee_objects) - send_to_db(match_result, applicants) + send_to_db(match_result, applicants, period["_id"]) return match_result -def send_to_db(match_result: MeetingMatch, applicants: List[dict]): +def send_to_db(match_result: MeetingMatch, applicants: List[dict], periodId): load_dotenv() - formatted_results = format_match_results(match_result, applicants) + formatted_results = format_match_results(match_result, applicants, periodId) print("Sending to db") print(formatted_results) @@ -108,7 +108,7 @@ def fetch_committee_times(periodId): return committee_times -def format_match_results(match_results: MeetingMatch, applicants: List[dict]) -> List[Dict]: +def format_match_results(match_results: MeetingMatch, applicants: List[dict], periodId) -> List[Dict]: transformed_results = {} applicant_dict = {str(applicant['_id']): applicant['name'] for applicant in applicants} @@ -118,6 +118,7 @@ def format_match_results(match_results: MeetingMatch, applicants: List[dict]) -> if applicant_id not in transformed_results: transformed_results[applicant_id] = { + "periodId": periodId, "applicantId": applicant_id, "applicantName": applicant_name, "interviews": [] From edc4ffc0e935e5c163cfe95c8193dda4a3963817 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 16:15:16 +0200 Subject: [PATCH 028/169] correct api route --- lib/utils/sendInterviewTimes/fetchFunctions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/sendInterviewTimes/fetchFunctions.ts b/lib/utils/sendInterviewTimes/fetchFunctions.ts index 70a15396..b44704ce 100644 --- a/lib/utils/sendInterviewTimes/fetchFunctions.ts +++ b/lib/utils/sendInterviewTimes/fetchFunctions.ts @@ -52,7 +52,7 @@ export const fetchAlgorithmData = async ( periodId: string ): Promise => { try { - const reponse = await fetch(`/interviews/${periodId}`); + const reponse = await fetch(`/api/interviews/${periodId}`); const data = await reponse.json(); if (!Array.isArray(data)) { throw new Error("Expected an array from the interviews API response"); From 11dbaf3280be6b4b62240002517bd5cd126b045d Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 16:19:20 +0200 Subject: [PATCH 029/169] add periodId as string --- algorithm/bridge/fetch_applicants_and_committees.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 63af4cc4..c5256210 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -20,6 +20,7 @@ def main(): now = datetime.now(timezone.utc) + #or period["name"] == "Juli Opptak" if (application_end > now and period["hasSentInterviewTimes"] == False and interview_end < now): applicants = fetch_applicants(periodId) committee_times = fetch_committee_times(periodId) @@ -37,7 +38,7 @@ def main(): match_result = match_meetings(applicant_objects, committee_objects) - send_to_db(match_result, applicants, period["_id"]) + send_to_db(match_result, applicants, periodId) return match_result @@ -137,12 +138,10 @@ def format_match_results(match_results: MeetingMatch, applicants: List[dict], pe return list(transformed_results.values()) - def create_applicant_objects(applicants_data: List[dict], all_committees: dict[str, Committee]) -> set[Applicant]: applicants = set() for data in applicants_data: - applicant = Applicant(name=data['name'], email=data['email'], id=data['_id']) - + applicant = Applicant(name=data['name'], email=data['email'], id=str(data['_id'])) optional_committee_names = data.get('optionalCommittees', []) optional_committees = {all_committees[name] for name in optional_committee_names if name in all_committees} @@ -162,9 +161,6 @@ def create_applicant_objects(applicants_data: List[dict], all_committees: dict[s applicants.add(applicant) return applicants - - - def create_committee_objects(committee_data: List[dict]) -> set[Committee]: committees = set() for data in committee_data: @@ -181,4 +177,4 @@ def create_committee_objects(committee_data: List[dict]) -> set[Committee]: if __name__ == "__main__": - main() \ No newline at end of file + main() From 9ce2a2326d3b89ae160bc1c73d3b499ad204e6d3 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 16:28:39 +0200 Subject: [PATCH 030/169] fix add email to applicants --- algorithm/bridge/fetch_applicants_and_committees.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index c5256210..386513c4 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -111,17 +111,18 @@ def fetch_committee_times(periodId): def format_match_results(match_results: MeetingMatch, applicants: List[dict], periodId) -> List[Dict]: transformed_results = {} - applicant_dict = {str(applicant['_id']): applicant['name'] for applicant in applicants} + applicant_dict = {str(applicant['_id']): {'name': applicant['name'], 'email': applicant['email']} for applicant in applicants} for result in match_results['matchings']: applicant_id = str(result[1]) - applicant_name = applicant_dict.get(applicant_id, 'Unknown') + applicant_info = applicant_dict.get(applicant_id, {'name': 'Unknown', 'email': 'Unknown'}) if applicant_id not in transformed_results: transformed_results[applicant_id] = { "periodId": periodId, "applicantId": applicant_id, - "applicantName": applicant_name, + "applicantName": applicant_info['name'], + "applicantEmail": applicant_info['email'], "interviews": [] } @@ -138,6 +139,7 @@ def format_match_results(match_results: MeetingMatch, applicants: List[dict], pe return list(transformed_results.values()) + def create_applicant_objects(applicants_data: List[dict], all_committees: dict[str, Committee]) -> set[Applicant]: applicants = set() for data in applicants_data: From 78471d5961fdef7fb248480b49a0edc3daccd0b1 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 16:32:42 +0200 Subject: [PATCH 031/169] fix correct fetching --- lib/utils/sendInterviewTimes/fetchFunctions.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/utils/sendInterviewTimes/fetchFunctions.ts b/lib/utils/sendInterviewTimes/fetchFunctions.ts index b44704ce..19e88abc 100644 --- a/lib/utils/sendInterviewTimes/fetchFunctions.ts +++ b/lib/utils/sendInterviewTimes/fetchFunctions.ts @@ -54,11 +54,10 @@ export const fetchAlgorithmData = async ( try { const reponse = await fetch(`/api/interviews/${periodId}`); const data = await reponse.json(); - if (!Array.isArray(data)) { + if (!Array.isArray(data.interviews.interviews)) { throw new Error("Expected an array from the interviews API response"); } - console.log(data); - return data; + return data.interviews.interviews; } catch (error) { console.error(error); throw new Error("Failed to fetch algorithm data"); From fe0809b53c40cae1e7efcffd8e8a384c802d0a8b Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 16:34:17 +0200 Subject: [PATCH 032/169] change display name --- lib/utils/sendInterviewTimes/formatAndSend.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index db967e1e..281d07b3 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -25,7 +25,7 @@ export const formatAndSendEmails = async ({ let body = `\n\n`; typedApplicant.committees.forEach((committee) => { - body += `Komite: ${committee.committeeName}\n`; + body += `Komite: ${changeDisplayName(committee.committeeName)}\n`; body += `Start: ${formatDateHours( new Date(committee.interviewTime.start) )}\n`; From aece38a412449f4f170316511c07d9ac12454901 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 24 Jul 2024 17:32:46 +0200 Subject: [PATCH 033/169] interviews are now sent out serverside --- lib/utils/sendInterviewTimes/formatAndSend.ts | 112 +++++++++--------- .../sendInterviewTimes/sendInterviewTimes.ts | 26 +++- pages/api/interviews/send/[period-id].ts | 47 ++++++++ 3 files changed, 128 insertions(+), 57 deletions(-) create mode 100644 pages/api/interviews/send/[period-id].ts diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 281d07b3..17e896d2 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -15,69 +15,73 @@ export const formatAndSendEmails = async ({ committeesToEmail, applicantsToEmail, }: sendInterviewTimesProps) => { - const sesClient = new SESClient({ region: "eu-north-1" }); + try { + const sesClient = new SESClient({ region: "eu-north-1" }); - // Send email to each applicant - for (const applicant of applicantsToEmail) { - const typedApplicant: emailApplicantInterviewType = applicant; - const applicantEmail = [typedApplicant.applicantEmail]; - const subject = `Hei, ${typedApplicant.applicantName}. Her er dine intervjutider:`; - let body = `\n\n`; + // Send email to each applicant + for (const applicant of applicantsToEmail) { + const typedApplicant: emailApplicantInterviewType = applicant; + const applicantEmail = [typedApplicant.applicantEmail]; + const subject = `Hei, ${typedApplicant.applicantName}. Her er dine intervjutider:`; + let body = `\n\n`; - typedApplicant.committees.forEach((committee) => { - body += `Komite: ${changeDisplayName(committee.committeeName)}\n`; - body += `Start: ${formatDateHours( - new Date(committee.interviewTime.start) - )}\n`; - body += `Slutt: ${formatDateHours( - new Date(committee.interviewTime.end) - )}\n`; - body += `Rom: ${committee.interviewTime.room}\n\n`; - }); + typedApplicant.committees.forEach((committee) => { + body += `Komite: ${changeDisplayName(committee.committeeName)}\n`; + body += `Start: ${formatDateHours( + new Date(committee.interviewTime.start) + )}\n`; + body += `Slutt: ${formatDateHours( + new Date(committee.interviewTime.end) + )}\n`; + body += `Rom: ${committee.interviewTime.room}\n\n`; + }); - body += `Med vennlig hilsen,\nAppkom <3`; + body += `Med vennlig hilsen,\nAppkom <3`; - // // await sendEmail({ - // // sesClient: sesClient, - // // fromEmail: "opptak@online.ntnu.no", - // // toEmails: applicantEmail, - // // subject: subject, - // // htmlContent: body, - // // }); + // // await sendEmail({ + // // sesClient: sesClient, + // // fromEmail: "opptak@online.ntnu.no", + // // toEmails: applicantEmail, + // // subject: subject, + // // htmlContent: body, + // // }); - console.log(applicantEmail[0], "\n", subject, "\n", body); - } + console.log(applicantEmail[0], "\n", subject, "\n", body); + } - // Send email to each committee - for (const committee of committeesToEmail) { - const typedCommittee: emailCommitteeInterviewType = committee; - const committeeEmail = [typedCommittee.committeeEmail]; - const subject = `${changeDisplayName( - typedCommittee.committeeName - )}s sine intervjutider for ${typedCommittee.period_name}`; - let body = `Her er intervjutidene for søkerene deres:\n\n`; + // Send email to each committee + for (const committee of committeesToEmail) { + const typedCommittee: emailCommitteeInterviewType = committee; + const committeeEmail = [typedCommittee.committeeEmail]; + const subject = `${changeDisplayName( + typedCommittee.committeeName + )}s sine intervjutider for ${typedCommittee.period_name}`; + let body = `Her er intervjutidene for søkerene deres:\n\n`; - typedCommittee.applicants.forEach((applicant) => { - body += `Navn: ${applicant.applicantName}\n`; - body += `Start: ${formatDateHours( - new Date(applicant.interviewTime.start) - )}\n`; - body += `Slutt: ${formatDateHours( - new Date(applicant.interviewTime.end) - )}\n`; - body += `Rom: ${applicant.interviewTime.room}\n\n`; - }); + typedCommittee.applicants.forEach((applicant) => { + body += `Navn: ${applicant.applicantName}\n`; + body += `Start: ${formatDateHours( + new Date(applicant.interviewTime.start) + )}\n`; + body += `Slutt: ${formatDateHours( + new Date(applicant.interviewTime.end) + )}\n`; + body += `Rom: ${applicant.interviewTime.room}\n\n`; + }); - body += `Med vennlig hilsen, Appkom <3`; + body += `Med vennlig hilsen, Appkom <3`; - // await sendEmail({ - // sesClient: sesClient, - // fromEmail: "opptak@online.ntnu.no", - // toEmails: committeeEmail, - // subject: subject, - // htmlContent: body, - // }); + // await sendEmail({ + // sesClient: sesClient, + // fromEmail: "opptak@online.ntnu.no", + // toEmails: committeeEmail, + // subject: subject, + // htmlContent: body, + // }); - console.log(committeeEmail[0], "\n", subject, "\n", body); + console.log(committeeEmail[0], "\n", subject, "\n", body); + } + } catch (error) { + return { error: "Failed to send out interview times" }; } }; diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index 104afcd4..8ea665bb 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -17,6 +17,7 @@ import { fetchPeriod, } from "./fetchFunctions"; import { formatAndSendEmails } from "./formatAndSend"; +import toast from "react-hot-toast"; interface Props { periodId: string; @@ -41,10 +42,29 @@ export const sendOutInterviewTimes = async ({ periodId }: Props) => { const committeesToEmail: emailCommitteeInterviewType[] = formatCommittees(applicantsToEmail); - // console.log(applicantsToEmailMap); - // console.log(committeesToEmail); + const dataToSend = { + committeesToEmail, + applicantsToEmail, + }; + + try { + const response = await fetch(`/api/interviews/send/${period?._id}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(dataToSend), + }); + + if (!response.ok) { + throw new Error("Failed to submit data"); + } - await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); + const result = await response.json(); + toast.success("Intervjutider er sendt inn!"); + } catch (error) { + toast.error("Kunne ikke sende inn!"); + } }; const formatApplicants = ( diff --git a/pages/api/interviews/send/[period-id].ts b/pages/api/interviews/send/[period-id].ts new file mode 100644 index 00000000..a18ebabd --- /dev/null +++ b/pages/api/interviews/send/[period-id].ts @@ -0,0 +1,47 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { getServerSession } from "next-auth"; +import { authOptions } from "../../auth/[...nextauth]"; +import { + hasSession, + isAdmin, + isInCommitee, +} from "../../../../lib/utils/apiChecks"; +import { getInterviewsByPeriod } from "../../../../lib/mongo/interviews"; +import { formatAndSendEmails } from "../../../../lib/utils/sendInterviewTimes/formatAndSend"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getServerSession(req, res, authOptions); + const periodId = req.query["period-id"]; + + if (!periodId || typeof periodId !== "string") { + return res + .status(400) + .json({ error: "Invalid or missing periodId parameter" }); + } + + if (!hasSession(res, session)) return; + if (!isInCommitee(res, session)) return; + + if (req.method === "POST") { + if (!isAdmin(res, session)) return; + const interviewData = req.body; + + try { + const result = await formatAndSendEmails(interviewData); + if (result && result.error) { + throw new Error(result.error); + } + + if (result?.error) throw new Error(result.error); + return res.status(201).json({ result }); + } catch (error: any) { + return res.status(500).json({ error: error.message }); + } + } + + res.setHeader("Allow", ["POST"]); + res.status(405).end(`Method ${req.method} is not allowed.`); +}; + +export default handler; From b1613240f9d58e962d4a4fe5c44b11979de25d65 Mon Sep 17 00:00:00 2001 From: fredrir Date: Thu, 25 Jul 2024 19:18:36 +0200 Subject: [PATCH 034/169] refactored the entire function to be server side --- lib/mongo/committees.ts | 5 +- .../sendInterviewTimes/fetchFunctions.ts | 108 +++++++++--------- .../sendInterviewTimes/sendInterviewTimes.ts | 77 +++++++------ pages/admin/[period-id]/index.tsx | 21 +++- pages/api/interviews/[period-id].ts | 15 ++- pages/api/interviews/send/[period-id].ts | 47 -------- 6 files changed, 125 insertions(+), 148 deletions(-) delete mode 100644 pages/api/interviews/send/[period-id].ts diff --git a/lib/mongo/committees.ts b/lib/mongo/committees.ts index 7018cee6..85d7181c 100644 --- a/lib/mongo/committees.ts +++ b/lib/mongo/committees.ts @@ -1,11 +1,10 @@ import { Collection, Db, MongoClient, ObjectId, UpdateResult } from "mongodb"; import clientPromise from "./mongodb"; import { commiteeType } from "../types/types"; -import { co } from "@fullcalendar/core/internal-common"; let client: MongoClient; let db: Db; -let committees: Collection; +let committees: Collection; async function init() { if (db) return; @@ -73,7 +72,7 @@ export const getCommitteesByPeriod = async (periodId: string) => { try { if (!committees) await init(); const result = await committees.find({ periodId: periodId }).toArray(); - return { committees: result }; + return { result }; } catch (error) { return { error: "Failed to fetch committees" }; } diff --git a/lib/utils/sendInterviewTimes/fetchFunctions.ts b/lib/utils/sendInterviewTimes/fetchFunctions.ts index 19e88abc..c21e0771 100644 --- a/lib/utils/sendInterviewTimes/fetchFunctions.ts +++ b/lib/utils/sendInterviewTimes/fetchFunctions.ts @@ -1,65 +1,65 @@ -import { - algorithmType, - committeeEmails, - committeeInterviewType, - periodType, -} from "../../types/types"; +import { committeeEmails, owCommitteeType } from "../../types/types"; -export const fetchPeriod = async (periodId: string): Promise => { +export const fetchCommitteeEmails = async (): Promise => { try { - const response = await fetch(`/api/periods/${periodId}`); - const data = await response.json(); - if (!data || !data.period) { - throw new Error("Invalid response from fetchPeriod"); + const baseUrl = + "https://old.online.ntnu.no/api/v1/group/online-groups/?page_size=999"; + + const excludedCommitteeNames = [ + "HS", + "Komiteledere", + "Pangkom", + "Fond", + "Æresmedlemmer", + "Bed&Fagkom", + "Bekk_påmeldte", + "booking", + "Buddy", + "CAG", + "Eksgruppa", + "Eldsterådet", + "Ex-Komiteer", + "interessegrupper", + "ITEX", + "ITEX-påmeldte", + "kobKom", + "Komiteer", + "Redaksjonen", + "Riddere", + "techtalks", + "Ex-Hovedstyret", + "Tillitsvalgte", + "Wiki - Komiteer access permissions", + "Wiki - Online edit permissions", + "X-Sport", + ]; + + const response = await fetch(baseUrl); + + if (!response.ok) { + throw new Error("Failed to fetch data"); } - return data.period; - } catch (error) { - console.error(error); - throw new Error("Failed to fetch period"); - } -}; -export const fetchCommitteeInterviewTimes = async ( - periodId: string -): Promise => { - try { - const response = await fetch(`/api/committees/times/${periodId}`); const data = await response.json(); - return data.committees.committees; - } catch (error) { - console.error(error); - throw new Error("Failed to fetch committee interview times"); - } -}; -export const fetchCommitteeEmails = async (): Promise => { - try { - const response = await fetch(`/api/periods/ow-committees`); - const data = await response.json(); - if (!Array.isArray(data)) { - throw new Error( - "Expected an array from the fetchCommitteeEmails API response" - ); - } - return data; - } catch (error) { - console.error(error); - throw new Error("Failed to fetch committee emails"); - } -}; + const groups = data.results + .filter( + (group: { name_short: string }) => + !excludedCommitteeNames.includes(group.name_short) + ) + .map((group: owCommitteeType) => ({ + name_short: group.name_short, + name_long: group.name_long, + email: group.email, + description_short: group.description_short, + description_long: group.description_long, + image: group?.image, + application_description: group.application_description, + })); -export const fetchAlgorithmData = async ( - periodId: string -): Promise => { - try { - const reponse = await fetch(`/api/interviews/${periodId}`); - const data = await reponse.json(); - if (!Array.isArray(data.interviews.interviews)) { - throw new Error("Expected an array from the interviews API response"); - } - return data.interviews.interviews; + return groups; } catch (error) { console.error(error); - throw new Error("Failed to fetch algorithm data"); + return []; } }; diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index 8ea665bb..ddd19156 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -10,60 +10,61 @@ import { } from "../../types/types"; import { changeDisplayName } from "../toString"; -import { - fetchAlgorithmData, - fetchCommitteeEmails, - fetchCommitteeInterviewTimes, - fetchPeriod, -} from "./fetchFunctions"; +import { fetchCommitteeEmails } from "./fetchFunctions"; import { formatAndSendEmails } from "./formatAndSend"; import toast from "react-hot-toast"; +import { getPeriodById } from "../../mongo/periods"; +import { getCommitteesByPeriod } from "../../mongo/committees"; +import { getInterviewsByPeriod } from "../../mongo/interviews"; interface Props { periodId: string; } export const sendOutInterviewTimes = async ({ periodId }: Props) => { - const period: periodType = await fetchPeriod(periodId); - const committeeInterviewTimes: committeeInterviewType[] = - await fetchCommitteeInterviewTimes(periodId); - const committeeEmails: committeeEmails[] = await fetchCommitteeEmails(); + try { + const periodData = await getPeriodById(periodId); - const algorithmData: algorithmType[] = await fetchAlgorithmData(periodId); + if (!periodData.exists || !periodData.period) { + return { error: "Failed to find period" }; + } + const period: periodType = periodData.period; - const applicantsToEmail: emailApplicantInterviewType[] = formatApplicants( - algorithmData, - periodId, - period, - committeeEmails, - committeeInterviewTimes - ); + const commmitteeInterviewTimesData = await getCommitteesByPeriod(periodId); - const committeesToEmail: emailCommitteeInterviewType[] = - formatCommittees(applicantsToEmail); + if (!commmitteeInterviewTimesData || commmitteeInterviewTimesData.error) { + return { error: "Failed to find committee interview times" }; + } - const dataToSend = { - committeesToEmail, - applicantsToEmail, - }; + const committeeInterviewTimes: committeeInterviewType[] = + commmitteeInterviewTimesData.result || []; - try { - const response = await fetch(`/api/interviews/send/${period?._id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(dataToSend), - }); + const committeeEmails: committeeEmails[] = await fetchCommitteeEmails(); - if (!response.ok) { - throw new Error("Failed to submit data"); - } + const fetchedAlgorithmData = await getInterviewsByPeriod(periodId); + + const algorithmData: algorithmType[] = + fetchedAlgorithmData.interviews || []; + + const applicantsToEmail: emailApplicantInterviewType[] = formatApplicants( + algorithmData, + periodId, + period, + committeeEmails, + committeeInterviewTimes + ); + + const committeesToEmail: emailCommitteeInterviewType[] = + formatCommittees(applicantsToEmail); + + const dataToSend = { + committeesToEmail, + applicantsToEmail, + }; - const result = await response.json(); - toast.success("Intervjutider er sendt inn!"); + formatAndSendEmails(dataToSend); } catch (error) { - toast.error("Kunne ikke sende inn!"); + return { error: "Failed to send out interview times" }; } }; diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index e62d58f9..ec844322 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -7,11 +7,11 @@ import ApplicantsOverview from "../../../components/applicantoverview/Applicants import { Tabs } from "../../../components/Tabs"; import { CalendarIcon, InboxIcon } from "@heroicons/react/24/solid"; import Button from "../../../components/Button"; -import { sendOutInterviewTimes } from "../../../lib/utils/sendInterviewTimes/sendInterviewTimes"; import { useQuery } from "@tanstack/react-query"; import { fetchPeriodById } from "../../../lib/api/periodApi"; import LoadingPage from "../../../components/LoadingPage"; import ErrorPage from "../../../components/ErrorPage"; +import toast from "react-hot-toast"; const Admin = () => { const { data: session } = useSession(); @@ -31,6 +31,25 @@ const Admin = () => { setCommittees(data?.period.committees); }, [data, session?.user?.owId]); + const sendOutInterviewTimes = async ({ periodId }: { periodId: string }) => { + try { + const response = await fetch(`/api/interviews/${periodId}`, { + method: "POST", + }); + if (!response.ok) { + throw new Error("Failed to send out interview times"); + } + const data = await response.json(); + if (data.error) { + throw new Error(data.error); + } + toast.success("Intervjutider er sendt ut! (Sjekk konsoll loggen)"); + return data; + } catch (error) { + toast.error("Failed to send out interview times"); + } + }; + if (session?.user?.role !== "admin") return ; if (isLoading) return ; if (isError) return ; diff --git a/pages/api/interviews/[period-id].ts b/pages/api/interviews/[period-id].ts index 07bf5c40..77a5fe1a 100644 --- a/pages/api/interviews/[period-id].ts +++ b/pages/api/interviews/[period-id].ts @@ -7,7 +7,7 @@ import { isAdmin, isInCommitee, } from "../../../lib/utils/apiChecks"; -import { getInterviewsByPeriod } from "../../../lib/mongo/interviews"; +import { sendOutInterviewTimes } from "../../../lib/utils/sendInterviewTimes/sendInterviewTimes"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -22,18 +22,23 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (!hasSession(res, session)) return; if (!isInCommitee(res, session)) return; - if (req.method === "GET") { + if (req.method === "POST") { if (!isAdmin(res, session)) return; try { - const interviews = await getInterviewsByPeriod(periodId); - return res.status(200).json({ interviews }); + const result = await sendOutInterviewTimes({ periodId }); + if (result && result.error) { + throw new Error(result.error); + } + + if (result?.error) throw new Error(result.error); + return res.status(201).json({ result }); } catch (error: any) { return res.status(500).json({ error: error.message }); } } - res.setHeader("Allow", ["GET"]); + res.setHeader("Allow", ["POST"]); res.status(405).end(`Method ${req.method} is not allowed.`); }; diff --git a/pages/api/interviews/send/[period-id].ts b/pages/api/interviews/send/[period-id].ts deleted file mode 100644 index a18ebabd..00000000 --- a/pages/api/interviews/send/[period-id].ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; - -import { getServerSession } from "next-auth"; -import { authOptions } from "../../auth/[...nextauth]"; -import { - hasSession, - isAdmin, - isInCommitee, -} from "../../../../lib/utils/apiChecks"; -import { getInterviewsByPeriod } from "../../../../lib/mongo/interviews"; -import { formatAndSendEmails } from "../../../../lib/utils/sendInterviewTimes/formatAndSend"; - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions); - const periodId = req.query["period-id"]; - - if (!periodId || typeof periodId !== "string") { - return res - .status(400) - .json({ error: "Invalid or missing periodId parameter" }); - } - - if (!hasSession(res, session)) return; - if (!isInCommitee(res, session)) return; - - if (req.method === "POST") { - if (!isAdmin(res, session)) return; - const interviewData = req.body; - - try { - const result = await formatAndSendEmails(interviewData); - if (result && result.error) { - throw new Error(result.error); - } - - if (result?.error) throw new Error(result.error); - return res.status(201).json({ result }); - } catch (error: any) { - return res.status(500).json({ error: error.message }); - } - } - - res.setHeader("Allow", ["POST"]); - res.status(405).end(`Method ${req.method} is not allowed.`); -}; - -export default handler; From ef5fc6cff87908c3b4bb6117a0674b29b37d93f3 Mon Sep 17 00:00:00 2001 From: fredrir Date: Fri, 26 Jul 2024 14:13:10 +0200 Subject: [PATCH 035/169] better formatting --- lib/utils/sendInterviewTimes/formatAndSend.ts | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 17e896d2..08130407 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -5,6 +5,7 @@ import { } from "../../types/types"; import { changeDisplayName } from "../toString"; import { formatDateHours } from "../dateUtils"; +import sendEmail from "../sendEmail"; interface sendInterviewTimesProps { committeesToEmail: emailCommitteeInterviewType[]; @@ -20,23 +21,33 @@ export const formatAndSendEmails = async ({ // Send email to each applicant for (const applicant of applicantsToEmail) { - const typedApplicant: emailApplicantInterviewType = applicant; + const typedApplicant: emailApplicantInterviewType = applicantsToEmail[0]; const applicantEmail = [typedApplicant.applicantEmail]; - const subject = `Hei, ${typedApplicant.applicantName}. Her er dine intervjutider:`; - let body = `\n\n`; + const subject = `Hei, ${typedApplicant.applicantName}, her er dine intervjutider:`; + + let body = `

Hei ${typedApplicant.applicantName},

Her er dine intervjutider for ${typedApplicant.period_name}:


    `; + + typedApplicant.committees.sort((a, b) => { + return ( + new Date(a.interviewTime.start).getTime() - + new Date(b.interviewTime.start).getTime() + ); + }); typedApplicant.committees.forEach((committee) => { - body += `Komite: ${changeDisplayName(committee.committeeName)}\n`; - body += `Start: ${formatDateHours( + body += `
  • Komite: ${changeDisplayName( + committee.committeeName + )}
    `; + body += `Start: ${formatDateHours( new Date(committee.interviewTime.start) - )}\n`; - body += `Slutt: ${formatDateHours( + )}
    `; + body += `Slutt: ${formatDateHours( new Date(committee.interviewTime.end) - )}\n`; - body += `Rom: ${committee.interviewTime.room}\n\n`; + )}
    `; + body += `Rom: ${committee.interviewTime.room}

  • `; }); - body += `Med vennlig hilsen,\nAppkom <3`; + body += `


Skjedd en feil? Ta kontakt med Appkom❤️

`; // // await sendEmail({ // // sesClient: sesClient, @@ -51,25 +62,35 @@ export const formatAndSendEmails = async ({ // Send email to each committee for (const committee of committeesToEmail) { - const typedCommittee: emailCommitteeInterviewType = committee; + const typedCommittee: emailCommitteeInterviewType = committeesToEmail[0]; const committeeEmail = [typedCommittee.committeeEmail]; const subject = `${changeDisplayName( typedCommittee.committeeName - )}s sine intervjutider for ${typedCommittee.period_name}`; - let body = `Her er intervjutidene for søkerene deres:\n\n`; + )} sine intervjutider for ${typedCommittee.period_name}`; + + let body = `

Hei ${changeDisplayName( + typedCommittee.committeeName + )},

Her er deres intervjutider:

    `; + + typedCommittee.applicants.sort((a, b) => { + return ( + new Date(a.interviewTime.start).getTime() - + new Date(b.interviewTime.start).getTime() + ); + }); typedCommittee.applicants.forEach((applicant) => { - body += `Navn: ${applicant.applicantName}\n`; - body += `Start: ${formatDateHours( + body += `
  • Navn: ${applicant.applicantName}
    `; + body += `Start: ${formatDateHours( new Date(applicant.interviewTime.start) - )}\n`; - body += `Slutt: ${formatDateHours( + )}
    `; + body += `Slutt: ${formatDateHours( new Date(applicant.interviewTime.end) - )}\n`; - body += `Rom: ${applicant.interviewTime.room}\n\n`; + )}
    `; + body += `Rom: ${applicant.interviewTime.room}

  • `; }); - body += `Med vennlig hilsen, Appkom <3`; + body += `


Skjedd en feil? Ta kontakt med Appkom❤️

`; // await sendEmail({ // sesClient: sesClient, From e310c71fa48c6e45d6995e264721fbc31aaf1b68 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 28 Jul 2024 20:54:16 +0200 Subject: [PATCH 036/169] feat: will send SMS to applicants about interviews --- .env.local.template | 4 + lib/types/types.ts | 1 + lib/utils/sendInterviewTimes/formatAndSend.ts | 46 ++- lib/utils/sendInterviewTimes/sendSMS.ts | 19 ++ package-lock.json | 282 +++++++++++++++++- package.json | 1 + 6 files changed, 322 insertions(+), 31 deletions(-) create mode 100644 lib/utils/sendInterviewTimes/sendSMS.ts diff --git a/.env.local.template b/.env.local.template index 3e58d9e5..418cb09e 100644 --- a/.env.local.template +++ b/.env.local.template @@ -12,3 +12,7 @@ AUTH0_ISSUER=#issuer base url, example: example.us.auth0.com AWS_SECRET_ACCESS_KEY=#aws secret access key AWS_ACCESS_KEY_ID=#aws access key id + +TWILIO_ACCOUNT_SID=#twilio account sid +TWILIO_AUTH_TOKEN=#twilio auth token +TWILIO_PHONE_NUMBER=#twilio phone number diff --git a/lib/types/types.ts b/lib/types/types.ts index 4318cce0..db719e15 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -141,6 +141,7 @@ export type emailApplicantInterviewType = { period_name: string; applicantName: string; applicantEmail: string; + applicantPhone: string; committees: { committeeName: string; committeeEmail: string; diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 08130407..44422a21 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -6,6 +6,7 @@ import { import { changeDisplayName } from "../toString"; import { formatDateHours } from "../dateUtils"; import sendEmail from "../sendEmail"; +import sendSMS from "./sendSMS"; interface sendInterviewTimesProps { committeesToEmail: emailCommitteeInterviewType[]; @@ -20,12 +21,13 @@ export const formatAndSendEmails = async ({ const sesClient = new SESClient({ region: "eu-north-1" }); // Send email to each applicant - for (const applicant of applicantsToEmail) { + if (applicantsToEmail.length > 0) { const typedApplicant: emailApplicantInterviewType = applicantsToEmail[0]; const applicantEmail = [typedApplicant.applicantEmail]; const subject = `Hei, ${typedApplicant.applicantName}, her er dine intervjutider:`; - let body = `

Hei ${typedApplicant.applicantName},

Her er dine intervjutider for ${typedApplicant.period_name}:


    `; + let emailBody = `

    Hei ${typedApplicant.applicantName},

    Her er dine intervjutider for ${typedApplicant.period_name}:


      `; + let phoneBody = `Hei ${typedApplicant.applicantName}, her er dine intervjutider for ${typedApplicant.period_name}: \n \n`; typedApplicant.committees.sort((a, b) => { return ( @@ -35,29 +37,43 @@ export const formatAndSendEmails = async ({ }); typedApplicant.committees.forEach((committee) => { - body += `
    • Komite: ${changeDisplayName( + emailBody += `
    • Komite: ${changeDisplayName( committee.committeeName )}
      `; - body += `Start: ${formatDateHours( + emailBody += `Start: ${formatDateHours( new Date(committee.interviewTime.start) )}
      `; - body += `Slutt: ${formatDateHours( + emailBody += `Slutt: ${formatDateHours( new Date(committee.interviewTime.end) )}
      `; - body += `Rom: ${committee.interviewTime.room}

    • `; + emailBody += `Rom: ${committee.interviewTime.room}
      `; + + phoneBody += `Komite: ${changeDisplayName(committee.committeeName)} \n`; + phoneBody += `Start: ${formatDateHours( + new Date(committee.interviewTime.start) + )}\n`; + phoneBody += `Slutt: ${formatDateHours( + new Date(committee.interviewTime.end) + )}\n`; + phoneBody += `Rom: ${committee.interviewTime.room} \n \n`; }); - body += `


    Skjedd en feil? Ta kontakt med Appkom❤️

    `; + emailBody += `


Skjedd en feil? Ta kontakt med Appkom❤️

`; + phoneBody += `Skjedd en feil? Ta kontakt med Appkom❤️`; + + // await sendEmail({ + // sesClient: sesClient, + // fromEmail: "opptak@online.ntnu.no", + // toEmails: applicantEmail, + // subject: subject, + // htmlContent: emailBody, + // }); - // // await sendEmail({ - // // sesClient: sesClient, - // // fromEmail: "opptak@online.ntnu.no", - // // toEmails: applicantEmail, - // // subject: subject, - // // htmlContent: body, - // // }); + let toPhoneNumber = "+47"; + toPhoneNumber += typedApplicant.applicantPhone; + sendSMS(toPhoneNumber, phoneBody); - console.log(applicantEmail[0], "\n", subject, "\n", body); + console.log(applicantEmail[0], "\n", subject, "\n", emailBody); } // Send email to each committee diff --git a/lib/utils/sendInterviewTimes/sendSMS.ts b/lib/utils/sendInterviewTimes/sendSMS.ts new file mode 100644 index 00000000..5d631655 --- /dev/null +++ b/lib/utils/sendInterviewTimes/sendSMS.ts @@ -0,0 +1,19 @@ +import twilio from "twilio"; + +export default function sendSMS(toPhoneNumber: string, message: string) { + const accountSid = process.env.TWILIO_ACCOUNT_SID; + const token = process.env.TWILIO_AUTH_TOKEN; + const client = twilio(accountSid, token); + const fromPhoneNumber = process.env.TWILIO_PHONE_NUMBER; + + client.messages + .create({ + body: message, + from: fromPhoneNumber, + to: toPhoneNumber, + }) + + .catch((error) => { + console.log(error); + }); +} diff --git a/package-lock.json b/package-lock.json index 899cebb9..423703e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "react-hot-toast": "^2.4.1", "react-icons": "^4.12.0", "react-loading-skeleton": "^3.4.0", + "twilio": "^5.2.2", "validator": "^13.11.0" }, "devDependencies": { @@ -1953,6 +1954,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2205,6 +2217,11 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/autoprefixer": { "version": "10.4.19", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", @@ -2266,6 +2283,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -2357,11 +2384,15 @@ "node": ">=16.20.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2478,6 +2509,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2578,11 +2620,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", + "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==" + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2637,7 +2683,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2667,6 +2712,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2706,6 +2759,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.828", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.828.tgz", @@ -2781,7 +2842,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -2793,7 +2853,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -3505,6 +3564,25 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -3540,6 +3618,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3611,7 +3702,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -3738,7 +3828,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -3774,7 +3863,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -3786,7 +3874,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3798,7 +3885,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -3832,6 +3918,18 @@ "node": ">= 0.4" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -4400,6 +4498,27 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4415,6 +4534,25 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4488,16 +4626,46 @@ "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4545,6 +4713,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -4639,8 +4826,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -4849,7 +5035,6 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -5372,6 +5557,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5380,6 +5570,20 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.3.tgz", + "integrity": "sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5624,6 +5828,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -5649,11 +5872,15 @@ "loose-envify": "^1.1.0" } }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" + }, "node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -5665,7 +5892,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5716,7 +5942,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -6239,6 +6464,23 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/twilio": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.2.2.tgz", + "integrity": "sha512-t2Nd8CvqAc0YxbJghKYQl1Vxc7e6SrWk4U28wwkarUohGcsUMLsGpYeGXKw1Va0KB9TGVZYCs8dcP4TdLJUN9Q==", + "dependencies": { + "axios": "^1.6.8", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.2", + "qs": "^6.9.4", + "scmp": "^2.1.0", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6641,6 +6883,14 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "engines": { + "node": ">=6.0" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index e69364f2..7a94dc90 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "react-hot-toast": "^2.4.1", "react-icons": "^4.12.0", "react-loading-skeleton": "^3.4.0", + "twilio": "^5.2.2", "validator": "^13.11.0" }, "devDependencies": { From ec682a2a7fa26c3224d6a6556870ec401478d825 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 28 Jul 2024 20:59:33 +0200 Subject: [PATCH 037/169] add phone number to applicant --- algorithm/bridge/fetch_applicants_and_committees.py | 5 +++-- algorithm/src/mip_matching/Applicant.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 386513c4..6117a422 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -111,7 +111,7 @@ def fetch_committee_times(periodId): def format_match_results(match_results: MeetingMatch, applicants: List[dict], periodId) -> List[Dict]: transformed_results = {} - applicant_dict = {str(applicant['_id']): {'name': applicant['name'], 'email': applicant['email']} for applicant in applicants} + applicant_dict = {str(applicant['_id']): {'name': applicant['name'], 'email': applicant['email'], 'phone': applicant['phone']} for applicant in applicants} for result in match_results['matchings']: applicant_id = str(result[1]) @@ -123,6 +123,7 @@ def format_match_results(match_results: MeetingMatch, applicants: List[dict], pe "applicantId": applicant_id, "applicantName": applicant_info['name'], "applicantEmail": applicant_info['email'], + "applicantPhone": applicant_info['phone'], "interviews": [] } @@ -143,7 +144,7 @@ def format_match_results(match_results: MeetingMatch, applicants: List[dict], pe def create_applicant_objects(applicants_data: List[dict], all_committees: dict[str, Committee]) -> set[Applicant]: applicants = set() for data in applicants_data: - applicant = Applicant(name=data['name'], email=data['email'], id=str(data['_id'])) + applicant = Applicant(name=data['name'], email=data['email'], phone=data['phone'], id=str(data['_id'])) optional_committee_names = data.get('optionalCommittees', []) optional_committees = {all_committees[name] for name in optional_committee_names if name in all_committees} diff --git a/algorithm/src/mip_matching/Applicant.py b/algorithm/src/mip_matching/Applicant.py index 5a08061c..8e2873e4 100644 --- a/algorithm/src/mip_matching/Applicant.py +++ b/algorithm/src/mip_matching/Applicant.py @@ -15,11 +15,12 @@ class Applicant: komitéer hen har søkt på, og når søkeren kan ha intervjuer. """ - def __init__(self, name: str, email: str, id: str): + def __init__(self, name: str, email: str, phone: str, id: str): self.committees: list[Committee] = [] self.slots: set[TimeInterval] = set() self.name = name self.email = email + self.phone = phone self.id = id def add_committee(self, committee: Committee) -> None: From be65fb4bcf0059470b4bd3eb3cf47d4f50c9ec4b Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 28 Jul 2024 21:21:38 +0200 Subject: [PATCH 038/169] remove heart from sms body --- lib/utils/sendInterviewTimes/formatAndSend.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 44422a21..b3398c8f 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -59,7 +59,7 @@ export const formatAndSendEmails = async ({ }); emailBody += `

Skjedd en feil? Ta kontakt med Appkom❤️

`; - phoneBody += `Skjedd en feil? Ta kontakt med Appkom❤️`; + phoneBody += `Skjedd en feil? Ta kontakt med Appkom`; // await sendEmail({ // sesClient: sesClient, From 07a4a12bef818a66244bdb9cfab7a51cdcc032e1 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 28 Jul 2024 22:10:38 +0200 Subject: [PATCH 039/169] send SMS from "Online" --- .env.local.template | 1 - lib/types/types.ts | 1 + lib/utils/sendInterviewTimes/formatAndSend.ts | 2 +- lib/utils/sendInterviewTimes/sendInterviewTimes.ts | 3 +-- lib/utils/sendInterviewTimes/sendSMS.ts | 3 +-- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.env.local.template b/.env.local.template index 418cb09e..a68bc96b 100644 --- a/.env.local.template +++ b/.env.local.template @@ -15,4 +15,3 @@ AWS_ACCESS_KEY_ID=#aws access key id TWILIO_ACCOUNT_SID=#twilio account sid TWILIO_AUTH_TOKEN=#twilio auth token -TWILIO_PHONE_NUMBER=#twilio phone number diff --git a/lib/types/types.ts b/lib/types/types.ts index db719e15..bb283b55 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -108,6 +108,7 @@ export type owCommitteeType = { export type algorithmType = { applicantName: string; applicantEmail: string; + applicantPhone: string; interviews: { start: string; end: string; diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index b3398c8f..5fc59190 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -21,7 +21,7 @@ export const formatAndSendEmails = async ({ const sesClient = new SESClient({ region: "eu-north-1" }); // Send email to each applicant - if (applicantsToEmail.length > 0) { + for (const applicant of applicantsToEmail) { const typedApplicant: emailApplicantInterviewType = applicantsToEmail[0]; const applicantEmail = [typedApplicant.applicantEmail]; const subject = `Hei, ${typedApplicant.applicantName}, her er dine intervjutider:`; diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index ddd19156..f3051eb4 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -9,10 +9,8 @@ import { algorithmType, } from "../../types/types"; -import { changeDisplayName } from "../toString"; import { fetchCommitteeEmails } from "./fetchFunctions"; import { formatAndSendEmails } from "./formatAndSend"; -import toast from "react-hot-toast"; import { getPeriodById } from "../../mongo/periods"; import { getCommitteesByPeriod } from "../../mongo/committees"; import { getInterviewsByPeriod } from "../../mongo/interviews"; @@ -118,6 +116,7 @@ const formatApplicants = ( period_name: period.name, applicantName: app.applicantName, applicantEmail: app.applicantEmail, + applicantPhone: app.applicantPhone, committees: committees, }; diff --git a/lib/utils/sendInterviewTimes/sendSMS.ts b/lib/utils/sendInterviewTimes/sendSMS.ts index 5d631655..4eac5c77 100644 --- a/lib/utils/sendInterviewTimes/sendSMS.ts +++ b/lib/utils/sendInterviewTimes/sendSMS.ts @@ -4,12 +4,11 @@ export default function sendSMS(toPhoneNumber: string, message: string) { const accountSid = process.env.TWILIO_ACCOUNT_SID; const token = process.env.TWILIO_AUTH_TOKEN; const client = twilio(accountSid, token); - const fromPhoneNumber = process.env.TWILIO_PHONE_NUMBER; client.messages .create({ body: message, - from: fromPhoneNumber, + from: "Online", to: toPhoneNumber, }) From a5a05258350af9977f77aad6742deb59916921cd Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 28 Jul 2024 23:56:48 +0200 Subject: [PATCH 040/169] correct committee and aplicant --- lib/utils/sendInterviewTimes/formatAndSend.ts | 6 +++--- lib/utils/sendInterviewTimes/sendInterviewTimes.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 5fc59190..8502767c 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -22,7 +22,7 @@ export const formatAndSendEmails = async ({ // Send email to each applicant for (const applicant of applicantsToEmail) { - const typedApplicant: emailApplicantInterviewType = applicantsToEmail[0]; + const typedApplicant: emailApplicantInterviewType = applicant; const applicantEmail = [typedApplicant.applicantEmail]; const subject = `Hei, ${typedApplicant.applicantName}, her er dine intervjutider:`; @@ -71,14 +71,14 @@ export const formatAndSendEmails = async ({ let toPhoneNumber = "+47"; toPhoneNumber += typedApplicant.applicantPhone; - sendSMS(toPhoneNumber, phoneBody); + // sendSMS(toPhoneNumber, phoneBody); console.log(applicantEmail[0], "\n", subject, "\n", emailBody); } // Send email to each committee for (const committee of committeesToEmail) { - const typedCommittee: emailCommitteeInterviewType = committeesToEmail[0]; + const typedCommittee: emailCommitteeInterviewType = committee; const committeeEmail = [typedCommittee.committeeEmail]; const subject = `${changeDisplayName( typedCommittee.committeeName diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index f3051eb4..2c7e53f3 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -88,7 +88,7 @@ const formatApplicants = ( time.committee.toLowerCase() === interview.committeeName.toLowerCase() ); - let room = ""; + let room = "Info kommer"; if (committeeTime) { const availableTime = committeeTime.availabletimes.find( From 11f8d284db48cb643e9b33b3970c823807bf8701 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 11:39:36 +0200 Subject: [PATCH 041/169] shorten logic --- .../sendInterviewTimes/fetchFunctions.ts | 47 ++----------------- 1 file changed, 4 insertions(+), 43 deletions(-) diff --git a/lib/utils/sendInterviewTimes/fetchFunctions.ts b/lib/utils/sendInterviewTimes/fetchFunctions.ts index c21e0771..069e1cb0 100644 --- a/lib/utils/sendInterviewTimes/fetchFunctions.ts +++ b/lib/utils/sendInterviewTimes/fetchFunctions.ts @@ -5,35 +5,6 @@ export const fetchCommitteeEmails = async (): Promise => { const baseUrl = "https://old.online.ntnu.no/api/v1/group/online-groups/?page_size=999"; - const excludedCommitteeNames = [ - "HS", - "Komiteledere", - "Pangkom", - "Fond", - "Æresmedlemmer", - "Bed&Fagkom", - "Bekk_påmeldte", - "booking", - "Buddy", - "CAG", - "Eksgruppa", - "Eldsterådet", - "Ex-Komiteer", - "interessegrupper", - "ITEX", - "ITEX-påmeldte", - "kobKom", - "Komiteer", - "Redaksjonen", - "Riddere", - "techtalks", - "Ex-Hovedstyret", - "Tillitsvalgte", - "Wiki - Komiteer access permissions", - "Wiki - Online edit permissions", - "X-Sport", - ]; - const response = await fetch(baseUrl); if (!response.ok) { @@ -42,20 +13,10 @@ export const fetchCommitteeEmails = async (): Promise => { const data = await response.json(); - const groups = data.results - .filter( - (group: { name_short: string }) => - !excludedCommitteeNames.includes(group.name_short) - ) - .map((group: owCommitteeType) => ({ - name_short: group.name_short, - name_long: group.name_long, - email: group.email, - description_short: group.description_short, - description_long: group.description_long, - image: group?.image, - application_description: group.application_description, - })); + const groups = data.results.map((group: owCommitteeType) => ({ + name_short: group.name_short, + email: group.email, + })); return groups; } catch (error) { From 83e5d650b8694bcb125c2b9a3bcc58731b667c07 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 18:20:41 +0200 Subject: [PATCH 042/169] update sendInterviewTimes to check the period --- lib/mongo/periods.ts | 22 +++++ lib/utils/sendInterviewTimes/formatAndSend.ts | 34 +++---- .../sendInterviewTimes/sendInterviewTimes.ts | 99 +++++++++++-------- 3 files changed, 93 insertions(+), 62 deletions(-) diff --git a/lib/mongo/periods.ts b/lib/mongo/periods.ts index d677a4d0..93d5870c 100644 --- a/lib/mongo/periods.ts +++ b/lib/mongo/periods.ts @@ -122,3 +122,25 @@ export const deletePeriodById = async (periodId: string | ObjectId) => { return { error: "Failed to delete period" }; } }; + +export const updatePeriod = async (periodId: string) => { + try { + if (!periods) await init(); + + const result = await periods.findOneAndUpdate( + { periodId: periodId }, + { $set: { hasSentInterviewTimes: true } }, + { returnDocument: "after" } + ); + + const updatedPeriod = result; + + if (updatedPeriod) { + return { updatedState: updatedPeriod.hasSentInterviewTimes }; + } else { + return { error: "Failed to update hasSentInterviewTimes" }; + } + } catch (error) { + return { error: "Failed to update hasSentInterviewTimes" }; + } +}; diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 8502767c..55558451 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -61,19 +61,17 @@ export const formatAndSendEmails = async ({ emailBody += `

Skjedd en feil? Ta kontakt med Appkom❤️

`; phoneBody += `Skjedd en feil? Ta kontakt med Appkom`; - // await sendEmail({ - // sesClient: sesClient, - // fromEmail: "opptak@online.ntnu.no", - // toEmails: applicantEmail, - // subject: subject, - // htmlContent: emailBody, - // }); + await sendEmail({ + sesClient: sesClient, + fromEmail: "opptak@online.ntnu.no", + toEmails: applicantEmail, + subject: subject, + htmlContent: emailBody, + }); let toPhoneNumber = "+47"; toPhoneNumber += typedApplicant.applicantPhone; - // sendSMS(toPhoneNumber, phoneBody); - - console.log(applicantEmail[0], "\n", subject, "\n", emailBody); + sendSMS(toPhoneNumber, phoneBody); } // Send email to each committee @@ -108,15 +106,13 @@ export const formatAndSendEmails = async ({ body += `

Skjedd en feil? Ta kontakt med Appkom❤️

`; - // await sendEmail({ - // sesClient: sesClient, - // fromEmail: "opptak@online.ntnu.no", - // toEmails: committeeEmail, - // subject: subject, - // htmlContent: body, - // }); - - console.log(committeeEmail[0], "\n", subject, "\n", body); + await sendEmail({ + sesClient: sesClient, + fromEmail: "opptak@online.ntnu.no", + toEmails: committeeEmail, + subject: subject, + htmlContent: body, + }); } } catch (error) { return { error: "Failed to send out interview times" }; diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index 2c7e53f3..d78bc77e 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -1,5 +1,3 @@ -import { SESClient } from "@aws-sdk/client-ses"; -import sendEmail from "../sendEmail"; import { committeeEmails, committeeInterviewType, @@ -11,56 +9,71 @@ import { import { fetchCommitteeEmails } from "./fetchFunctions"; import { formatAndSendEmails } from "./formatAndSend"; -import { getPeriodById } from "../../mongo/periods"; +import { getPeriods, updatePeriod } from "../../mongo/periods"; import { getCommitteesByPeriod } from "../../mongo/committees"; import { getInterviewsByPeriod } from "../../mongo/interviews"; -interface Props { - periodId: string; -} - -export const sendOutInterviewTimes = async ({ periodId }: Props) => { +export const sendOutInterviewTimes = async () => { try { - const periodData = await getPeriodById(periodId); + const periodData = await getPeriods(); - if (!periodData.exists || !periodData.period) { + if (!periodData || !periodData.periods) { return { error: "Failed to find period" }; } - const period: periodType = periodData.period; - - const commmitteeInterviewTimesData = await getCommitteesByPeriod(periodId); - if (!commmitteeInterviewTimesData || commmitteeInterviewTimesData.error) { - return { error: "Failed to find committee interview times" }; + for (const period of periodData.periods) { + if ( + period.hasSentInterviewTimes === false && + period.interviewPeriod.end < new Date() + ) { + const periodId = String(period._id); + try { + const commmitteeInterviewTimesData = + await getCommitteesByPeriod(periodId); + + if ( + !commmitteeInterviewTimesData || + commmitteeInterviewTimesData.error + ) { + return { error: "Failed to find committee interview times" }; + } + + const committeeInterviewTimes: committeeInterviewType[] = + commmitteeInterviewTimesData.result || []; + + const committeeEmails: committeeEmails[] = + await fetchCommitteeEmails(); + + const fetchedAlgorithmData = await getInterviewsByPeriod(periodId); + + const algorithmData: algorithmType[] = + fetchedAlgorithmData.interviews || []; + + const applicantsToEmail: emailApplicantInterviewType[] = + formatApplicants( + algorithmData, + periodId, + period, + committeeEmails, + committeeInterviewTimes + ); + + const committeesToEmail: emailCommitteeInterviewType[] = + formatCommittees(applicantsToEmail); + + const dataToSend = { + committeesToEmail, + applicantsToEmail, + }; + + formatAndSendEmails(dataToSend); + } catch (error) { + return { error: "Failed to send out interview times" }; + } finally { + updatePeriod(periodId); + } + } } - - const committeeInterviewTimes: committeeInterviewType[] = - commmitteeInterviewTimesData.result || []; - - const committeeEmails: committeeEmails[] = await fetchCommitteeEmails(); - - const fetchedAlgorithmData = await getInterviewsByPeriod(periodId); - - const algorithmData: algorithmType[] = - fetchedAlgorithmData.interviews || []; - - const applicantsToEmail: emailApplicantInterviewType[] = formatApplicants( - algorithmData, - periodId, - period, - committeeEmails, - committeeInterviewTimes - ); - - const committeesToEmail: emailCommitteeInterviewType[] = - formatCommittees(applicantsToEmail); - - const dataToSend = { - committeesToEmail, - applicantsToEmail, - }; - - formatAndSendEmails(dataToSend); } catch (error) { return { error: "Failed to send out interview times" }; } From e70039cc4b26070c78ba6b55a6d9dadfbff2391a Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 18:22:44 +0200 Subject: [PATCH 043/169] remove unused stuff --- pages/admin/[period-id]/index.tsx | 68 ++----------------- .../api/committees/times/[period-id]/index.ts | 26 +++---- pages/api/interviews/[period-id].ts | 45 ------------ 3 files changed, 15 insertions(+), 124 deletions(-) delete mode 100644 pages/api/interviews/[period-id].ts diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index 2d8bcc59..64569fee 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -4,22 +4,17 @@ import router from "next/router"; import { periodType } from "../../../lib/types/types"; import NotFound from "../../404"; import ApplicantsOverview from "../../../components/applicantoverview/ApplicantsOverview"; -import { Tabs } from "../../../components/Tabs"; -import { CalendarIcon, InboxIcon } from "@heroicons/react/24/solid"; -import Button from "../../../components/Button"; import { useQuery } from "@tanstack/react-query"; import { fetchPeriodById } from "../../../lib/api/periodApi"; import LoadingPage from "../../../components/LoadingPage"; import ErrorPage from "../../../components/ErrorPage"; -import toast from "react-hot-toast"; const Admin = () => { const { data: session } = useSession(); - const periodId = router.query["period-id"] as string; + const periodId = router.query["period-id"]; + const [period, setPeriod] = useState(null); const [committees, setCommittees] = useState(null); - const [activeTab, setActiveTab] = useState(0); - const [tabClicked, setTabClicked] = useState(0); const { data, isError, isLoading } = useQuery({ queryKey: ["periods", periodId], @@ -33,25 +28,6 @@ const Admin = () => { ); }, [data, session?.user?.owId]); - const sendOutInterviewTimes = async ({ periodId }: { periodId: string }) => { - try { - const response = await fetch(`/api/interviews/${periodId}`, { - method: "POST", - }); - if (!response.ok) { - throw new Error("Failed to send out interview times"); - } - const data = await response.json(); - if (data.error) { - throw new Error(data.error); - } - toast.success("Intervjutider er sendt ut! (Sjekk konsoll loggen)"); - return data; - } catch (error) { - toast.error("Failed to send out interview times"); - } - }; - console.log(committees); if (session?.user?.role !== "admin") return ; @@ -59,41 +35,11 @@ const Admin = () => { if (isError) return ; return ( -
- { - setActiveTab(index); - setTabClicked(index); - }} - content={[ - { - title: "Søkere", - icon: , - content: ( - - ), - }, - { - title: "Send ut", - icon: , - content: ( -
-
- ), - }, - ]} - /> -
+ ); }; diff --git a/pages/api/committees/times/[period-id]/index.ts b/pages/api/committees/times/[period-id]/index.ts index 5be5a1f1..9cd92f1b 100644 --- a/pages/api/committees/times/[period-id]/index.ts +++ b/pages/api/committees/times/[period-id]/index.ts @@ -2,15 +2,12 @@ import { NextApiRequest, NextApiResponse } from "next"; import { getCommittees, createCommittee, - getCommitteesByPeriod, + deleteCommittee, + updateCommitteeMessage, } from "../../../../../lib/mongo/committees"; import { getServerSession } from "next-auth"; import { authOptions } from "../../../auth/[...nextauth]"; -import { - hasSession, - isAdmin, - isInCommitee, -} from "../../../../../lib/utils/apiChecks"; +import { hasSession, isInCommitee } from "../../../../../lib/utils/apiChecks"; import { isCommitteeType, validateCommittee, @@ -31,17 +28,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (!hasSession(res, session)) return; if (!isInCommitee(res, session)) return; - if (req.method === "GET") { - if (!isAdmin(res, session)) return; - - try { - const committees = await getCommitteesByPeriod(periodId); - return res.status(200).json({ committees }); - } catch (error: any) { - return res.status(500).json({ error: error.message }); - } - } - if (req.method === "POST") { const committeeData: commiteeType = req.body; @@ -54,6 +40,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(400).json({ error: "Invalid periodId" }); } + if (new Date() > new Date(period.applicationPeriod.end)) { + return res.status(400).json({ error: "Application period has ended" }); + } + if (!validateCommittee(committeeData, period)) { return res.status(400).json({ error: "Invalid data format" }); } @@ -72,7 +62,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } } - res.setHeader("Allow", ["POST", "GET"]); + res.setHeader("Allow", ["POST"]); res.status(405).end(`Method ${req.method} is not allowed.`); }; diff --git a/pages/api/interviews/[period-id].ts b/pages/api/interviews/[period-id].ts deleted file mode 100644 index 77a5fe1a..00000000 --- a/pages/api/interviews/[period-id].ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; - -import { getServerSession } from "next-auth"; -import { authOptions } from "../auth/[...nextauth]"; -import { - hasSession, - isAdmin, - isInCommitee, -} from "../../../lib/utils/apiChecks"; -import { sendOutInterviewTimes } from "../../../lib/utils/sendInterviewTimes/sendInterviewTimes"; - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions); - const periodId = req.query["period-id"]; - - if (!periodId || typeof periodId !== "string") { - return res - .status(400) - .json({ error: "Invalid or missing periodId parameter" }); - } - - if (!hasSession(res, session)) return; - if (!isInCommitee(res, session)) return; - - if (req.method === "POST") { - if (!isAdmin(res, session)) return; - - try { - const result = await sendOutInterviewTimes({ periodId }); - if (result && result.error) { - throw new Error(result.error); - } - - if (result?.error) throw new Error(result.error); - return res.status(201).json({ result }); - } catch (error: any) { - return res.status(500).json({ error: error.message }); - } - } - - res.setHeader("Allow", ["POST"]); - res.status(405).end(`Method ${req.method} is not allowed.`); -}; - -export default handler; From 82a16b1fa7c9fd9ceef507741465af98ad46d9f3 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 18:24:05 +0200 Subject: [PATCH 044/169] update sendEmail --- lib/utils/sendInterviewTimes/formatAndSend.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 55558451..ed918084 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -5,7 +5,7 @@ import { } from "../../types/types"; import { changeDisplayName } from "../toString"; import { formatDateHours } from "../dateUtils"; -import sendEmail from "../sendEmail"; +import sendEmail from "../../email/sendEmail"; import sendSMS from "./sendSMS"; interface sendInterviewTimesProps { @@ -62,8 +62,6 @@ export const formatAndSendEmails = async ({ phoneBody += `Skjedd en feil? Ta kontakt med Appkom`; await sendEmail({ - sesClient: sesClient, - fromEmail: "opptak@online.ntnu.no", toEmails: applicantEmail, subject: subject, htmlContent: emailBody, @@ -107,8 +105,6 @@ export const formatAndSendEmails = async ({ body += `

Skjedd en feil? Ta kontakt med Appkom❤️

`; await sendEmail({ - sesClient: sesClient, - fromEmail: "opptak@online.ntnu.no", toEmails: committeeEmail, subject: subject, htmlContent: body, From fe33e8b0e0107ea1a6d01f1d2d3fa25c6d9c645f Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 18:26:21 +0200 Subject: [PATCH 045/169] correct check --- lib/utils/sendInterviewTimes/sendInterviewTimes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index d78bc77e..3cb8d9ba 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -24,7 +24,7 @@ export const sendOutInterviewTimes = async () => { for (const period of periodData.periods) { if ( period.hasSentInterviewTimes === false && - period.interviewPeriod.end < new Date() + period.applicationPeriod.end < new Date() ) { const periodId = String(period._id); try { From 3d55a92ff03b9a89b42fc303b831e1da8c341176 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 29 Jul 2024 19:13:18 +0200 Subject: [PATCH 046/169] repo to manually send interview times --- .../committee/CommitteeInterviewTimes.tsx | 48 +------ components/committee/SendCommitteeMessage.tsx | 11 -- lib/api/applicantApi.ts | 35 ----- lib/mongo/periods.ts | 22 ---- lib/utils/dateUtils.ts | 10 +- lib/utils/sendEmail.ts | 44 +++++++ lib/utils/sendInterviewTimes/formatAndSend.ts | 32 +++-- .../sendInterviewTimes/sendInterviewTimes.ts | 99 ++++++-------- lib/utils/validators.ts | 5 +- package-lock.json | 8 +- package.json | 2 +- pages/admin/[period-id]/index.tsx | 68 +++++++++- pages/api/applicants/index.ts | 10 +- .../times/[period-id]/[committee].ts | 19 --- .../api/committees/times/[period-id]/index.ts | 26 ++-- pages/api/interviews/[period-id].ts | 45 +++++++ pages/application/[period-id].tsx | 124 +++++++++++------- pages/apply.tsx | 42 +++--- pages/committees.tsx | 2 +- 19 files changed, 351 insertions(+), 301 deletions(-) create mode 100644 lib/utils/sendEmail.ts create mode 100644 pages/api/interviews/[period-id].ts diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 38e1f139..8863b785 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -9,7 +9,6 @@ import toast from "react-hot-toast"; import NotFound from "../../pages/404"; import Button from "../Button"; import ImportantNote from "../ImportantNote"; -import useUnsavedChangesWarning from "../../lib/utils/unSavedChangesWarning"; interface Interview { title: string; @@ -37,7 +36,6 @@ const CommitteeInterviewTimes = ({ const [visibleRange, setVisibleRange] = useState({ start: "", end: "" }); const [selectedTimeslot, setSelectedTimeslot] = useState("15"); - const [interviewsPlanned, setInterviewsPlanned] = useState(0); const [calendarEvents, setCalendarEvents] = useState([]); const [hasAlreadySubmitted, setHasAlreadySubmitted] = @@ -50,10 +48,6 @@ const CommitteeInterviewTimes = ({ const inputRef = useRef(null); const calendarRef = useRef(null); - const [deadLineHasPassed, setDeadLineHasPassed] = useState(false); - - const { unsavedChanges, setUnsavedChanges } = useUnsavedChangesWarning(); - useEffect(() => { if (period) { setVisibleRange({ @@ -118,16 +112,9 @@ const CommitteeInterviewTimes = ({ } }, [isModalOpen]); - useEffect(() => { - if (calendarEvents.length > 0) { - calculateInterviewsPlanned(); - } - }, [calendarEvents, selectedTimeslot]); - const handleDateSelect = (selectionInfo: any) => { setCurrentSelection(selectionInfo); setIsModalOpen(true); - setUnsavedChanges(true); }; const handleRoomSubmit = () => { @@ -144,7 +131,7 @@ const CommitteeInterviewTimes = ({ const calendarApi = currentSelection.view.calendar; calendarApi.addEvent(event); - calendarApi.render(); + calendarApi.render(); // Force the calendar to re-render addCell([ roomInput, @@ -154,7 +141,7 @@ const CommitteeInterviewTimes = ({ setRoomInput(""); setIsModalOpen(false); - setCalendarEvents((prevEvents) => [...prevEvents, event]); + setCalendarEvents((prevEvents) => [...prevEvents, event]); // Trigger re-render }; const submit = async (e: BaseSyntheticEvent) => { @@ -190,7 +177,6 @@ const CommitteeInterviewTimes = ({ const result = await response.json(); toast.success("Tidene er sendt inn!"); setHasAlreadySubmitted(true); - setUnsavedChanges(false); } catch (error) { toast.error("Kunne ikke sende inn!"); } @@ -203,7 +189,6 @@ const CommitteeInterviewTimes = ({ ) ); event.remove(); - setUnsavedChanges(true); }; const addCell = (cell: string[]) => { @@ -211,12 +196,10 @@ const CommitteeInterviewTimes = ({ ...markedCells, { title: cell[0], start: cell[1], end: cell[2] }, ]); - setUnsavedChanges(true); }; const updateInterviewInterval = (e: BaseSyntheticEvent) => { setInterviewInterval(parseInt(e.target.value)); - setUnsavedChanges(true); }; const renderEventContent = (eventContent: any) => { @@ -269,7 +252,6 @@ const CommitteeInterviewTimes = ({ const handleTimeslotSelection = (e: React.ChangeEvent) => { setSelectedTimeslot(e.target.value); - setUnsavedChanges(true); }; const deleteSubmission = async (e: BaseSyntheticEvent) => { @@ -291,7 +273,6 @@ const CommitteeInterviewTimes = ({ setHasAlreadySubmitted(false); setCalendarEvents([]); - setUnsavedChanges(false); } catch (error: any) { console.error("Error deleting submission:", error); toast.error("Klarte ikke å slette innsendingen"); @@ -309,16 +290,12 @@ const CommitteeInterviewTimes = ({ }, [period]); const getSubmissionDeadline = (): string => { - const deadlineIso = period!.applicationPeriod.end; + const deadlineIso = period!.interviewPeriod.start; - if (deadlineIso != null && !deadLineHasPassed) { + if (deadlineIso != null) { const deadlineDate = new Date(deadlineIso); const now = new Date(); - if (now > deadlineDate) { - setDeadLineHasPassed(true); - } - let delta = Math.floor((deadlineDate.getTime() - now.getTime()) / 1000); const days = Math.floor(delta / 86400); @@ -344,25 +321,11 @@ const CommitteeInterviewTimes = ({ return ""; }; - const calculateInterviewsPlanned = () => { - const totalMinutes = calendarEvents.reduce((acc, event) => { - const start = new Date(event.start); - const end = new Date(event.end); - const duration = (end.getTime() - start.getTime()) / 1000 / 60; - return acc + duration; - }, 0); - - const plannedInterviews = Math.floor( - totalMinutes / parseInt(selectedTimeslot) - ); - setInterviewsPlanned(plannedInterviews); - }; - if (!session || !session.user?.isCommittee) { return ; } - if (deadLineHasPassed) { + if (period!.interviewPeriod.start < new Date()) { return (

@@ -412,7 +375,6 @@ const CommitteeInterviewTimes = ({

)} -

{`${interviewsPlanned} intervjuer planlagt`}

{ @@ -80,16 +79,6 @@ const SendCommitteeMessage = ({ } }; - if (new Date(period!.applicationPeriod.end) < new Date()) { - return ( -
-

- Det er ikke lenger mulig å legge inn tider! -

-
- ); - } - return (

diff --git a/lib/api/applicantApi.ts b/lib/api/applicantApi.ts index ac0ef8c5..7f72a51c 100644 --- a/lib/api/applicantApi.ts +++ b/lib/api/applicantApi.ts @@ -1,5 +1,4 @@ import { QueryFunctionContext } from "@tanstack/react-query"; -import { applicantType } from "../types/types"; export const fetchApplicantByPeriodAndId = async ( context: QueryFunctionContext @@ -27,37 +26,3 @@ export const fetchApplicantsByPeriodIdAndCommittee = async ( (res) => res.json() ); }; - -export const createApplicant = async (applicant: applicantType) => { - const response = await fetch(`/api/applicants/`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(applicant), - }); - - const data = await response.json(); - if (!response.ok) { - throw new Error(data.error || "Unknown error occurred"); - } - return data; -}; - -export const deleteApplicant = async ({ - periodId, - owId, -}: { - periodId: string; - owId: string; -}) => { - const response = await fetch(`/api/applicants/${periodId}/${owId}`, { - method: "DELETE", - }); - - if (!response.ok) { - throw new Error("Failed to delete the application"); - } - - return response; -}; diff --git a/lib/mongo/periods.ts b/lib/mongo/periods.ts index 93d5870c..d677a4d0 100644 --- a/lib/mongo/periods.ts +++ b/lib/mongo/periods.ts @@ -122,25 +122,3 @@ export const deletePeriodById = async (periodId: string | ObjectId) => { return { error: "Failed to delete period" }; } }; - -export const updatePeriod = async (periodId: string) => { - try { - if (!periods) await init(); - - const result = await periods.findOneAndUpdate( - { periodId: periodId }, - { $set: { hasSentInterviewTimes: true } }, - { returnDocument: "after" } - ); - - const updatedPeriod = result; - - if (updatedPeriod) { - return { updatedState: updatedPeriod.hasSentInterviewTimes }; - } else { - return { error: "Failed to update hasSentInterviewTimes" }; - } - } catch (error) { - return { error: "Failed to update hasSentInterviewTimes" }; - } -}; diff --git a/lib/utils/dateUtils.ts b/lib/utils/dateUtils.ts index 5338ee9d..c2ddfe7b 100644 --- a/lib/utils/dateUtils.ts +++ b/lib/utils/dateUtils.ts @@ -25,15 +25,15 @@ export const formatDateHours = (inputDate: undefined | Date) => { export const formatDateNorwegian = (inputDate?: Date): string => { const date = new Date(inputDate || ""); - const day = date.getDate().toString().padStart(2); + const day = date.getDate().toString().padStart(2, "0"); const monthsNorwegian = [ "jan", "feb", - "mar", - "apr", + "mars", + "april", "mai", - "jun", - "jul", + "juni", + "juli", "aug", "sep", "okt", diff --git a/lib/utils/sendEmail.ts b/lib/utils/sendEmail.ts new file mode 100644 index 00000000..74be47cd --- /dev/null +++ b/lib/utils/sendEmail.ts @@ -0,0 +1,44 @@ +import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses"; + +interface SendEmailProps { + sesClient: SESClient; + fromEmail: string; + toEmails: string[]; + subject: string; + htmlContent: string; +} + +export default async function sendEmail(emailParams: SendEmailProps) { + try { + const sesClient = emailParams.sesClient; + const params = { + Source: emailParams.fromEmail, + Destination: { + ToAddresses: emailParams.toEmails, + CcAddresses: [], + BccAddresses: [], + }, + Message: { + Subject: { + Data: emailParams.subject, + Charset: "UTF-8", + }, + Body: { + Html: { + Data: emailParams.htmlContent, + Charset: "UTF-8", + }, + }, + }, + ReplyToAddresses: [], + }; + + const command = new SendEmailCommand(params); + await sesClient.send(command); + + console.log("Email sent to: ", emailParams.toEmails); + } catch (error) { + console.error("Error sending email: ", error); + throw error; + } +} diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index ed918084..8502767c 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -5,7 +5,7 @@ import { } from "../../types/types"; import { changeDisplayName } from "../toString"; import { formatDateHours } from "../dateUtils"; -import sendEmail from "../../email/sendEmail"; +import sendEmail from "../sendEmail"; import sendSMS from "./sendSMS"; interface sendInterviewTimesProps { @@ -61,15 +61,19 @@ export const formatAndSendEmails = async ({ emailBody += `

Skjedd en feil? Ta kontakt med Appkom❤️

`; phoneBody += `Skjedd en feil? Ta kontakt med Appkom`; - await sendEmail({ - toEmails: applicantEmail, - subject: subject, - htmlContent: emailBody, - }); + // await sendEmail({ + // sesClient: sesClient, + // fromEmail: "opptak@online.ntnu.no", + // toEmails: applicantEmail, + // subject: subject, + // htmlContent: emailBody, + // }); let toPhoneNumber = "+47"; toPhoneNumber += typedApplicant.applicantPhone; - sendSMS(toPhoneNumber, phoneBody); + // sendSMS(toPhoneNumber, phoneBody); + + console.log(applicantEmail[0], "\n", subject, "\n", emailBody); } // Send email to each committee @@ -104,11 +108,15 @@ export const formatAndSendEmails = async ({ body += `

Skjedd en feil? Ta kontakt med Appkom❤️

`; - await sendEmail({ - toEmails: committeeEmail, - subject: subject, - htmlContent: body, - }); + // await sendEmail({ + // sesClient: sesClient, + // fromEmail: "opptak@online.ntnu.no", + // toEmails: committeeEmail, + // subject: subject, + // htmlContent: body, + // }); + + console.log(committeeEmail[0], "\n", subject, "\n", body); } } catch (error) { return { error: "Failed to send out interview times" }; diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index 3cb8d9ba..2c7e53f3 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -1,3 +1,5 @@ +import { SESClient } from "@aws-sdk/client-ses"; +import sendEmail from "../sendEmail"; import { committeeEmails, committeeInterviewType, @@ -9,71 +11,56 @@ import { import { fetchCommitteeEmails } from "./fetchFunctions"; import { formatAndSendEmails } from "./formatAndSend"; -import { getPeriods, updatePeriod } from "../../mongo/periods"; +import { getPeriodById } from "../../mongo/periods"; import { getCommitteesByPeriod } from "../../mongo/committees"; import { getInterviewsByPeriod } from "../../mongo/interviews"; -export const sendOutInterviewTimes = async () => { +interface Props { + periodId: string; +} + +export const sendOutInterviewTimes = async ({ periodId }: Props) => { try { - const periodData = await getPeriods(); + const periodData = await getPeriodById(periodId); - if (!periodData || !periodData.periods) { + if (!periodData.exists || !periodData.period) { return { error: "Failed to find period" }; } + const period: periodType = periodData.period; - for (const period of periodData.periods) { - if ( - period.hasSentInterviewTimes === false && - period.applicationPeriod.end < new Date() - ) { - const periodId = String(period._id); - try { - const commmitteeInterviewTimesData = - await getCommitteesByPeriod(periodId); - - if ( - !commmitteeInterviewTimesData || - commmitteeInterviewTimesData.error - ) { - return { error: "Failed to find committee interview times" }; - } - - const committeeInterviewTimes: committeeInterviewType[] = - commmitteeInterviewTimesData.result || []; - - const committeeEmails: committeeEmails[] = - await fetchCommitteeEmails(); - - const fetchedAlgorithmData = await getInterviewsByPeriod(periodId); - - const algorithmData: algorithmType[] = - fetchedAlgorithmData.interviews || []; - - const applicantsToEmail: emailApplicantInterviewType[] = - formatApplicants( - algorithmData, - periodId, - period, - committeeEmails, - committeeInterviewTimes - ); - - const committeesToEmail: emailCommitteeInterviewType[] = - formatCommittees(applicantsToEmail); - - const dataToSend = { - committeesToEmail, - applicantsToEmail, - }; - - formatAndSendEmails(dataToSend); - } catch (error) { - return { error: "Failed to send out interview times" }; - } finally { - updatePeriod(periodId); - } - } + const commmitteeInterviewTimesData = await getCommitteesByPeriod(periodId); + + if (!commmitteeInterviewTimesData || commmitteeInterviewTimesData.error) { + return { error: "Failed to find committee interview times" }; } + + const committeeInterviewTimes: committeeInterviewType[] = + commmitteeInterviewTimesData.result || []; + + const committeeEmails: committeeEmails[] = await fetchCommitteeEmails(); + + const fetchedAlgorithmData = await getInterviewsByPeriod(periodId); + + const algorithmData: algorithmType[] = + fetchedAlgorithmData.interviews || []; + + const applicantsToEmail: emailApplicantInterviewType[] = formatApplicants( + algorithmData, + periodId, + period, + committeeEmails, + committeeInterviewTimes + ); + + const committeesToEmail: emailCommitteeInterviewType[] = + formatCommittees(applicantsToEmail); + + const dataToSend = { + committeesToEmail, + applicantsToEmail, + }; + + formatAndSendEmails(dataToSend); } catch (error) { return { error: "Failed to send out interview times" }; } diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts index 47b88887..f49faf7c 100644 --- a/lib/utils/validators.ts +++ b/lib/utils/validators.ts @@ -79,8 +79,6 @@ export const validateCommittee = (data: any, period: periodType): boolean => { const isPeriodNameValid = data.periodId === String(period._id); - const isBeforeDeadline = new Date() <= new Date(period.applicationPeriod.end); - const committeeExists = period.committees.some((committee) => { return committee.toLowerCase() === data.committee.toLowerCase(); @@ -107,8 +105,7 @@ export const validateCommittee = (data: any, period: periodType): boolean => { hasBasicFields && isPeriodNameValid && committeeExists && - isWithinInterviewPeriod && - isBeforeDeadline + isWithinInterviewPeriod ); }; diff --git a/package-lock.json b/package-lock.json index 7df220e6..423703e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "autoprefixer": "^10.4.12", "eslint": "8.24.0", "eslint-config-next": "12.3.1", - "postcss": "^8.4.40", + "postcss": "^8.4.16", "prettier": "3.0.3", "tailwindcss": "^3.1.8", "typescript": "4.8.3" @@ -5345,9 +5345,9 @@ } }, "node_modules/postcss": { - "version": "8.4.40", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", - "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "funding": [ { "type": "opencollective", diff --git a/package.json b/package.json index 0daab81c..7a94dc90 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "autoprefixer": "^10.4.12", "eslint": "8.24.0", "eslint-config-next": "12.3.1", - "postcss": "^8.4.40", + "postcss": "^8.4.16", "prettier": "3.0.3", "tailwindcss": "^3.1.8", "typescript": "4.8.3" diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index 64569fee..2d8bcc59 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -4,17 +4,22 @@ import router from "next/router"; import { periodType } from "../../../lib/types/types"; import NotFound from "../../404"; import ApplicantsOverview from "../../../components/applicantoverview/ApplicantsOverview"; +import { Tabs } from "../../../components/Tabs"; +import { CalendarIcon, InboxIcon } from "@heroicons/react/24/solid"; +import Button from "../../../components/Button"; import { useQuery } from "@tanstack/react-query"; import { fetchPeriodById } from "../../../lib/api/periodApi"; import LoadingPage from "../../../components/LoadingPage"; import ErrorPage from "../../../components/ErrorPage"; +import toast from "react-hot-toast"; const Admin = () => { const { data: session } = useSession(); - const periodId = router.query["period-id"]; - + const periodId = router.query["period-id"] as string; const [period, setPeriod] = useState(null); const [committees, setCommittees] = useState(null); + const [activeTab, setActiveTab] = useState(0); + const [tabClicked, setTabClicked] = useState(0); const { data, isError, isLoading } = useQuery({ queryKey: ["periods", periodId], @@ -28,6 +33,25 @@ const Admin = () => { ); }, [data, session?.user?.owId]); + const sendOutInterviewTimes = async ({ periodId }: { periodId: string }) => { + try { + const response = await fetch(`/api/interviews/${periodId}`, { + method: "POST", + }); + if (!response.ok) { + throw new Error("Failed to send out interview times"); + } + const data = await response.json(); + if (data.error) { + throw new Error(data.error); + } + toast.success("Intervjutider er sendt ut! (Sjekk konsoll loggen)"); + return data; + } catch (error) { + toast.error("Failed to send out interview times"); + } + }; + console.log(committees); if (session?.user?.role !== "admin") return ; @@ -35,11 +59,41 @@ const Admin = () => { if (isError) return ; return ( - +
+ { + setActiveTab(index); + setTabClicked(index); + }} + content={[ + { + title: "Søkere", + icon: , + content: ( + + ), + }, + { + title: "Send ut", + icon: , + content: ( +
+
+ ), + }, + ]} + /> +
); }; diff --git a/pages/api/applicants/index.ts b/pages/api/applicants/index.ts index 5f971757..da40e134 100644 --- a/pages/api/applicants/index.ts +++ b/pages/api/applicants/index.ts @@ -6,10 +6,10 @@ import { getServerSession } from "next-auth"; import { emailDataType } from "../../../lib/types/types"; import { isApplicantType } from "../../../lib/utils/validators"; import { isAdmin, hasSession, checkOwId } from "../../../lib/utils/apiChecks"; +import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses"; import capitalizeFirstLetter from "../../../lib/utils/capitalizeFirstLetter"; -import sendEmail from "../../../lib/email/sendEmail"; +import sendEmail from "../../../lib/utils/sendEmail"; import { changeDisplayName } from "../../../lib/utils/toString"; -import { generateApplicantEmail } from "../../../lib/email/applicantEmailTemplate"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -55,6 +55,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { applicant, error } = await createApplicant(requestBody); if (error) throw new Error(error); + const sesClient = new SESClient({ region: "eu-north-1" }); + if (applicant != null) { let optionalCommitteesString = ""; if (applicant.optionalCommittees.length > 0) { @@ -101,9 +103,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { try { await sendEmail({ + sesClient: sesClient, + fromEmail: "opptak@online.ntnu.no", toEmails: emailData.emails, subject: "Vi har mottatt din søknad!", - htmlContent: generateApplicantEmail(emailData), + htmlContent: `Dette er en bekreftelse på at vi har mottatt din søknad. Du vil motta en ny e-post med intervjutider etter søkeperioden er over. Her er en oppsummering av din søknad:

E-post: ${emailData.emails[0]}

Fullt navn: ${emailData.name}

Telefonnummer: ${emailData.phone}

Trinn: ${emailData.grade}

Førstevalg: ${emailData.firstChoice}

Andrevalg: ${emailData.secondChoice}

Tredjevalg: ${emailData.thirdChoice}

Ønsker du å være økonomiansvarlig: ${emailData.bankom}

Andre valg: ${emailData.optionalCommittees}

Kort om deg selv:
${emailData.about}`, }); console.log("Email sent to: ", emailData.emails); diff --git a/pages/api/committees/times/[period-id]/[committee].ts b/pages/api/committees/times/[period-id]/[committee].ts index ff20f15e..13871657 100644 --- a/pages/api/committees/times/[period-id]/[committee].ts +++ b/pages/api/committees/times/[period-id]/[committee].ts @@ -7,7 +7,6 @@ import { import { getServerSession } from "next-auth"; import { authOptions } from "../../../auth/[...nextauth]"; import { hasSession, isInCommitee } from "../../../../../lib/utils/apiChecks"; -import { getPeriodById } from "../../../../../lib/mongo/periods"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -52,15 +51,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(400).json({ error: "Invalid message parameter" }); } - const { period } = await getPeriodById(String(periodId)); - if (!period) { - return res.status(400).json({ error: "Invalid periodId" }); - } - - if (new Date() > new Date(period.applicationPeriod.end)) { - return res.status(400).json({ error: "Application period has ended" }); - } - const { updatedMessage, error } = await updateCommitteeMessage( selectedCommittee, periodId, @@ -77,15 +67,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === "DELETE") { try { - const { period } = await getPeriodById(String(periodId)); - if (!period) { - return res.status(400).json({ error: "Invalid periodId" }); - } - - if (new Date() > new Date(period.applicationPeriod.end)) { - return res.status(400).json({ error: "Application period has ended" }); - } - const { error } = await deleteCommittee( selectedCommittee, periodId, diff --git a/pages/api/committees/times/[period-id]/index.ts b/pages/api/committees/times/[period-id]/index.ts index 9cd92f1b..5be5a1f1 100644 --- a/pages/api/committees/times/[period-id]/index.ts +++ b/pages/api/committees/times/[period-id]/index.ts @@ -2,12 +2,15 @@ import { NextApiRequest, NextApiResponse } from "next"; import { getCommittees, createCommittee, - deleteCommittee, - updateCommitteeMessage, + getCommitteesByPeriod, } from "../../../../../lib/mongo/committees"; import { getServerSession } from "next-auth"; import { authOptions } from "../../../auth/[...nextauth]"; -import { hasSession, isInCommitee } from "../../../../../lib/utils/apiChecks"; +import { + hasSession, + isAdmin, + isInCommitee, +} from "../../../../../lib/utils/apiChecks"; import { isCommitteeType, validateCommittee, @@ -28,6 +31,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (!hasSession(res, session)) return; if (!isInCommitee(res, session)) return; + if (req.method === "GET") { + if (!isAdmin(res, session)) return; + + try { + const committees = await getCommitteesByPeriod(periodId); + return res.status(200).json({ committees }); + } catch (error: any) { + return res.status(500).json({ error: error.message }); + } + } + if (req.method === "POST") { const committeeData: commiteeType = req.body; @@ -40,10 +54,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { return res.status(400).json({ error: "Invalid periodId" }); } - if (new Date() > new Date(period.applicationPeriod.end)) { - return res.status(400).json({ error: "Application period has ended" }); - } - if (!validateCommittee(committeeData, period)) { return res.status(400).json({ error: "Invalid data format" }); } @@ -62,7 +72,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } } - res.setHeader("Allow", ["POST"]); + res.setHeader("Allow", ["POST", "GET"]); res.status(405).end(`Method ${req.method} is not allowed.`); }; diff --git a/pages/api/interviews/[period-id].ts b/pages/api/interviews/[period-id].ts new file mode 100644 index 00000000..77a5fe1a --- /dev/null +++ b/pages/api/interviews/[period-id].ts @@ -0,0 +1,45 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { getServerSession } from "next-auth"; +import { authOptions } from "../auth/[...nextauth]"; +import { + hasSession, + isAdmin, + isInCommitee, +} from "../../../lib/utils/apiChecks"; +import { sendOutInterviewTimes } from "../../../lib/utils/sendInterviewTimes/sendInterviewTimes"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const session = await getServerSession(req, res, authOptions); + const periodId = req.query["period-id"]; + + if (!periodId || typeof periodId !== "string") { + return res + .status(400) + .json({ error: "Invalid or missing periodId parameter" }); + } + + if (!hasSession(res, session)) return; + if (!isInCommitee(res, session)) return; + + if (req.method === "POST") { + if (!isAdmin(res, session)) return; + + try { + const result = await sendOutInterviewTimes({ periodId }); + if (result && result.error) { + throw new Error(result.error); + } + + if (result?.error) throw new Error(result.error); + return res.status(201).json({ result }); + } catch (error: any) { + return res.status(500).json({ error: error.message }); + } + } + + res.setHeader("Allow", ["POST"]); + res.status(405).end(`Method ${req.method} is not allowed.`); +}; + +export default handler; diff --git a/pages/application/[period-id].tsx b/pages/application/[period-id].tsx index 88064337..34a68086 100644 --- a/pages/application/[period-id].tsx +++ b/pages/application/[period-id].tsx @@ -16,22 +16,25 @@ import ApplicantCard from "../../components/applicantoverview/ApplicantCard"; import LoadingPage from "../../components/LoadingPage"; import { formatDateNorwegian } from "../../lib/utils/dateUtils"; import PageTitle from "../../components/PageTitle"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { fetchPeriodById } from "../../lib/api/periodApi"; -import { - createApplicant, - deleteApplicant, - fetchApplicantByPeriodAndId, -} from "../../lib/api/applicantApi"; +import { fetchApplicantByPeriodAndId } from "../../lib/api/applicantApi"; import ErrorPage from "../../components/ErrorPage"; +interface FetchedApplicationData { + exists: boolean; + application: applicantType; +} + const Application: NextPage = () => { - const queryClient = useQueryClient(); const { data: session } = useSession(); const router = useRouter(); const periodId = router.query["period-id"] as string; const applicantId = session?.user?.owId; + const [hasAlreadySubmitted, setHasAlreadySubmitted] = useState(true); + const [periodExists, setPeriodExists] = useState(false); + const [activeTab, setActiveTab] = useState(0); const [applicationData, setApplicationData] = useState< DeepPartial @@ -65,43 +68,18 @@ const Application: NextPage = () => { data: applicantData, isError: applicantIsError, isLoading: applicantIsLoading, + refetch: refetchApplicant, } = useQuery({ - queryKey: ["applicant", periodId, applicantId], + queryKey: ["applicants", periodId, applicantId], queryFn: fetchApplicantByPeriodAndId, }); - const createApplicantMutation = useMutation({ - mutationFn: createApplicant, - onSuccess: () => { - queryClient.setQueryData(["applicant", periodId, applicantId], { - applicant: applicationData, - exists: true, - }); - toast.success("Søknad sendt inn"); - }, - onError: (error) => { - if (error.message === "409 Application already exists for this period") { - toast.error("Du har allerede søkt for denne perioden"); - } else { - toast.error("Det skjedde en feil, vennligst prøv igjen"); - } - }, - }); - - const deleteApplicantMutation = useMutation({ - mutationFn: deleteApplicant, - onSuccess: () => { - queryClient.setQueryData(["applicant", periodId, applicantId], null); - toast.success("Søknad trukket tilbake"); - setActiveTab(0); - }, - onError: () => toast.error("Det skjedde en feil, vennligst prøv igjen"), - }); - useEffect(() => { - if (!periodData || !periodData.period) return; + if (!periodData) return; + if (!periodData.period) return; setPeriod(periodData.period); + setPeriodExists(periodData.exists); const currentDate = new Date().toISOString(); if ( @@ -111,10 +89,46 @@ const Application: NextPage = () => { } }, [periodData]); + useEffect(() => { + setHasAlreadySubmitted(applicantData?.exists); + }, [applicantData]); + const handleSubmitApplication = async () => { if (!validateApplication(applicationData)) return; - applicationData.periodId = periodId as string; - createApplicantMutation.mutate(applicationData as applicantType); + + try { + applicationData.periodId = periodId as string; + const response = await fetch("/api/applicants", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(applicationData), + }); + + const responseData = await response.json(); + + if (response.ok) { + toast.success("Søknad sendt inn"); + setHasAlreadySubmitted(true); + refetchApplicant(); + } else { + if ( + responseData.error === + "409 Application already exists for this period" + ) { + toast.error("Du har allerede søkt for denne perioden"); + } else { + throw new Error(`Error creating applicant: ${response.statusText}`); + } + } + } catch (error) { + if (error instanceof Error) { + toast.error(error.message); + } else { + toast.error("Det skjedde en feil, vennligst prøv igjen"); + } + } }; const handleDeleteApplication = async () => { @@ -125,19 +139,29 @@ const Application: NextPage = () => { if (!confirm("Er du sikker på at du vil trekke tilbake søknaden?")) return; - deleteApplicantMutation.mutate({ periodId, owId: session?.user?.owId }); + try { + const response = await fetch( + `/api/applicants/${periodId}/${session.user.owId}`, + { + method: "DELETE", + } + ); + + if (!response.ok) { + throw new Error("Failed to delete the application"); + } + + toast.success("Søknad trukket tilbake"); + setHasAlreadySubmitted(false); + } catch (error) { + toast.error("Det skjedde en feil, vennligst prøv igjen"); + } }; - if ( - periodIsLoading || - applicantIsLoading || - createApplicantMutation.isPending || - deleteApplicantMutation.isPending - ) - return ; + if (periodIsLoading || applicantIsLoading) return ; if (periodIsError || applicantIsError) return ; - if (!periodData?.exists) + if (!periodExists) { return (

@@ -145,8 +169,9 @@ const Application: NextPage = () => {

); + } - if (applicantData?.exists) + if (hasAlreadySubmitted) { return (
@@ -178,6 +203,7 @@ const Application: NextPage = () => { )}
); + } return (
diff --git a/pages/apply.tsx b/pages/apply.tsx index d6b4ed61..784e71c7 100644 --- a/pages/apply.tsx +++ b/pages/apply.tsx @@ -4,7 +4,6 @@ import PeriodCard from "../components/PeriodCard"; import { fetchPeriods } from "../lib/api/periodApi"; import { useQuery } from "@tanstack/react-query"; import ErrorPage from "../components/ErrorPage"; -import Link from "next/link"; import { PeriodSkeletonPage } from "../components/PeriodSkeleton"; const Apply = () => { @@ -45,28 +44,29 @@ const Apply = () => {

Ingen åpne opptak for øyeblikket

Opptak til{" "} - - - komiteene - - {" "} + + komiteene + {" "} skjer vanligvis i august etter fadderuka. Noen komiteer har - vanligvis suppleringsopptak i februar. -
-
- Følg med på{" "} - - - online.ntnu.no - - {" "} + vanligvis suppleringsopptak i februar.{

}

Følg + med på{" "} + + online.ntnu.no + {" "} eller på vår{" "} - - - Facebook-gruppe - - {" "} - for kunngjøringer! + + Facebook + {" "} + side for kunngjøringer!

) : ( diff --git a/pages/committees.tsx b/pages/committees.tsx index ce28e9e1..e472303d 100644 --- a/pages/committees.tsx +++ b/pages/committees.tsx @@ -7,7 +7,7 @@ import { fetchOwCommittees } from "../lib/api/committees"; import ErrorPage from "../components/ErrorPage"; import { fetchPeriods } from "../lib/api/periodApi"; -const excludedCommittees = ["Faddere", "Output"]; +const excludedCommittees = ["Faddere"]; const Committees = () => { const [committees, setCommittees] = useState([]); From f86e32d455d19b36e6991e483f0c341fa8d1f1a1 Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 31 Jul 2024 17:54:28 +0200 Subject: [PATCH 047/169] changed bankkom type from english to norwegian --- components/applicantoverview/ApplicantCard.tsx | 8 +++----- .../applicantoverview/ApplicantsOverview.tsx | 9 ++++----- components/form/ApplicationForm.tsx | 14 +++++++------- lib/types/types.ts | 2 +- lib/utils/toString.ts | 13 ------------- lib/utils/validators.ts | 2 +- pages/api/applicants/index.ts | 4 ++-- 7 files changed, 18 insertions(+), 34 deletions(-) diff --git a/components/applicantoverview/ApplicantCard.tsx b/components/applicantoverview/ApplicantCard.tsx index b77f73c9..8787106e 100644 --- a/components/applicantoverview/ApplicantCard.tsx +++ b/components/applicantoverview/ApplicantCard.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { applicantType } from "../../lib/types/types"; import { ChevronDownIcon } from "@heroicons/react/24/outline"; -import { changeDisplayName, getBankomValue } from "../../lib/utils/toString"; +import { changeDisplayName } from "../../lib/utils/toString"; interface Props { applicant: applicantType | undefined; @@ -71,10 +71,8 @@ const ApplicantCard = ({ applicant, includePreferences }: Props) => {

)} -

Om:

-

- Ønsker å være økonomiansvarlig: {getBankomValue(applicant?.bankom)} -

+

Om:

+

Ønsker å være økonomiansvarlig: {applicant?.bankom}

{applicant?.about}

diff --git a/components/applicantoverview/ApplicantsOverview.tsx b/components/applicantoverview/ApplicantsOverview.tsx index 889d48ab..a3e369db 100644 --- a/components/applicantoverview/ApplicantsOverview.tsx +++ b/components/applicantoverview/ApplicantsOverview.tsx @@ -14,7 +14,6 @@ import { fetchApplicantsByPeriodIdAndCommittee, } from "../../lib/api/applicantApi"; import ErrorPage from "../ErrorPage"; -import { getBankomValue } from "../../lib/utils/toString"; import ApplicantCard from "./ApplicantCard"; interface Props { @@ -52,7 +51,7 @@ const ApplicantsOverview = ({ const [applicants, setApplicants] = useState([]); const years: string[] = ["1", "2", "3", "4", "5"]; - const bankomOptions: bankomOptionsType[] = ["yes", "no", "maybe"]; + const bankomOptions: bankomOptionsType[] = ["ja", "nei", "kanskje"]; const { data: applicantsData, @@ -224,9 +223,9 @@ const ApplicantsOverview = ({ } > - {bankomOptions.map((bankom) => ( - ))} diff --git a/components/form/ApplicationForm.tsx b/components/form/ApplicationForm.tsx index 59a2f107..f101e8d1 100644 --- a/components/form/ApplicationForm.tsx +++ b/components/form/ApplicationForm.tsx @@ -35,9 +35,9 @@ export const ApplicationForm = (props: Props) => { const addOptionalCommittee = (committee: string, value: string) => { let updatedCommittees = [...selectedOptionalCommittees]; - if (value === "yes" && !updatedCommittees.includes(committee)) { + if (value === "ja" && !updatedCommittees.includes(committee)) { updatedCommittees.push(committee); - } else if (value === "no" && updatedCommittees.includes(committee)) { + } else if (value === "nei" && updatedCommittees.includes(committee)) { updatedCommittees = updatedCommittees.filter( (item) => item !== committee ); @@ -182,9 +182,9 @@ export const ApplicationForm = (props: Props) => { {
{ - switch (bankom) { - case "yes": - return "Ja"; - case "maybe": - return "Kanskje"; - case "no": - return "Nei"; - default: - return "Ikke valgt"; - } -}; - export const changeDisplayName = (committee: string) => { if (committee.toLowerCase() === "kjelleren") { return "Realfagskjelleren"; diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts index 6bd0302c..5f0b4a61 100644 --- a/lib/utils/validators.ts +++ b/lib/utils/validators.ts @@ -23,7 +23,7 @@ export const isApplicantType = ( typeof grade === "string" && typeof about === "string" && typeof bankom === "string" && - (bankom === "yes" || bankom === "no" || bankom === "maybe") && + (bankom === "ja" || bankom === "nei" || bankom === "kanskje") && typeof applicantPeriodId === "string" && applicantPeriodId === periodId && date instanceof Date; diff --git a/pages/api/applicants/index.ts b/pages/api/applicants/index.ts index 85b2a9af..72ec6460 100644 --- a/pages/api/applicants/index.ts +++ b/pages/api/applicants/index.ts @@ -75,9 +75,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { secondChoice: "Tom", thirdChoice: "Tom", bankom: - applicant.bankom == "yes" + applicant.bankom == "ja" ? "Ja" - : applicant.bankom == "no" + : applicant.bankom == "nei" ? "Nei" : "Kanskje", optionalCommittees: optionalCommitteesString, From e3a494b2ba45a10351efc93edc530d1afa5c1f74 Mon Sep 17 00:00:00 2001 From: fredrir Date: Thu, 1 Aug 2024 14:15:36 +0200 Subject: [PATCH 048/169] can now apply for only a optionalCommittee --- components/committee/Schedule.tsx | 1 + lib/utils/validateApplication.ts | 37 +++++++++++++++++++------------ lib/utils/validators.ts | 27 +++++++++++----------- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/components/committee/Schedule.tsx b/components/committee/Schedule.tsx index 786057e8..82778fec 100644 --- a/components/committee/Schedule.tsx +++ b/components/committee/Schedule.tsx @@ -35,6 +35,7 @@ export default function Schedule({ const getDatesWithinPeriod = ( periodTime: any ): { [date: string]: string } => { + if (!periodTime) return {}; const startDate = new Date(periodTime.start); startDate.setHours(startDate.getHours() + 2); const endDate = new Date(periodTime.end); diff --git a/lib/utils/validateApplication.ts b/lib/utils/validateApplication.ts index 514ba6bf..55f2bcbb 100644 --- a/lib/utils/validateApplication.ts +++ b/lib/utils/validateApplication.ts @@ -2,14 +2,27 @@ import toast from "react-hot-toast"; import validator from "validator"; export const validateApplication = (applicationData: any) => { + const { + name, + email, + phone, + grade, + about, + bankom, + selectedTimes, + optionalCommittees, + } = applicationData; + + const { first, second, third } = applicationData.preferences; + // Check if email is valid - if (!validator.isEmail(applicationData.email)) { + if (!validator.isEmail(email)) { toast.error("Fyll inn en gyldig e-postadresse"); return false; } // Check if ntnu email is used - if (applicationData.email.includes("ntnu.no")) { + if (email.includes("ntnu.no")) { toast.error( "Vi har problemer med å sende e-post til NTNU e-poster. Vennligst bruk en annen e-postadresse." ); @@ -17,35 +30,31 @@ export const validateApplication = (applicationData: any) => { } // Check if phone number is valid - if (!validator.isMobilePhone(applicationData.phone, "nb-NO")) { + if (!validator.isMobilePhone(phone, "nb-NO")) { toast.error("Fyll inn et gyldig mobilnummer"); return false; } // Check if grade is valid - if (applicationData.grade == 0) { + if (grade == 0) { toast.error("Velg et trinn"); return false; } // Check if about section is filled - if (applicationData.about === "") { + if (about === "") { toast.error("Skriv litt om deg selv"); return false; } // Check if at least one preference is selected - if ( - !applicationData.preferences.first && - !applicationData.preferences.second && - !applicationData.preferences.third - ) { + if (!first && !second && !third && optionalCommittees.length === 0) { toast.error("Velg minst én komité"); return false; } // Check for duplicate committee preferences - const { first, second, third } = applicationData.preferences; + if ( (first && second && first === second) || (first && third && first === third) || @@ -56,18 +65,18 @@ export const validateApplication = (applicationData: any) => { } // Check if Bankom interest is specified - if (applicationData.bankom === undefined) { + if (bankom === undefined) { toast.error("Velg om du er interessert i Bankom"); return false; } // Validate selected times - if (applicationData.selectedTimes.length === 0) { + if (selectedTimes.length === 0) { toast.error("Velg minst én tilgjengelig tid"); return false; } - for (const time of applicationData.selectedTimes) { + for (const time of selectedTimes) { const startTime = new Date(time.start); const endTime = new Date(time.end); if (isNaN(startTime.getTime()) || isNaN(endTime.getTime())) { diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts index 6bd0302c..97a24077 100644 --- a/lib/utils/validators.ts +++ b/lib/utils/validators.ts @@ -34,20 +34,22 @@ export const isApplicantType = ( ); const { first, second, third } = applicant.preferences as preferencesType; + const applicantOptionalCommittees = applicant.optionalCommittees; const hasPreferencesFields = - (applicant.preferences as preferencesType) && - typeof first === "string" && - (typeof second === "string" || second === "") && - (typeof third === "string" || third === "") && - // Ensure that non-empty preferences are unique - first !== second && - (first === "" || first !== third) && - (second === "" || second !== third) && - // Ensure preferences are in period committees or empty - periodCommittees.includes(first) && - (second === "" || periodCommittees.includes(second)) && - (third === "" || periodCommittees.includes(third)); + ((applicant.preferences as preferencesType) && typeof first === "string") || + (first === "" && + applicantOptionalCommittees.length !== 0 && + (typeof second === "string" || second === "") && + (typeof third === "string" || third === "") && + // Ensure that non-empty preferences are unique + first !== second && + (first === "" || first !== third) && + (second === "" || second !== third) && + // Ensure preferences are in period committees or empty + periodCommittees.includes(first) && + (second === "" || periodCommittees.includes(second)) && + (third === "" || periodCommittees.includes(third))); // Check that the selectedTimes array is valid @@ -70,7 +72,6 @@ export const isApplicantType = ( const periodOptionalCommittees = period.optionalCommittees.map((committee) => committee.toLowerCase() ); - const applicantOptionalCommittees = applicant.optionalCommittees; const hasOptionalFields = applicantOptionalCommittees && From 74e3c956ff664e156bcbbc997c1bb1effd15a279 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 2 Aug 2024 18:07:05 +0200 Subject: [PATCH 049/169] removed unnesecary console.logs --- pages/_app.tsx | 1 - pages/api/applicants/index.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/pages/_app.tsx b/pages/_app.tsx index 4d16ef6f..5e31eb1b 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -27,7 +27,6 @@ const SessionHandler: React.FC<{ children: React.ReactNode }> = ({ //Tihi useEffect(() => { console.log(Signature); - console.log("jo tester"); }, []); useEffect(() => { diff --git a/pages/api/applicants/index.ts b/pages/api/applicants/index.ts index 85b2a9af..34dfb0d4 100644 --- a/pages/api/applicants/index.ts +++ b/pages/api/applicants/index.ts @@ -105,8 +105,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { subject: "Vi har mottatt din søknad!", htmlContent: generateApplicantEmail(emailData), }); - - console.log("Email sent to: ", emailData.emails); } catch (error) { console.error("Error sending email: ", error); throw error; From 3fc5f08add0d540e087fd6c7502bfa25acf37e04 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 11 Aug 2024 14:58:08 +0200 Subject: [PATCH 050/169] add interviewsplanned / applications --- .../committee/CommitteeInterviewTimes.tsx | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 5dc23a02..9db4056a 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -11,6 +11,8 @@ import Button from "../Button"; import ImportantNote from "../ImportantNote"; import useUnsavedChangesWarning from "../../lib/utils/unSavedChangesWarning"; import { SimpleTitle } from "../Typography"; +import { useQuery } from "@tanstack/react-query"; +import { fetchApplicantsByPeriodIdAndCommittee } from "../../lib/api/applicantApi"; interface Interview { title: string; @@ -55,6 +57,8 @@ const CommitteeInterviewTimes = ({ const { unsavedChanges, setUnsavedChanges } = useUnsavedChangesWarning(); + const [numberOfApplications, setNumberOfApplications] = useState(0); + useEffect(() => { if (period) { setVisibleRange({ @@ -64,6 +68,21 @@ const CommitteeInterviewTimes = ({ } }, [period]); + const { + data: applicantsData, + isError: applicantsIsError, + isLoading: applicantsIsLoading, + } = useQuery({ + queryKey: ["applicants", period?._id, committee], + queryFn: fetchApplicantsByPeriodIdAndCommittee, + }); + + useEffect(() => { + if (applicantsData) { + setNumberOfApplications(applicantsData.applicants.length); + } + }, [applicantsData]); + useEffect(() => { if (committee && committeeInterviewTimes) { const cleanString = (input: string) => @@ -166,6 +185,14 @@ const CommitteeInterviewTimes = ({ return; } + if (interviewsPlanned < numberOfApplications) { + console.log(interviewsPlanned, numberOfApplications); + toast.error( + "Du har valgt færre tider enn antall søkere. Vennligst legg til flere tider." + ); + return; + } + const dataToSend = { periodId: period!._id, period_name: period!.name, @@ -411,7 +438,7 @@ const CommitteeInterviewTimes = ({
)} -

{`${interviewsPlanned} intervjuer planlagt`}

+

{`${interviewsPlanned} / ${numberOfApplications} intervjuer planlagt`}

Date: Sun, 11 Aug 2024 15:16:01 +0200 Subject: [PATCH 051/169] update collection names --- algorithm/bridge/fetch_applicants_and_committees.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 6117a422..c666bf5f 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -19,9 +19,10 @@ def main(): application_end = datetime.fromisoformat(period["applicationPeriod"]["end"].replace("Z", "+00:00")) now = datetime.now(timezone.utc) + #or period["name"] == "Juli Opptak" - if (application_end > now and period["hasSentInterviewTimes"] == False and interview_end < now): + if (application_end > now and period["hasSentInterviewTimes"] == False and interview_end < now) or period["name"] == "FAKE TEST OPPTAK!": applicants = fetch_applicants(periodId) committee_times = fetch_committee_times(periodId) @@ -77,7 +78,7 @@ def connect_to_db(collection_name): return collection, client def fetch_periods(): - collection, client = connect_to_db("period") + collection, client = connect_to_db("periods") periods = collection.find() @@ -88,7 +89,7 @@ def fetch_periods(): return periods def fetch_applicants(periodId): - collection, client = connect_to_db("applicant") + collection, client = connect_to_db("applications") applicants = collection.find({"periodId": periodId}) @@ -99,7 +100,7 @@ def fetch_applicants(periodId): return applicants def fetch_committee_times(periodId): - collection, client = connect_to_db("committee") + collection, client = connect_to_db("committees") committee_times = collection.find({"periodId": periodId}) From a891a8a408687054342dcabab69dd5004b6a8b04 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 11 Aug 2024 15:18:19 +0200 Subject: [PATCH 052/169] . --- .../committee/CommitteeInterviewTimes.tsx | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index cd5117ea..5dc23a02 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -9,6 +9,7 @@ import toast from "react-hot-toast"; import NotFound from "../../pages/404"; import Button from "../Button"; import ImportantNote from "../ImportantNote"; +import useUnsavedChangesWarning from "../../lib/utils/unSavedChangesWarning"; import { SimpleTitle } from "../Typography"; interface Interview { @@ -37,6 +38,7 @@ const CommitteeInterviewTimes = ({ const [visibleRange, setVisibleRange] = useState({ start: "", end: "" }); const [selectedTimeslot, setSelectedTimeslot] = useState("15"); + const [interviewsPlanned, setInterviewsPlanned] = useState(0); const [calendarEvents, setCalendarEvents] = useState([]); const [hasAlreadySubmitted, setHasAlreadySubmitted] = @@ -49,6 +51,10 @@ const CommitteeInterviewTimes = ({ const inputRef = useRef(null); const calendarRef = useRef(null); + const [deadLineHasPassed, setDeadLineHasPassed] = useState(false); + + const { unsavedChanges, setUnsavedChanges } = useUnsavedChangesWarning(); + useEffect(() => { if (period) { setVisibleRange({ @@ -113,9 +119,16 @@ const CommitteeInterviewTimes = ({ } }, [isModalOpen]); + useEffect(() => { + if (calendarEvents.length > 0) { + calculateInterviewsPlanned(); + } + }, [calendarEvents, selectedTimeslot]); + const handleDateSelect = (selectionInfo: any) => { setCurrentSelection(selectionInfo); setIsModalOpen(true); + setUnsavedChanges(true); }; const handleRoomSubmit = () => { @@ -132,7 +145,7 @@ const CommitteeInterviewTimes = ({ const calendarApi = currentSelection.view.calendar; calendarApi.addEvent(event); - calendarApi.render(); // Force the calendar to re-render + calendarApi.render(); addCell([ roomInput, @@ -142,7 +155,7 @@ const CommitteeInterviewTimes = ({ setRoomInput(""); setIsModalOpen(false); - setCalendarEvents((prevEvents) => [...prevEvents, event]); // Trigger re-render + setCalendarEvents((prevEvents) => [...prevEvents, event]); }; const submit = async (e: BaseSyntheticEvent) => { @@ -178,6 +191,7 @@ const CommitteeInterviewTimes = ({ const result = await response.json(); toast.success("Tidene er sendt inn!"); setHasAlreadySubmitted(true); + setUnsavedChanges(false); } catch (error) { toast.error("Kunne ikke sende inn!"); } @@ -190,6 +204,7 @@ const CommitteeInterviewTimes = ({ ) ); event.remove(); + setUnsavedChanges(true); }; const addCell = (cell: string[]) => { @@ -197,10 +212,12 @@ const CommitteeInterviewTimes = ({ ...markedCells, { title: cell[0], start: cell[1], end: cell[2] }, ]); + setUnsavedChanges(true); }; const updateInterviewInterval = (e: BaseSyntheticEvent) => { setInterviewInterval(parseInt(e.target.value)); + setUnsavedChanges(true); }; const renderEventContent = (eventContent: any) => { @@ -253,6 +270,7 @@ const CommitteeInterviewTimes = ({ const handleTimeslotSelection = (e: React.ChangeEvent) => { setSelectedTimeslot(e.target.value); + setUnsavedChanges(true); }; const deleteSubmission = async (e: BaseSyntheticEvent) => { @@ -274,6 +292,7 @@ const CommitteeInterviewTimes = ({ setHasAlreadySubmitted(false); setCalendarEvents([]); + setUnsavedChanges(false); } catch (error: any) { console.error("Error deleting submission:", error); toast.error("Klarte ikke å slette innsendingen"); @@ -291,12 +310,16 @@ const CommitteeInterviewTimes = ({ }, [period]); const getSubmissionDeadline = (): string => { - const deadlineIso = period!.interviewPeriod.start; + const deadlineIso = period!.applicationPeriod.end; - if (deadlineIso != null) { + if (deadlineIso != null && !deadLineHasPassed) { const deadlineDate = new Date(deadlineIso); const now = new Date(); + if (now > deadlineDate) { + setDeadLineHasPassed(true); + } + let delta = Math.floor((deadlineDate.getTime() - now.getTime()) / 1000); const days = Math.floor(delta / 86400); @@ -322,6 +345,20 @@ const CommitteeInterviewTimes = ({ return ""; }; + const calculateInterviewsPlanned = () => { + const totalMinutes = calendarEvents.reduce((acc, event) => { + const start = new Date(event.start); + const end = new Date(event.end); + const duration = (end.getTime() - start.getTime()) / 1000 / 60; + return acc + duration; + }, 0); + + const plannedInterviews = Math.floor( + totalMinutes / parseInt(selectedTimeslot) + ); + setInterviewsPlanned(plannedInterviews); + }; + if (!session || !session.user?.isCommittee) { return ; } @@ -374,6 +411,7 @@ const CommitteeInterviewTimes = ({
)} +

{`${interviewsPlanned} intervjuer planlagt`}

Date: Sun, 11 Aug 2024 15:31:52 +0200 Subject: [PATCH 053/169] update formatting --- lib/types/types.ts | 1 + lib/utils/dateUtils.ts | 17 ++++ lib/utils/sendInterviewTimes/formatAndSend.ts | 96 ++++--------------- .../formatInterviewEmail.ts | 67 +++++++++++++ .../sendInterviewTimes/formatInterviewSMS.ts | 27 ++++++ 5 files changed, 130 insertions(+), 78 deletions(-) create mode 100644 lib/utils/sendInterviewTimes/formatInterviewEmail.ts create mode 100644 lib/utils/sendInterviewTimes/formatInterviewSMS.ts diff --git a/lib/types/types.ts b/lib/types/types.ts index 0e16e49b..5f0663d9 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -130,6 +130,7 @@ export type emailCommitteeInterviewType = { committeeEmail: string; applicants: { applicantName: string; + applicantPhone: string; applicantEmail: string; interviewTime: { start: string; diff --git a/lib/utils/dateUtils.ts b/lib/utils/dateUtils.ts index c2ddfe7b..a116bed5 100644 --- a/lib/utils/dateUtils.ts +++ b/lib/utils/dateUtils.ts @@ -22,6 +22,23 @@ export const formatDateHours = (inputDate: undefined | Date) => { return `${formatDateNorwegian(inputDate)}, ${hours}:${minutes}`; // - ${hours}:${minutes} }; +export const formatDateForSMS = ( + start: undefined | Date, + end: undefined | Date +) => { + const startDate = new Date(start || ""); + const endDate = new Date(end || ""); + + const startHour = startDate.getHours().toString().padStart(2, "0"); + const startMinute = startDate.getMinutes().toString().padStart(2, "0"); + const endHour = endDate.getHours().toString().padStart(2, "0"); + const endMinute = endDate.getMinutes().toString().padStart(2, "0"); + + return `${formatDateNorwegian( + startDate + )}, ${startHour}:${startMinute} til ${endHour}:${endMinute}`; // - ${hours}:${minutes} +}; + export const formatDateNorwegian = (inputDate?: Date): string => { const date = new Date(inputDate || ""); diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 8502767c..6d9cf1f3 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -1,12 +1,16 @@ -import { SESClient } from "@aws-sdk/client-ses"; import { emailCommitteeInterviewType, emailApplicantInterviewType, } from "../../types/types"; import { changeDisplayName } from "../toString"; import { formatDateHours } from "../dateUtils"; -import sendEmail from "../sendEmail"; +import sendEmail from "../../email/sendEmail"; import sendSMS from "./sendSMS"; +import { + formatApplicantInterviewEmail, + formatCommitteeInterviewEmail, +} from "./formatInterviewEmail"; +import { formatInterviewSMS } from "./formatInterviewSMS"; interface sendInterviewTimesProps { committeesToEmail: emailCommitteeInterviewType[]; @@ -18,62 +22,24 @@ export const formatAndSendEmails = async ({ applicantsToEmail, }: sendInterviewTimesProps) => { try { - const sesClient = new SESClient({ region: "eu-north-1" }); - // Send email to each applicant for (const applicant of applicantsToEmail) { const typedApplicant: emailApplicantInterviewType = applicant; const applicantEmail = [typedApplicant.applicantEmail]; const subject = `Hei, ${typedApplicant.applicantName}, her er dine intervjutider:`; - let emailBody = `

Hei ${typedApplicant.applicantName},

Her er dine intervjutider for ${typedApplicant.period_name}:


    `; - let phoneBody = `Hei ${typedApplicant.applicantName}, her er dine intervjutider for ${typedApplicant.period_name}: \n \n`; + const emailBody = formatApplicantInterviewEmail(typedApplicant); + const phoneBody = formatInterviewSMS(typedApplicant); - typedApplicant.committees.sort((a, b) => { - return ( - new Date(a.interviewTime.start).getTime() - - new Date(b.interviewTime.start).getTime() - ); + await sendEmail({ + toEmails: applicantEmail, + subject: subject, + htmlContent: emailBody, }); - typedApplicant.committees.forEach((committee) => { - emailBody += `
  • Komite: ${changeDisplayName( - committee.committeeName - )}
    `; - emailBody += `Start: ${formatDateHours( - new Date(committee.interviewTime.start) - )}
    `; - emailBody += `Slutt: ${formatDateHours( - new Date(committee.interviewTime.end) - )}
    `; - emailBody += `Rom: ${committee.interviewTime.room}

  • `; - - phoneBody += `Komite: ${changeDisplayName(committee.committeeName)} \n`; - phoneBody += `Start: ${formatDateHours( - new Date(committee.interviewTime.start) - )}\n`; - phoneBody += `Slutt: ${formatDateHours( - new Date(committee.interviewTime.end) - )}\n`; - phoneBody += `Rom: ${committee.interviewTime.room} \n \n`; - }); - - emailBody += `


Skjedd en feil? Ta kontakt med Appkom❤️

`; - phoneBody += `Skjedd en feil? Ta kontakt med Appkom`; - - // await sendEmail({ - // sesClient: sesClient, - // fromEmail: "opptak@online.ntnu.no", - // toEmails: applicantEmail, - // subject: subject, - // htmlContent: emailBody, - // }); - let toPhoneNumber = "+47"; toPhoneNumber += typedApplicant.applicantPhone; - // sendSMS(toPhoneNumber, phoneBody); - - console.log(applicantEmail[0], "\n", subject, "\n", emailBody); + sendSMS(toPhoneNumber, phoneBody); } // Send email to each committee @@ -84,39 +50,13 @@ export const formatAndSendEmails = async ({ typedCommittee.committeeName )} sine intervjutider for ${typedCommittee.period_name}`; - let body = `

Hei ${changeDisplayName( - typedCommittee.committeeName - )},

Her er deres intervjutider:

    `; - - typedCommittee.applicants.sort((a, b) => { - return ( - new Date(a.interviewTime.start).getTime() - - new Date(b.interviewTime.start).getTime() - ); - }); + const emailBody = formatCommitteeInterviewEmail(typedCommittee); - typedCommittee.applicants.forEach((applicant) => { - body += `
  • Navn: ${applicant.applicantName}
    `; - body += `Start: ${formatDateHours( - new Date(applicant.interviewTime.start) - )}
    `; - body += `Slutt: ${formatDateHours( - new Date(applicant.interviewTime.end) - )}
    `; - body += `Rom: ${applicant.interviewTime.room}

  • `; + await sendEmail({ + toEmails: committeeEmail, + subject: subject, + htmlContent: emailBody, }); - - body += `


Skjedd en feil? Ta kontakt med Appkom❤️

`; - - // await sendEmail({ - // sesClient: sesClient, - // fromEmail: "opptak@online.ntnu.no", - // toEmails: committeeEmail, - // subject: subject, - // htmlContent: body, - // }); - - console.log(committeeEmail[0], "\n", subject, "\n", body); } } catch (error) { return { error: "Failed to send out interview times" }; diff --git a/lib/utils/sendInterviewTimes/formatInterviewEmail.ts b/lib/utils/sendInterviewTimes/formatInterviewEmail.ts new file mode 100644 index 00000000..5acbf6e4 --- /dev/null +++ b/lib/utils/sendInterviewTimes/formatInterviewEmail.ts @@ -0,0 +1,67 @@ +import { + emailApplicantInterviewType, + emailCommitteeInterviewType, +} from "../../types/types"; +import { formatDateHours } from "../dateUtils"; +import { changeDisplayName } from "../toString"; + +export const formatApplicantInterviewEmail = ( + applicant: emailApplicantInterviewType +) => { + let emailBody = `

Hei ${applicant.applicantName},

Her er dine intervjutider for ${applicant.period_name}:


    `; + + applicant.committees.sort((a, b) => { + return ( + new Date(a.interviewTime.start).getTime() - + new Date(b.interviewTime.start).getTime() + ); + }); + + applicant.committees.forEach((committee) => { + emailBody += `
  • Komite: ${changeDisplayName( + committee.committeeName + )}
    `; + emailBody += `Start: ${formatDateHours( + new Date(committee.interviewTime.start) + )}
    `; + emailBody += `Slutt: ${formatDateHours( + new Date(committee.interviewTime.end) + )}
    `; + emailBody += `Rom: ${committee.interviewTime.room}

  • `; + }); + + emailBody += `


Skjedd en feil? Ta kontakt med Appkom❤️

`; + + return emailBody; +}; + +export const formatCommitteeInterviewEmail = ( + committee: emailCommitteeInterviewType +) => { + let emailBody = `

Hei ${changeDisplayName( + committee.committeeName + )},

Her er deres intervjutider:

    `; + + committee.applicants.sort((a, b) => { + return ( + new Date(a.interviewTime.start).getTime() - + new Date(b.interviewTime.start).getTime() + ); + }); + + committee.applicants.forEach((applicant) => { + emailBody += `
  • Navn: ${applicant.applicantName}
    `; + emailBody += `Telefon: ${applicant.applicantPhone}
    `; + emailBody += `Start: ${formatDateHours( + new Date(applicant.interviewTime.start) + )}
    `; + emailBody += `Slutt: ${formatDateHours( + new Date(applicant.interviewTime.end) + )}
    `; + emailBody += `Rom: ${applicant.interviewTime.room}

  • `; + }); + + emailBody += `


Skjedd en feil? Ta kontakt med Appkom❤️

`; + + return emailBody; +}; diff --git a/lib/utils/sendInterviewTimes/formatInterviewSMS.ts b/lib/utils/sendInterviewTimes/formatInterviewSMS.ts new file mode 100644 index 00000000..5e0f2181 --- /dev/null +++ b/lib/utils/sendInterviewTimes/formatInterviewSMS.ts @@ -0,0 +1,27 @@ +import { emailApplicantInterviewType } from "../../types/types"; +import { formatDateHours } from "../dateUtils"; +import { changeDisplayName } from "../toString"; + +export const formatInterviewSMS = (applicant: emailApplicantInterviewType) => { + let phoneBody = `Hei ${applicant.applicantName}, her er dine intervjutider for ${applicant.period_name}: \n \n`; + + applicant.committees.sort((a, b) => { + return ( + new Date(a.interviewTime.start).getTime() - + new Date(b.interviewTime.start).getTime() + ); + }); + + applicant.committees.forEach((committee) => { + phoneBody += `Komite: ${changeDisplayName(committee.committeeName)} \n`; + phoneBody += `Tid: ${formatDateHours( + new Date(committee.interviewTime.start) + )}\n`; + + phoneBody += `Rom: ${committee.interviewTime.room} \n \n`; + }); + + phoneBody += `Skjedd en feil? Ta kontakt med appkom@online.ntnu.no`; + + return phoneBody; +}; From 9ebeca555d187182e234af709245e30e4a01dd50 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 11 Aug 2024 15:33:26 +0200 Subject: [PATCH 054/169] add mark period sendt --- lib/mongo/periods.ts | 20 +++++ .../sendInterviewTimes/sendInterviewTimes.ts | 73 +++++++++---------- 2 files changed, 54 insertions(+), 39 deletions(-) diff --git a/lib/mongo/periods.ts b/lib/mongo/periods.ts index 763212bc..dccc87d8 100644 --- a/lib/mongo/periods.ts +++ b/lib/mongo/periods.ts @@ -122,3 +122,23 @@ export const deletePeriodById = async (periodId: string | ObjectId) => { return { error: "Failed to delete period" }; } }; + +export const markInterviewsSentByPeriodId = async (periodId: string) => { + try { + if (!periods) await init(); + + const objectPeriodId = new ObjectId(periodId); + + const result = await periods.findOneAndUpdate( + { _id: objectPeriodId }, + { + $set: { hasSentInterviewTimes: true }, + } + ); + { + return { result: result }; + } + } catch (error) { + return { error: "Failed to update hasSentInterviewTimes" }; + } +}; diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index 2c7e53f3..9499416f 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -1,5 +1,3 @@ -import { SESClient } from "@aws-sdk/client-ses"; -import sendEmail from "../sendEmail"; import { committeeEmails, committeeInterviewType, @@ -11,56 +9,52 @@ import { import { fetchCommitteeEmails } from "./fetchFunctions"; import { formatAndSendEmails } from "./formatAndSend"; -import { getPeriodById } from "../../mongo/periods"; +import { getPeriods, markInterviewsSentByPeriodId } from "../../mongo/periods"; import { getCommitteesByPeriod } from "../../mongo/committees"; import { getInterviewsByPeriod } from "../../mongo/interviews"; -interface Props { - periodId: string; -} - -export const sendOutInterviewTimes = async ({ periodId }: Props) => { +export const sendOutInterviewTimes = async () => { try { - const periodData = await getPeriodById(periodId); + const { periods } = await getPeriods(); - if (!periodData.exists || !periodData.period) { + if (!periods) { return { error: "Failed to find period" }; } - const period: periodType = periodData.period; - - const commmitteeInterviewTimesData = await getCommitteesByPeriod(periodId); - - if (!commmitteeInterviewTimesData || commmitteeInterviewTimesData.error) { - return { error: "Failed to find committee interview times" }; - } - - const committeeInterviewTimes: committeeInterviewType[] = - commmitteeInterviewTimesData.result || []; - - const committeeEmails: committeeEmails[] = await fetchCommitteeEmails(); - const fetchedAlgorithmData = await getInterviewsByPeriod(periodId); + for (const period of periods) { + if ( + period.hasSentInterviewTimes === false && + period.applicationPeriod.end < new Date() + ) { + const periodId = String(period._id); + + const committeeInterviewTimesData = + await getCommitteesByPeriod(periodId); + if (!committeeInterviewTimesData || committeeInterviewTimesData.error) { + return { error: "Failed to find committee interview times" }; + } - const algorithmData: algorithmType[] = - fetchedAlgorithmData.interviews || []; + const committeeInterviewTimes = + committeeInterviewTimesData.result || []; + const committeeEmails = await fetchCommitteeEmails(); - const applicantsToEmail: emailApplicantInterviewType[] = formatApplicants( - algorithmData, - periodId, - period, - committeeEmails, - committeeInterviewTimes - ); + const fetchedAlgorithmData = await getInterviewsByPeriod(periodId); + const algorithmData = fetchedAlgorithmData.interviews || []; - const committeesToEmail: emailCommitteeInterviewType[] = - formatCommittees(applicantsToEmail); + const applicantsToEmail = formatApplicants( + algorithmData, + periodId, + period, + committeeEmails, + committeeInterviewTimes + ); - const dataToSend = { - committeesToEmail, - applicantsToEmail, - }; + const committeesToEmail = formatCommittees(applicantsToEmail); - formatAndSendEmails(dataToSend); + await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); + markInterviewsSentByPeriodId(periodId); + } + } } catch (error) { return { error: "Failed to send out interview times" }; } @@ -145,6 +139,7 @@ const formatCommittees = ( committeesToEmail[committee.committeeName].applicants.push({ applicantName: applicant.applicantName, + applicantPhone: applicant.applicantPhone, applicantEmail: applicant.applicantEmail, interviewTime: committee.interviewTime, }); From 61ba170e89cdf5417703ee45ae672f533212b08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Sun, 11 Aug 2024 15:52:02 +0200 Subject: [PATCH 055/169] fix: flyttet ut slot-validation, slik at den kun brukes ved innsending --- lib/utils/validateApplication.ts | 6 ------ pages/apply/[period-id].tsx | 7 +++++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/utils/validateApplication.ts b/lib/utils/validateApplication.ts index 514ba6bf..0a494ef1 100644 --- a/lib/utils/validateApplication.ts +++ b/lib/utils/validateApplication.ts @@ -61,12 +61,6 @@ export const validateApplication = (applicationData: any) => { return false; } - // Validate selected times - if (applicationData.selectedTimes.length === 0) { - toast.error("Velg minst én tilgjengelig tid"); - return false; - } - for (const time of applicationData.selectedTimes) { const startTime = new Date(time.start); const endTime = new Date(time.end); diff --git a/pages/apply/[period-id].tsx b/pages/apply/[period-id].tsx index ab8ce157..e978d3b3 100644 --- a/pages/apply/[period-id].tsx +++ b/pages/apply/[period-id].tsx @@ -113,6 +113,13 @@ const Application: NextPage = () => { const handleSubmitApplication = async () => { if (!validateApplication(applicationData)) return; + + // Validate selected times + if (applicationData.selectedTimes && applicationData.selectedTimes.length === 0) { + toast.error("Velg minst én tilgjengelig tid"); + return false; + } + applicationData.periodId = periodId as string; createApplicantMutation.mutate(applicationData as applicantType); }; From 35e2c131fe83836ec4b3eae969172a45a7422393 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 11 Aug 2024 17:17:55 +0200 Subject: [PATCH 056/169] update manual send interview times --- lib/api/applicantApi.ts | 35 ++++++++++ lib/mongo/applicants.ts | 21 ++++++ lib/types/types.ts | 1 + lib/utils/dateUtils.ts | 12 ++-- lib/utils/sendInterviewTimes/formatAndSend.ts | 24 +++++-- .../formatInterviewEmail.ts | 40 ++++++++---- .../sendInterviewTimes/formatInterviewSMS.ts | 13 +++- .../sendInterviewTimes/sendInterviewTimes.ts | 65 +++++++++++++++++-- pages/admin/[period-id]/index.tsx | 2 +- pages/api/interviews/[period-id].ts | 45 ------------- pages/api/periods/send-interview-times.ts | 35 ++++++++++ 11 files changed, 215 insertions(+), 78 deletions(-) delete mode 100644 pages/api/interviews/[period-id].ts create mode 100644 pages/api/periods/send-interview-times.ts diff --git a/lib/api/applicantApi.ts b/lib/api/applicantApi.ts index 7f72a51c..ac0ef8c5 100644 --- a/lib/api/applicantApi.ts +++ b/lib/api/applicantApi.ts @@ -1,4 +1,5 @@ import { QueryFunctionContext } from "@tanstack/react-query"; +import { applicantType } from "../types/types"; export const fetchApplicantByPeriodAndId = async ( context: QueryFunctionContext @@ -26,3 +27,37 @@ export const fetchApplicantsByPeriodIdAndCommittee = async ( (res) => res.json() ); }; + +export const createApplicant = async (applicant: applicantType) => { + const response = await fetch(`/api/applicants/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(applicant), + }); + + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || "Unknown error occurred"); + } + return data; +}; + +export const deleteApplicant = async ({ + periodId, + owId, +}: { + periodId: string; + owId: string; +}) => { + const response = await fetch(`/api/applicants/${periodId}/${owId}`, { + method: "DELETE", + }); + + if (!response.ok) { + throw new Error("Failed to delete the application"); + } + + return response; +}; diff --git a/lib/mongo/applicants.ts b/lib/mongo/applicants.ts index e3883136..5a214400 100644 --- a/lib/mongo/applicants.ts +++ b/lib/mongo/applicants.ts @@ -64,6 +64,27 @@ export const getApplicants = async () => { } }; +export const getApplicationById = async ( + id: string, + periodId: string | ObjectId +) => { + try { + if (!applicants) await init(); + + const objectId = new ObjectId(id); + + const result = await applicants.findOne({ + _id: objectId, + periodId: periodId, + }); + + return { application: result, exists: !!result }; + } catch (error) { + console.error(error); + return { error: "Failed to fetch application", exists: false }; + } +}; + export const getApplication = async ( id: string, periodId: string | ObjectId diff --git a/lib/types/types.ts b/lib/types/types.ts index 5f0663d9..8d2fb5d3 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -111,6 +111,7 @@ export type algorithmType = { applicantName: string; applicantEmail: string; applicantPhone: string; + applicantId: string; interviews: { start: string; end: string; diff --git a/lib/utils/dateUtils.ts b/lib/utils/dateUtils.ts index a116bed5..a2ec7b6d 100644 --- a/lib/utils/dateUtils.ts +++ b/lib/utils/dateUtils.ts @@ -13,13 +13,13 @@ export const formatDate = (inputDate: undefined | Date) => { export const formatDateHours = (inputDate: undefined | Date) => { const date = new Date(inputDate || ""); - const day = date.getDate().toString().padStart(2, "0"); - const month = (date.getMonth() + 1).toString().padStart(2, "0"); - const year = date.getFullYear(); - const hours = date.getHours().toString().padStart(2, "0"); - const minutes = date.getMinutes().toString().padStart(2, "0"); + const day = date.getUTCDate().toString().padStart(2, "0"); + const month = (date.getUTCMonth() + 1).toString().padStart(2, "0"); + const year = date.getUTCFullYear(); + const hours = date.getUTCHours().toString().padStart(2, "0"); + const minutes = date.getUTCMinutes().toString().padStart(2, "0"); - return `${formatDateNorwegian(inputDate)}, ${hours}:${minutes}`; // - ${hours}:${minutes} + return `${formatDateNorwegian(inputDate)}, ${hours}:${minutes}`; }; export const formatDateForSMS = ( diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 6d9cf1f3..6d173332 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -32,14 +32,21 @@ export const formatAndSendEmails = async ({ const phoneBody = formatInterviewSMS(typedApplicant); await sendEmail({ - toEmails: applicantEmail, + // toEmails: applicantEmail, + toEmails: [ + "fhansteen@gmail.com", + "julian.ottosen@online.ntnu.no", + "jorgen.galdal@online.ntnu.no", + ], subject: subject, htmlContent: emailBody, }); - let toPhoneNumber = "+47"; - toPhoneNumber += typedApplicant.applicantPhone; - sendSMS(toPhoneNumber, phoneBody); + // let toPhoneNumber = "+47"; + // toPhoneNumber += typedApplicant.applicantPhone; + // sendSMS(toPhoneNumber, phoneBody); + + console.log(emailBody); } // Send email to each committee @@ -53,10 +60,17 @@ export const formatAndSendEmails = async ({ const emailBody = formatCommitteeInterviewEmail(typedCommittee); await sendEmail({ - toEmails: committeeEmail, + // toEmails: committeeEmail, + toEmails: [ + "fhansteen@gmail.com", + "julian.ottosen@online.ntnu.no", + "jorgen.galdal@online.ntnu.no", + ], subject: subject, htmlContent: emailBody, }); + + console.log(emailBody); } } catch (error) { return { error: "Failed to send out interview times" }; diff --git a/lib/utils/sendInterviewTimes/formatInterviewEmail.ts b/lib/utils/sendInterviewTimes/formatInterviewEmail.ts index 5acbf6e4..b86a23b4 100644 --- a/lib/utils/sendInterviewTimes/formatInterviewEmail.ts +++ b/lib/utils/sendInterviewTimes/formatInterviewEmail.ts @@ -21,12 +21,20 @@ export const formatApplicantInterviewEmail = ( emailBody += `
  • Komite: ${changeDisplayName( committee.committeeName )}
    `; - emailBody += `Start: ${formatDateHours( - new Date(committee.interviewTime.start) - )}
    `; - emailBody += `Slutt: ${formatDateHours( - new Date(committee.interviewTime.end) - )}
    `; + + if (committee.interviewTime.start !== "Ikke satt") { + emailBody += `Start: ${formatDateHours( + new Date(committee.interviewTime.start) + )}
    `; + emailBody += `Slutt: ${formatDateHours( + new Date(committee.interviewTime.end) + )}
    `; + } + if (committee.interviewTime.start === "Ikke satt") { + emailBody += `Start: Ikke satt
    `; + emailBody += `Slutt: Ikke satt
    `; + } + emailBody += `Rom: ${committee.interviewTime.room}

  • `; }); @@ -52,12 +60,20 @@ export const formatCommitteeInterviewEmail = ( committee.applicants.forEach((applicant) => { emailBody += `
  • Navn: ${applicant.applicantName}
    `; emailBody += `Telefon: ${applicant.applicantPhone}
    `; - emailBody += `Start: ${formatDateHours( - new Date(applicant.interviewTime.start) - )}
    `; - emailBody += `Slutt: ${formatDateHours( - new Date(applicant.interviewTime.end) - )}
    `; + + if (applicant.interviewTime.start !== "Ikke satt") { + emailBody += `Start: ${formatDateHours( + new Date(applicant.interviewTime.start) + )}
    `; + emailBody += `Slutt: ${formatDateHours( + new Date(applicant.interviewTime.end) + )}
    `; + } + + if (applicant.interviewTime.start === "Ikke satt") { + emailBody += `Start: Ikke satt
    `; + emailBody += `Slutt: Ikke satt
    `; + } emailBody += `Rom: ${applicant.interviewTime.room}

  • `; }); diff --git a/lib/utils/sendInterviewTimes/formatInterviewSMS.ts b/lib/utils/sendInterviewTimes/formatInterviewSMS.ts index 5e0f2181..3d06b95d 100644 --- a/lib/utils/sendInterviewTimes/formatInterviewSMS.ts +++ b/lib/utils/sendInterviewTimes/formatInterviewSMS.ts @@ -14,9 +14,16 @@ export const formatInterviewSMS = (applicant: emailApplicantInterviewType) => { applicant.committees.forEach((committee) => { phoneBody += `Komite: ${changeDisplayName(committee.committeeName)} \n`; - phoneBody += `Tid: ${formatDateHours( - new Date(committee.interviewTime.start) - )}\n`; + + if (committee.interviewTime.start !== "Ikke satt") { + phoneBody += `Tid: ${formatDateHours( + new Date(committee.interviewTime.start) + )}\n`; + } + + if (committee.interviewTime.start === "Ikke satt") { + phoneBody += `Tid: Ikke satt \n`; + } phoneBody += `Rom: ${committee.interviewTime.room} \n \n`; }); diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index 9499416f..0ac8aabe 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -5,6 +5,8 @@ import { emailCommitteeInterviewType, periodType, algorithmType, + preferencesType, + committeePreferenceType, } from "../../types/types"; import { fetchCommitteeEmails } from "./fetchFunctions"; @@ -12,6 +14,7 @@ import { formatAndSendEmails } from "./formatAndSend"; import { getPeriods, markInterviewsSentByPeriodId } from "../../mongo/periods"; import { getCommitteesByPeriod } from "../../mongo/committees"; import { getInterviewsByPeriod } from "../../mongo/interviews"; +import { getApplication, getApplicationById } from "../../mongo/applicants"; export const sendOutInterviewTimes = async () => { try { @@ -23,11 +26,14 @@ export const sendOutInterviewTimes = async () => { for (const period of periods) { if ( - period.hasSentInterviewTimes === false && - period.applicationPeriod.end < new Date() + (period.hasSentInterviewTimes === false && + period.applicationPeriod.end < new Date()) || + period.name === "FAKE TEST OPPTAK!" ) { const periodId = String(period._id); + console.log("hei"); + const committeeInterviewTimesData = await getCommitteesByPeriod(periodId); if (!committeeInterviewTimesData || committeeInterviewTimesData.error) { @@ -41,7 +47,7 @@ export const sendOutInterviewTimes = async () => { const fetchedAlgorithmData = await getInterviewsByPeriod(periodId); const algorithmData = fetchedAlgorithmData.interviews || []; - const applicantsToEmail = formatApplicants( + const applicantsToEmail = await formatApplicants( algorithmData, periodId, period, @@ -52,7 +58,7 @@ export const sendOutInterviewTimes = async () => { const committeesToEmail = formatCommittees(applicantsToEmail); await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); - markInterviewsSentByPeriodId(periodId); + // markInterviewsSentByPeriodId(periodId); } } } catch (error) { @@ -60,16 +66,46 @@ export const sendOutInterviewTimes = async () => { } }; -const formatApplicants = ( +const formatApplicants = async ( algorithmData: algorithmType[], periodId: string, period: periodType, committeeEmails: committeeEmails[], committeeInterviewTimes: committeeInterviewType[] -): emailApplicantInterviewType[] => { +): Promise => { const applicantsToEmailMap: emailApplicantInterviewType[] = []; for (const app of algorithmData) { + const dbApplication = await getApplicationById(app.applicantId, periodId); + + if (!dbApplication || !dbApplication.application) continue; + + const preferencesCommittees: string[] = + "first" in dbApplication.application.preferences + ? [ + dbApplication.application.preferences.first, + dbApplication.application.preferences.second, + dbApplication.application.preferences.third, + ].filter((committee) => committee !== "") + : dbApplication.application.preferences.map( + (preference: committeePreferenceType) => preference.committee + ); + + const allCommittees = [ + ...preferencesCommittees, + ...dbApplication.application.optionalCommittees, + ]; + + console.log(allCommittees); + + const scheduledCommittees = app.interviews.map( + (interview) => interview.committeeName + ); + + const missingCommittees = allCommittees.filter( + (committee) => !scheduledCommittees.includes(committee) + ); + const committees = app.interviews.map((interview) => { const committeeEmail = committeeEmails.find( (email) => @@ -105,6 +141,23 @@ const formatApplicants = ( }; }); + for (const missingCommittee of missingCommittees) { + const committeeEmail = committeeEmails.find( + (email) => + email.name_short.toLowerCase() === missingCommittee.toLowerCase() + ); + + committees.push({ + committeeName: missingCommittee, + committeeEmail: committeeEmail?.email || "", + interviewTime: { + start: "Ikke satt", + end: "Ikke satt", + room: "Ikke satt", + }, + }); + } + const applicantToEmail = { periodId: periodId, period_name: period.name, diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index 2d8bcc59..e3ae87e2 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -35,7 +35,7 @@ const Admin = () => { const sendOutInterviewTimes = async ({ periodId }: { periodId: string }) => { try { - const response = await fetch(`/api/interviews/${periodId}`, { + const response = await fetch(`/api/periods/send-interview-times`, { method: "POST", }); if (!response.ok) { diff --git a/pages/api/interviews/[period-id].ts b/pages/api/interviews/[period-id].ts deleted file mode 100644 index 77a5fe1a..00000000 --- a/pages/api/interviews/[period-id].ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; - -import { getServerSession } from "next-auth"; -import { authOptions } from "../auth/[...nextauth]"; -import { - hasSession, - isAdmin, - isInCommitee, -} from "../../../lib/utils/apiChecks"; -import { sendOutInterviewTimes } from "../../../lib/utils/sendInterviewTimes/sendInterviewTimes"; - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerSession(req, res, authOptions); - const periodId = req.query["period-id"]; - - if (!periodId || typeof periodId !== "string") { - return res - .status(400) - .json({ error: "Invalid or missing periodId parameter" }); - } - - if (!hasSession(res, session)) return; - if (!isInCommitee(res, session)) return; - - if (req.method === "POST") { - if (!isAdmin(res, session)) return; - - try { - const result = await sendOutInterviewTimes({ periodId }); - if (result && result.error) { - throw new Error(result.error); - } - - if (result?.error) throw new Error(result.error); - return res.status(201).json({ result }); - } catch (error: any) { - return res.status(500).json({ error: error.message }); - } - } - - res.setHeader("Allow", ["POST"]); - res.status(405).end(`Method ${req.method} is not allowed.`); -}; - -export default handler; diff --git a/pages/api/periods/send-interview-times.ts b/pages/api/periods/send-interview-times.ts new file mode 100644 index 00000000..ddb4772a --- /dev/null +++ b/pages/api/periods/send-interview-times.ts @@ -0,0 +1,35 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { sendOutInterviewTimes } from "../../../lib/utils/sendInterviewTimes/sendInterviewTimes"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const secret = process.env.SEND_INTERVIEW_TIMES_SECRET; + + if (!secret) { + return res.status(500).json({ message: "Server configuration error" }); + } + + try { + if (req.method === "POST") { + const requestSecret = req.headers["x-secret"]; + + // if (!requestSecret || requestSecret !== secret) { + // return res.status(403).json({ message: "Forbidden" }); + // } + + const result = await sendOutInterviewTimes(); + if (result === undefined) { + throw new Error("An error occurred"); + } + const { error } = result; + if (error) throw new Error(error); + return res.status(201).json({ message: "Period created successfully" }); + } + } catch { + return res.status(500).json("An error occurred"); + } + + res.setHeader("Allow", ["POST"]); + res.status(405).end(`Method ${req.method} is not allowed.`); +}; + +export default handler; From c409bea2a91c879cfd1218cd0cc90c1128081124 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 11 Aug 2024 21:40:26 +0200 Subject: [PATCH 057/169] applicant object will only consist of ID --- .../bridge/fetch_applicants_and_committees.py | 29 +++++++++---------- algorithm/src/mip_matching/Applicant.py | 7 ++--- algorithm/src/mip_matching/Committee.py | 2 +- algorithm/src/mip_matching/TimeInterval.py | 2 +- algorithm/src/mip_matching/match_meetings.py | 24 +++++++-------- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index c666bf5f..098a25e1 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -49,17 +49,17 @@ def send_to_db(match_result: MeetingMatch, applicants: List[dict], periodId): print("Sending to db") print(formatted_results) - mongo_uri = os.getenv("MONGODB_URI") - db_name = os.getenv("DB_NAME") - client = MongoClient(mongo_uri, tlsCAFile=certifi.where()) + # mongo_uri = os.getenv("MONGODB_URI") + # db_name = os.getenv("DB_NAME") + # client = MongoClient(mongo_uri, tlsCAFile=certifi.where()) - db = client[db_name] # type: ignore + # db = client[db_name] # type: ignore - collection = db["interviews"] + # collection = db["interviews"] - collection.insert_many(formatted_results) + # collection.insert_many(formatted_results) - client.close() + # client.close() @@ -112,24 +112,21 @@ def fetch_committee_times(periodId): def format_match_results(match_results: MeetingMatch, applicants: List[dict], periodId) -> List[Dict]: transformed_results = {} - applicant_dict = {str(applicant['_id']): {'name': applicant['name'], 'email': applicant['email'], 'phone': applicant['phone']} for applicant in applicants} + # applicant_dict = {str(applicant['_id']): {'name': applicant['name'], 'email': applicant['email'], 'phone': applicant['phone']} for applicant in applicants} for result in match_results['matchings']: - applicant_id = str(result[1]) - applicant_info = applicant_dict.get(applicant_id, {'name': 'Unknown', 'email': 'Unknown'}) + applicant_id = str(result[0]) + if applicant_id not in transformed_results: transformed_results[applicant_id] = { "periodId": periodId, "applicantId": applicant_id, - "applicantName": applicant_info['name'], - "applicantEmail": applicant_info['email'], - "applicantPhone": applicant_info['phone'], "interviews": [] } - committee = result[2] - time_interval = result[3] + committee = result[1] + time_interval = result[2] start = time_interval.start.isoformat() end = time_interval.end.isoformat() @@ -145,7 +142,7 @@ def format_match_results(match_results: MeetingMatch, applicants: List[dict], pe def create_applicant_objects(applicants_data: List[dict], all_committees: dict[str, Committee]) -> set[Applicant]: applicants = set() for data in applicants_data: - applicant = Applicant(name=data['name'], email=data['email'], phone=data['phone'], id=str(data['_id'])) + applicant = Applicant(name=str(data['_id'])) optional_committee_names = data.get('optionalCommittees', []) optional_committees = {all_committees[name] for name in optional_committee_names if name in all_committees} diff --git a/algorithm/src/mip_matching/Applicant.py b/algorithm/src/mip_matching/Applicant.py index 8e2873e4..6172e5fb 100644 --- a/algorithm/src/mip_matching/Applicant.py +++ b/algorithm/src/mip_matching/Applicant.py @@ -15,13 +15,10 @@ class Applicant: komitéer hen har søkt på, og når søkeren kan ha intervjuer. """ - def __init__(self, name: str, email: str, phone: str, id: str): + def __init__(self, name: str): self.committees: list[Committee] = [] self.slots: set[TimeInterval] = set() self.name = name - self.email = email - self.phone = phone - self.id = id def add_committee(self, committee: Committee) -> None: self.committees.append(committee) @@ -86,4 +83,4 @@ def __str__(self) -> str: return self.name def __repr__(self) -> str: - return str(self) + return str(self) \ No newline at end of file diff --git a/algorithm/src/mip_matching/Committee.py b/algorithm/src/mip_matching/Committee.py index 115debc4..c24d529b 100644 --- a/algorithm/src/mip_matching/Committee.py +++ b/algorithm/src/mip_matching/Committee.py @@ -84,4 +84,4 @@ def __repr__(self): if __name__ == "__main__": - print("running") + print("running") \ No newline at end of file diff --git a/algorithm/src/mip_matching/TimeInterval.py b/algorithm/src/mip_matching/TimeInterval.py index f2156d5e..990ec4a6 100644 --- a/algorithm/src/mip_matching/TimeInterval.py +++ b/algorithm/src/mip_matching/TimeInterval.py @@ -110,4 +110,4 @@ def divide_interval(interval: TimeInterval, length: timedelta) -> list[TimeInter # return result # def __iter__(self): -# return self.list.__iter__() +# return self.list.__iter__() \ No newline at end of file diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index f4b9a6cd..3e773d10 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -1,4 +1,5 @@ from typing import TypedDict + from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee from mip_matching.Applicant import Applicant @@ -12,14 +13,12 @@ class MeetingMatch(TypedDict): solver_status: mip.OptimizationStatus matched_meetings: int total_wanted_meetings: int - matchings: list[tuple[str, Applicant, Committee, TimeInterval]] + matchings: list[tuple[Applicant, Committee, TimeInterval]] + def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> MeetingMatch: """Matches meetings and returns a MeetingMatch-object""" model = mip.Model(sense=mip.MAXIMIZE) - - for applicant in applicants: - print(f"Applicant: {applicant.name}, Committees: {[str(committee) for committee in applicant.get_committees()]}") m = {} @@ -66,22 +65,21 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me solver_status = model.optimize() # Få de faktiske møtetidene - matched_meetings: int = 0 + antall_matchede_møter: int = 0 matchings: list = [] - for (applicant, committee, interval), variable in m.items(): + for name, variable in m.items(): if variable.x: - matched_meetings += 1 - matchings.append((applicant.email, applicant.id, committee, interval)) - print(f"{(applicant, committee, interval)}") - + antall_matchede_møter += 1 + matchings.append(name) + print(f"{name}") - total_wanted_meetings = sum( + antall_ønskede_møter = sum( len(applicant.get_committees()) for applicant in applicants) match_object: MeetingMatch = { "solver_status": solver_status, - "matched_meetings": matched_meetings, - "total_wanted_meetings": total_wanted_meetings, + "matched_meetings": antall_matchede_møter, + "total_wanted_meetings": antall_ønskede_møter, "matchings": matchings, } From 390c011f0c777cc54c673989dc05048ba81f3d2a Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 11 Aug 2024 21:42:55 +0200 Subject: [PATCH 058/169] get name, email and phone from application object not algorithm --- lib/types/types.ts | 3 --- lib/utils/sendInterviewTimes/sendInterviewTimes.ts | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/types/types.ts b/lib/types/types.ts index 8d2fb5d3..deb48886 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -108,9 +108,6 @@ export type owCommitteeType = { }; export type algorithmType = { - applicantName: string; - applicantEmail: string; - applicantPhone: string; applicantId: string; interviews: { start: string; diff --git a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts index 0ac8aabe..6a397f4b 100644 --- a/lib/utils/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/utils/sendInterviewTimes/sendInterviewTimes.ts @@ -161,9 +161,9 @@ const formatApplicants = async ( const applicantToEmail = { periodId: periodId, period_name: period.name, - applicantName: app.applicantName, - applicantEmail: app.applicantEmail, - applicantPhone: app.applicantPhone, + applicantName: dbApplication.application.name, + applicantEmail: dbApplication.application.email, + applicantPhone: dbApplication.application.phone, committees: committees, }; From 3c603f5dfa7902d39eb195d734c75b3cf890fae3 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 11 Aug 2024 21:44:04 +0200 Subject: [PATCH 059/169] uncomment code --- .../bridge/fetch_applicants_and_committees.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 098a25e1..0b47cbb1 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -49,17 +49,17 @@ def send_to_db(match_result: MeetingMatch, applicants: List[dict], periodId): print("Sending to db") print(formatted_results) - # mongo_uri = os.getenv("MONGODB_URI") - # db_name = os.getenv("DB_NAME") - # client = MongoClient(mongo_uri, tlsCAFile=certifi.where()) + mongo_uri = os.getenv("MONGODB_URI") + db_name = os.getenv("DB_NAME") + client = MongoClient(mongo_uri, tlsCAFile=certifi.where()) - # db = client[db_name] # type: ignore + db = client[db_name] # type: ignore - # collection = db["interviews"] + collection = db["interviews"] - # collection.insert_many(formatted_results) + collection.insert_many(formatted_results) - # client.close() + client.close() From 9d612c95cee571d96c2631395117c4058ae0d2a9 Mon Sep 17 00:00:00 2001 From: fredrir Date: Sun, 11 Aug 2024 21:49:06 +0200 Subject: [PATCH 060/169] . --- lib/utils/sendInterviewTimes/formatAndSend.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/utils/sendInterviewTimes/formatAndSend.ts b/lib/utils/sendInterviewTimes/formatAndSend.ts index 6d173332..c8d559a9 100644 --- a/lib/utils/sendInterviewTimes/formatAndSend.ts +++ b/lib/utils/sendInterviewTimes/formatAndSend.ts @@ -33,11 +33,7 @@ export const formatAndSendEmails = async ({ await sendEmail({ // toEmails: applicantEmail, - toEmails: [ - "fhansteen@gmail.com", - "julian.ottosen@online.ntnu.no", - "jorgen.galdal@online.ntnu.no", - ], + toEmails: ["fhansteen@gmail.com"], subject: subject, htmlContent: emailBody, }); @@ -61,11 +57,7 @@ export const formatAndSendEmails = async ({ await sendEmail({ // toEmails: committeeEmail, - toEmails: [ - "fhansteen@gmail.com", - "julian.ottosen@online.ntnu.no", - "jorgen.galdal@online.ntnu.no", - ], + toEmails: ["fhansteen@gmail.com"], subject: subject, htmlContent: emailBody, }); From c24f919a4221794c4cd63561d5323aca2e354206 Mon Sep 17 00:00:00 2001 From: Julian Ammouche Ottosen <42567826+julian-ao@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:10:27 +0200 Subject: [PATCH 061/169] npm audit fix (#250) --- package-lock.json | 407 +++++++++++++++++++++++----------------------- 1 file changed, 205 insertions(+), 202 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e84e0a8..96e66b54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -169,46 +169,46 @@ } }, "node_modules/@aws-sdk/client-ses": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.614.0.tgz", - "integrity": "sha512-PbnnfIq2oNy+Um++rzqAk+7+OXtRzv6EymiIhvRYCq/T5rRVPjuIgV3mjrxl8hrF4SIAq40YVojzT1DITERFiw==", + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.624.0.tgz", + "integrity": "sha512-CIi5tbQQ3wI4MBYUNZDti0xkj5SonZ+l/Bc4eBxqJK61tQB45T68SSigdMBr9fICTQ067WMYvvOtTvq9PqV4jA==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.614.0", - "@aws-sdk/client-sts": "3.614.0", - "@aws-sdk/core": "3.614.0", - "@aws-sdk/credential-provider-node": "3.614.0", - "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/client-sso-oidc": "3.624.0", + "@aws-sdk/client-sts": "3.624.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", - "@aws-sdk/middleware-recursion-detection": "3.609.0", - "@aws-sdk/middleware-user-agent": "3.614.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.2.6", - "@smithy/fetch-http-handler": "^3.2.1", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", - "@smithy/middleware-content-length": "^3.0.3", - "@smithy/middleware-endpoint": "^3.0.5", - "@smithy/middleware-retry": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", - "@smithy/node-http-handler": "^3.1.2", - "@smithy/protocol-http": "^4.0.3", - "@smithy/smithy-client": "^3.1.7", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.9", - "@smithy/util-defaults-mode-node": "^3.0.9", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -221,43 +221,43 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.614.0.tgz", - "integrity": "sha512-p5pyYaxRzBttjBkqfc8i3K7DzBdTg3ECdVgBo6INIUxfvDy0J8QUE8vNtCgvFIkq+uPw/8M+Eo4zzln7anuO0Q==", + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.624.0.tgz", + "integrity": "sha512-EX6EF+rJzMPC5dcdsu40xSi2To7GSvdGQNIpe97pD9WvZwM9tRNQnNM4T6HA4gjV1L6Jwk8rBlG/CnveXtLEMw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.614.0", - "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", - "@aws-sdk/middleware-recursion-detection": "3.609.0", - "@aws-sdk/middleware-user-agent": "3.614.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.2.6", - "@smithy/fetch-http-handler": "^3.2.1", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", - "@smithy/middleware-content-length": "^3.0.3", - "@smithy/middleware-endpoint": "^3.0.5", - "@smithy/middleware-retry": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", - "@smithy/node-http-handler": "^3.1.2", - "@smithy/protocol-http": "^4.0.3", - "@smithy/smithy-client": "^3.1.7", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.9", - "@smithy/util-defaults-mode-node": "^3.0.9", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -269,44 +269,44 @@ } }, "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.614.0.tgz", - "integrity": "sha512-BI1NWcpppbHg/28zbUg54dZeckork8BItZIcjls12vxasy+p3iEzrJVG60jcbUTTsk3Qc1tyxNfrdcVqx0y7Ww==", + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.624.0.tgz", + "integrity": "sha512-Ki2uKYJKKtfHxxZsiMTOvJoVRP6b2pZ1u3rcUb2m/nVgBPUfLdl8ZkGpqE29I+t5/QaS/sEdbn6cgMUZwl+3Dg==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.614.0", - "@aws-sdk/credential-provider-node": "3.614.0", - "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", - "@aws-sdk/middleware-recursion-detection": "3.609.0", - "@aws-sdk/middleware-user-agent": "3.614.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.2.6", - "@smithy/fetch-http-handler": "^3.2.1", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", - "@smithy/middleware-content-length": "^3.0.3", - "@smithy/middleware-endpoint": "^3.0.5", - "@smithy/middleware-retry": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", - "@smithy/node-http-handler": "^3.1.2", - "@smithy/protocol-http": "^4.0.3", - "@smithy/smithy-client": "^3.1.7", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.9", - "@smithy/util-defaults-mode-node": "^3.0.9", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -317,49 +317,49 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.614.0" + "@aws-sdk/client-sts": "^3.624.0" } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.614.0.tgz", - "integrity": "sha512-i6QmaVA1KHHYNnI2VYQy/sc31rLm4+jSp8b/YbQpFnD0w3aXsrEEHHlxek45uSkHb4Nrj1omFBVy/xp1WVYx2Q==", + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.624.0.tgz", + "integrity": "sha512-k36fLZCb2nfoV/DKK3jbRgO/Yf7/R80pgYfMiotkGjnZwDmRvNN08z4l06L9C+CieazzkgRxNUzyppsYcYsQaw==", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.614.0", - "@aws-sdk/core": "3.614.0", - "@aws-sdk/credential-provider-node": "3.614.0", - "@aws-sdk/middleware-host-header": "3.609.0", + "@aws-sdk/client-sso-oidc": "3.624.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", "@aws-sdk/middleware-logger": "3.609.0", - "@aws-sdk/middleware-recursion-detection": "3.609.0", - "@aws-sdk/middleware-user-agent": "3.614.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", "@aws-sdk/region-config-resolver": "3.614.0", "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", "@aws-sdk/util-user-agent-browser": "3.609.0", "@aws-sdk/util-user-agent-node": "3.614.0", "@smithy/config-resolver": "^3.0.5", - "@smithy/core": "^2.2.6", - "@smithy/fetch-http-handler": "^3.2.1", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", "@smithy/hash-node": "^3.0.3", "@smithy/invalid-dependency": "^3.0.3", - "@smithy/middleware-content-length": "^3.0.3", - "@smithy/middleware-endpoint": "^3.0.5", - "@smithy/middleware-retry": "^3.0.9", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", "@smithy/middleware-stack": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", - "@smithy/node-http-handler": "^3.1.2", - "@smithy/protocol-http": "^4.0.3", - "@smithy/smithy-client": "^3.1.7", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/url-parser": "^3.0.3", "@smithy/util-base64": "^3.0.0", "@smithy/util-body-length-browser": "^3.0.0", "@smithy/util-body-length-node": "^3.0.0", - "@smithy/util-defaults-mode-browser": "^3.0.9", - "@smithy/util-defaults-mode-node": "^3.0.9", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -371,16 +371,18 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.614.0.tgz", - "integrity": "sha512-BUuS5/1YkgmKc4J0bg83XEtMyDHVyqG2QDzfmhYe8gbOIZabUl1FlrFVwhCAthtrrI6MPGTQcERB4BtJKUSplw==", + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.624.0.tgz", + "integrity": "sha512-WyFmPbhRIvtWi7hBp8uSFy+iPpj8ccNV/eX86hwF4irMjfc/FtsGVIAeBXxXM/vGCjkdfEzOnl+tJ2XACD4OXg==", "dependencies": { - "@smithy/core": "^2.2.6", - "@smithy/protocol-http": "^4.0.3", - "@smithy/signature-v4": "^3.1.2", - "@smithy/smithy-client": "^3.1.7", + "@smithy/core": "^2.3.2", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", - "fast-xml-parser": "4.2.5", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" }, "engines": { @@ -388,9 +390,9 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.609.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.609.0.tgz", - "integrity": "sha512-v69ZCWcec2iuV9vLVJMa6fAb5xwkzN4jYIT8yjo2c4Ia/j976Q+TPf35Pnz5My48Xr94EFcaBazrWedF+kwfuQ==", + "version": "3.620.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.620.1.tgz", + "integrity": "sha512-ExuILJ2qLW5ZO+rgkNRj0xiAipKT16Rk77buvPP8csR7kkCflT/gXTyzRe/uzIiETTxM7tr8xuO9MP/DQXqkfg==", "dependencies": { "@aws-sdk/types": "3.609.0", "@smithy/property-provider": "^3.1.3", @@ -402,18 +404,18 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.614.0.tgz", - "integrity": "sha512-YIEjlNUKb3Vo/iTnGAPdsiDC3FUUnNoex2OwU8LmR7AkYZiWdB8nx99DfgkkY+OFMUpw7nKD2PCOtuFONelfGA==", + "version": "3.622.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.622.0.tgz", + "integrity": "sha512-VUHbr24Oll1RK3WR8XLUugLpgK9ZuxEm/NVeVqyFts1Ck9gsKpRg1x4eH7L7tW3SJ4TDEQNMbD7/7J+eoL2svg==", "dependencies": { "@aws-sdk/types": "3.609.0", - "@smithy/fetch-http-handler": "^3.2.1", - "@smithy/node-http-handler": "^3.1.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", "@smithy/property-provider": "^3.1.3", - "@smithy/protocol-http": "^4.0.3", - "@smithy/smithy-client": "^3.1.7", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", - "@smithy/util-stream": "^3.0.6", + "@smithy/util-stream": "^3.1.3", "tslib": "^2.6.2" }, "engines": { @@ -421,17 +423,17 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.614.0.tgz", - "integrity": "sha512-KfLuLFGwlvFSZ2MuzYwWGPb1y5TeiwX5okIDe0aQ1h10oD3924FXbN+mabOnUHQ8EFcGAtCaWbrC86mI7ktC6A==", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.609.0", - "@aws-sdk/credential-provider-http": "3.614.0", - "@aws-sdk/credential-provider-process": "3.614.0", - "@aws-sdk/credential-provider-sso": "3.614.0", - "@aws-sdk/credential-provider-web-identity": "3.609.0", + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.624.0.tgz", + "integrity": "sha512-mMoNIy7MO2WTBbdqMyLpbt6SZpthE6e0GkRYpsd0yozPt0RZopcBhEh+HG1U9Y1PVODo+jcMk353vAi61CfnhQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.624.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", - "@smithy/credential-provider-imds": "^3.1.4", + "@smithy/credential-provider-imds": "^3.2.0", "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", @@ -441,22 +443,22 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.614.0" + "@aws-sdk/client-sts": "^3.624.0" } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.614.0.tgz", - "integrity": "sha512-4J6gPEuFZP0mkWq5E//oMS1vrmMM88iNNcv7TEljYnsc6JTAlKejCyFwx6CN+nkIhmIZsl06SXIhBemzBdBPfg==", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.609.0", - "@aws-sdk/credential-provider-http": "3.614.0", - "@aws-sdk/credential-provider-ini": "3.614.0", - "@aws-sdk/credential-provider-process": "3.614.0", - "@aws-sdk/credential-provider-sso": "3.614.0", - "@aws-sdk/credential-provider-web-identity": "3.609.0", + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.624.0.tgz", + "integrity": "sha512-vYyGK7oNpd81BdbH5IlmQ6zfaQqU+rPwsKTDDBeLRjshtrGXOEpfoahVpG9PX0ibu32IOWp4ZyXBNyVrnvcMOw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-ini": "3.624.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.624.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", "@aws-sdk/types": "3.609.0", - "@smithy/credential-provider-imds": "^3.1.4", + "@smithy/credential-provider-imds": "^3.2.0", "@smithy/property-provider": "^3.1.3", "@smithy/shared-ini-file-loader": "^3.1.4", "@smithy/types": "^3.3.0", @@ -467,9 +469,9 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.614.0.tgz", - "integrity": "sha512-Q0SI0sTRwi8iNODLs5+bbv8vgz8Qy2QdxbCHnPk/6Cx6LMf7i3dqmWquFbspqFRd8QiqxStrblwxrUYZi09tkA==", + "version": "3.620.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.620.1.tgz", + "integrity": "sha512-hWqFMidqLAkaV9G460+1at6qa9vySbjQKKc04p59OT7lZ5cO5VH5S4aI05e+m4j364MBROjjk2ugNvfNf/8ILg==", "dependencies": { "@aws-sdk/types": "3.609.0", "@smithy/property-provider": "^3.1.3", @@ -482,11 +484,11 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.614.0.tgz", - "integrity": "sha512-55+gp0JY4451cWI1qXmVMFM0GQaBKiQpXv2P0xmd9P3qLDyeFUSEW8XPh0d2lb1ICr6x4s47ynXVdGCIv2mXMg==", + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.624.0.tgz", + "integrity": "sha512-A02bayIjU9APEPKr3HudrFHEx0WfghoSPsPopckDkW7VBqO4wizzcxr75Q9A3vNX+cwg0wCN6UitTNe6pVlRaQ==", "dependencies": { - "@aws-sdk/client-sso": "3.614.0", + "@aws-sdk/client-sso": "3.624.0", "@aws-sdk/token-providers": "3.614.0", "@aws-sdk/types": "3.609.0", "@smithy/property-provider": "^3.1.3", @@ -499,9 +501,9 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.609.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.609.0.tgz", - "integrity": "sha512-U+PG8NhlYYF45zbr1km3ROtBMYqyyj/oK8NRp++UHHeuavgrP+4wJ4wQnlEaKvJBjevfo3+dlIBcaeQ7NYejWg==", + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.621.0.tgz", + "integrity": "sha512-w7ASSyfNvcx7+bYGep3VBgC3K6vEdLmlpjT7nSIHxxQf+WSdvy+HynwJosrpZax0sK5q0D1Jpn/5q+r5lwwW6w==", "dependencies": { "@aws-sdk/types": "3.609.0", "@smithy/property-provider": "^3.1.3", @@ -512,16 +514,16 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.609.0" + "@aws-sdk/client-sts": "^3.621.0" } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.609.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.609.0.tgz", - "integrity": "sha512-iTKfo158lc4jLDfYeZmYMIBHsn8m6zX+XB6birCSNZ/rrlzAkPbGE43CNdKfvjyWdqgLMRXF+B+OcZRvqhMXPQ==", + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.620.0.tgz", + "integrity": "sha512-VMtPEZwqYrII/oUkffYsNWY9PZ9xpNJpMgmyU0rlDQ25O1c0Hk3fJmZRe6pEkAJ0omD7kLrqGl1DUjQVxpd/Rg==", "dependencies": { "@aws-sdk/types": "3.609.0", - "@smithy/protocol-http": "^4.0.3", + "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, @@ -543,12 +545,12 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.609.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.609.0.tgz", - "integrity": "sha512-6sewsYB7/o/nbUfA99Aa/LokM+a/u4Wpm/X2o0RxOsDtSB795ObebLJe2BxY5UssbGaWkn7LswyfvrdZNXNj1w==", + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.620.0.tgz", + "integrity": "sha512-nh91S7aGK3e/o1ck64sA/CyoFw+gAYj2BDOnoNa6ouyCrVJED96ZXWbhye/fz9SgmNUZR2g7GdVpiLpMKZoI5w==", "dependencies": { "@aws-sdk/types": "3.609.0", - "@smithy/protocol-http": "^4.0.3", + "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, @@ -557,13 +559,13 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.614.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.614.0.tgz", - "integrity": "sha512-xUxh0UPQiMTG6E31Yvu6zVYlikrIcFDKljM11CaatInzvZubGTGiX0DjpqRlfGzUNsuPc/zNrKwRP2+wypgqIw==", + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.620.0.tgz", + "integrity": "sha512-bvS6etn+KsuL32ubY5D3xNof1qkenpbJXf/ugGXbg0n98DvDFQ/F+SMLxHgbnER5dsKYchNnhmtI6/FC3HFu/A==", "dependencies": { "@aws-sdk/types": "3.609.0", "@aws-sdk/util-endpoints": "3.614.0", - "@smithy/protocol-http": "^4.0.3", + "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, @@ -1207,15 +1209,15 @@ } }, "node_modules/@smithy/core": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.2.7.tgz", - "integrity": "sha512-Wwd9QWKaYdR+n/oIqJbuwSr9lHuv7sa1e3Zu4wIToZl0sS7xapTYYqQtXP1hKKtIWz0jl8AhvOfNwkfT5jjV0w==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.3.2.tgz", + "integrity": "sha512-in5wwt6chDBcUv1Lw1+QzZxN9fBffi+qOixfb65yK4sDuKG7zAUO9HAFqmVzsZM3N+3tTyvZjtnDXePpvp007Q==", "dependencies": { - "@smithy/middleware-endpoint": "^3.0.5", - "@smithy/middleware-retry": "^3.0.10", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", "@smithy/middleware-serde": "^3.0.3", - "@smithy/protocol-http": "^4.0.4", - "@smithy/smithy-client": "^3.1.8", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/util-middleware": "^3.0.3", "tslib": "^2.6.2" @@ -1225,9 +1227,9 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.1.4.tgz", - "integrity": "sha512-NKyH01m97Xa5xf3pB2QOF3lnuE8RIK0hTVNU5zvZAwZU8uspYO4DHQVlK+Y5gwSrujTfHvbfd1D9UFJAc0iYKQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.0.tgz", + "integrity": "sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA==", "dependencies": { "@smithy/node-config-provider": "^3.1.4", "@smithy/property-provider": "^3.1.3", @@ -1240,11 +1242,11 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.2.tgz", - "integrity": "sha512-3LaWlBZObyGrOOd7e5MlacnAKEwFBmAeiW/TOj2eR9475Vnq30uS2510+tnKbxrGjROfNdOhQqGo5j3sqLT6bA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.4.tgz", + "integrity": "sha512-kBprh5Gs5h7ug4nBWZi1FZthdqSM+T7zMmsZxx0IBvWUn7dK3diz2SHn7Bs4dQGFDk8plDv375gzenDoNwrXjg==", "dependencies": { - "@smithy/protocol-http": "^4.0.4", + "@smithy/protocol-http": "^4.1.0", "@smithy/querystring-builder": "^3.0.3", "@smithy/types": "^3.3.0", "@smithy/util-base64": "^3.0.0", @@ -1286,11 +1288,11 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.4.tgz", - "integrity": "sha512-wySGje/KfhsnF8YSh9hP16pZcl3C+X6zRsvSfItQGvCyte92LliilU3SD0nR7kTlxnAJwxY8vE/k4Eoezj847Q==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.5.tgz", + "integrity": "sha512-ILEzC2eyxx6ncej3zZSwMpB5RJ0zuqH7eMptxC4KN3f+v9bqT8ohssKbhNR78k/2tWW+KS5Spw+tbPF4Ejyqvw==", "dependencies": { - "@smithy/protocol-http": "^4.0.4", + "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, @@ -1299,9 +1301,9 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.0.5.tgz", - "integrity": "sha512-V4acqqrh5tDxUEGVTOgf2lYMZqPQsoGntCrjrJZEeBzEzDry2d2vcI1QCXhGltXPPY+BMc6eksZMguA9fIY8vA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.0.tgz", + "integrity": "sha512-5y5aiKCEwg9TDPB4yFE7H6tYvGFf1OJHNczeY10/EFF8Ir8jZbNntQJxMWNfeQjC1mxPsaQ6mR9cvQbf+0YeMw==", "dependencies": { "@smithy/middleware-serde": "^3.0.3", "@smithy/node-config-provider": "^3.1.4", @@ -1316,14 +1318,14 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.10.tgz", - "integrity": "sha512-+6ibpv6jpkTNJS6yErQSEjbxCWf1/jMeUSlpSlUiTYf73LGR9riSRlIrL1+JEW0eEpb6MelQ04BIc38aj8GtxQ==", + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.14.tgz", + "integrity": "sha512-7ZaWZJOjUxa5hgmuMspyt8v/zVsh0GXYuF7OvCmdcbVa/xbnKQoYC+uYKunAqRGTkxjOyuOCw9rmFUFOqqC0eQ==", "dependencies": { "@smithy/node-config-provider": "^3.1.4", - "@smithy/protocol-http": "^4.0.4", + "@smithy/protocol-http": "^4.1.0", "@smithy/service-error-classification": "^3.0.3", - "@smithy/smithy-client": "^3.1.8", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", @@ -1373,12 +1375,12 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.1.3.tgz", - "integrity": "sha512-UiKZm8KHb/JeOPzHZtRUfyaRDO1KPKPpsd7iplhiwVGOeVdkiVJ5bVe7+NhWREMOKomrDIDdSZyglvMothLg0Q==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.1.4.tgz", + "integrity": "sha512-+UmxgixgOr/yLsUxcEKGH0fMNVteJFGkmRltYFHnBMlogyFdpzn2CwqWmxOrfJELhV34v0WSlaqG1UtE1uXlJg==", "dependencies": { "@smithy/abort-controller": "^3.1.1", - "@smithy/protocol-http": "^4.0.4", + "@smithy/protocol-http": "^4.1.0", "@smithy/querystring-builder": "^3.0.3", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" @@ -1400,9 +1402,9 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.0.4.tgz", - "integrity": "sha512-fAA2O4EFyNRyYdFLVIv5xMMeRb+3fRKc/Rt2flh5k831vLvUmNFXcydeg7V3UeEhGURJI4c1asmGJBjvmF6j8Q==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.0.tgz", + "integrity": "sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==", "dependencies": { "@smithy/types": "^3.3.0", "tslib": "^2.6.2" @@ -1460,11 +1462,12 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-3.1.2.tgz", - "integrity": "sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.1.0.tgz", + "integrity": "sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag==", "dependencies": { "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", "@smithy/util-middleware": "^3.0.3", @@ -1477,15 +1480,15 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.1.8.tgz", - "integrity": "sha512-nUNGCa0NgvtD0eM45732EBp1H9JQITChMBegGtPRhJD00v3hiFF6tibiOihcYwP5mbp9Kui+sOCl86rDT/Ew2w==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.1.12.tgz", + "integrity": "sha512-wtm8JtsycthkHy1YA4zjIh2thJgIQ9vGkoR639DBx5lLlLNU0v4GARpQZkr2WjXue74nZ7MiTSWfVrLkyD8RkA==", "dependencies": { - "@smithy/middleware-endpoint": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", "@smithy/middleware-stack": "^3.0.3", - "@smithy/protocol-http": "^4.0.4", + "@smithy/protocol-http": "^4.1.0", "@smithy/types": "^3.3.0", - "@smithy/util-stream": "^3.1.0", + "@smithy/util-stream": "^3.1.3", "tslib": "^2.6.2" }, "engines": { @@ -1569,12 +1572,12 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.10.tgz", - "integrity": "sha512-WgaNxh33md2zvlD+1TSceVmM7DIy7qYMtuhOat+HYoTntsg0QTbNvoB/5DRxEwSpN84zKf9O34yqzRRtxJZgFg==", + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.14.tgz", + "integrity": "sha512-0iwTgKKmAIf+vFLV8fji21Jb2px11ktKVxbX6LIDPAUJyWQqGqBVfwba7xwa1f2FZUoolYQgLvxQEpJycXuQ5w==", "dependencies": { "@smithy/property-provider": "^3.1.3", - "@smithy/smithy-client": "^3.1.8", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -1584,15 +1587,15 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.10.tgz", - "integrity": "sha512-3x/pcNIFyaAEQqXc3qnQsCFLlTz/Mwsfl9ciEPU56/Dk/g1kTFjkzyLbUNJaeOo5HT01VrpJBKrBuN94qbPm9A==", + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.14.tgz", + "integrity": "sha512-e9uQarJKfXApkTMMruIdxHprhcXivH1flYCe8JRDTzkkLx8dA3V5J8GZlST9yfDiRWkJpZJlUXGN9Rc9Ade3OQ==", "dependencies": { "@smithy/config-resolver": "^3.0.5", - "@smithy/credential-provider-imds": "^3.1.4", + "@smithy/credential-provider-imds": "^3.2.0", "@smithy/node-config-provider": "^3.1.4", "@smithy/property-provider": "^3.1.3", - "@smithy/smithy-client": "^3.1.8", + "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "tslib": "^2.6.2" }, @@ -1650,12 +1653,12 @@ } }, "node_modules/@smithy/util-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.0.tgz", - "integrity": "sha512-QEMvyv58QIptWA8cpQPbHagJOAlrbCt3ueB9EShwdFfVMYAviXdVtksszQQq+o+dv5dalUMWUbUHUDSJgkF9xg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.3.tgz", + "integrity": "sha512-FIv/bRhIlAxC0U7xM1BCnF2aDRPq0UaelqBHkM2lsCp26mcBbgI0tCVTv+jGdsQLUmAMybua/bjDsSu8RQHbmw==", "dependencies": { - "@smithy/fetch-http-handler": "^3.2.2", - "@smithy/node-http-handler": "^3.1.3", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", "@smithy/types": "^3.3.0", "@smithy/util-base64": "^3.0.0", "@smithy/util-buffer-from": "^3.0.0", @@ -3418,17 +3421,17 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", - "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", "funding": [ - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - }, { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" } ], "dependencies": { From 3ab9a0923a378ac7f62b8ec74b4035f3365e4b8c Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 12 Aug 2024 15:05:24 +0200 Subject: [PATCH 062/169] add node committees and shuffle list --- lib/utils/shuffleList.ts | 7 +++ pages/committees.tsx | 94 +++++++++++++++++++++++++++++++--------- 2 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 lib/utils/shuffleList.ts diff --git a/lib/utils/shuffleList.ts b/lib/utils/shuffleList.ts new file mode 100644 index 00000000..ebf2fc1c --- /dev/null +++ b/lib/utils/shuffleList.ts @@ -0,0 +1,7 @@ +export function shuffleList(array: any[]) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +} diff --git a/pages/committees.tsx b/pages/committees.tsx index a272109b..d4327113 100644 --- a/pages/committees.tsx +++ b/pages/committees.tsx @@ -7,12 +7,20 @@ import { fetchOwCommittees } from "../lib/api/committeesApi"; import ErrorPage from "../components/ErrorPage"; import { fetchPeriods } from "../lib/api/periodApi"; import { MainTitle } from "../components/Typography"; +import { UsersIcon } from "@heroicons/react/24/outline"; +import { Tabs } from "../components/Tabs"; +import { UserIcon } from "@heroicons/react/24/solid"; +import { shuffleList } from "../lib/utils/shuffleList"; const excludedCommittees = ["Faddere", "Output"]; +const otherCommittees = ["Jubkom", "Velkom", "Ekskom", "Debug"]; + const Committees = () => { const [committees, setCommittees] = useState([]); + const [nodeCommittees, setNodeCommittees] = useState([]); const [periods, setPeriods] = useState([]); + const [activeTab, setActiveTab] = useState(0); const { data: owCommitteeData, @@ -35,11 +43,19 @@ const Committees = () => { useEffect(() => { if (!owCommitteeData) return; - const filteredCommittees = owCommitteeData.filter( + const filterNodeCommittees = owCommitteeData.filter( + (committee: owCommitteeType) => + otherCommittees.includes(committee.name_short) + ); + setNodeCommittees(filterNodeCommittees); + + let filteredCommittees = owCommitteeData.filter( (committee: owCommitteeType) => - !excludedCommittees.includes(committee.name_short) + !excludedCommittees.includes(committee.name_short) && + !otherCommittees.includes(committee.name_short) ); + filteredCommittees = shuffleList(filteredCommittees); setCommittees(filteredCommittees); }, [owCommitteeData]); @@ -79,25 +95,61 @@ const Committees = () => { if (owCommitteeIsError || periodsIsError) return ; return ( -
    -
    - -
    -
    - {committees?.map((committee, index) => { - return ( - - ); - })} -
    +
    + + + { + setActiveTab(index); + }} + content={[ + { + title: "Komitéer", + icon: , + content: ( +
    +
    + {committees?.map((committee, index) => { + return ( + + ); + })} +
    +
    + ), + }, + { + title: "Node Komitéer", + icon: , + content: ( +
    +
    + {nodeCommittees?.map((committee, index) => { + return ( + + ); + })} +
    +
    + ), + }, + ]} + />
    ); }; From 41fa2270f412bcfde86489ff27bdd9743f18543d Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 12 Aug 2024 15:07:32 +0200 Subject: [PATCH 063/169] slightly enlarge image --- components/CommitteeAboutCard.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/CommitteeAboutCard.tsx b/components/CommitteeAboutCard.tsx index 502719b3..8fbf680a 100644 --- a/components/CommitteeAboutCard.tsx +++ b/components/CommitteeAboutCard.tsx @@ -17,7 +17,7 @@ const CommitteeAboutCard = ({ {name_long}
    @@ -25,9 +25,11 @@ const CommitteeAboutCard = ({ {name_long} {name_long !== name_short && `(${name_short})`} {hasPeriod && ( - Har opptak! + + Har opptak! + )} -
    +

    {email}

    {application_description || "Ingen opptaksbeskrivelse"} From 63af012197ac13abfd52227b409013a28eed711f Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 12 Aug 2024 15:10:45 +0200 Subject: [PATCH 064/169] use sm image instead --- components/CommitteeAboutCard.tsx | 2 +- lib/types/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/CommitteeAboutCard.tsx b/components/CommitteeAboutCard.tsx index 8fbf680a..f2f56093 100644 --- a/components/CommitteeAboutCard.tsx +++ b/components/CommitteeAboutCard.tsx @@ -15,7 +15,7 @@ const CommitteeAboutCard = ({ return (

    {name_long} diff --git a/lib/types/types.ts b/lib/types/types.ts index 6deed148..1255aba5 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -103,5 +103,5 @@ export type owCommitteeType = { description_long?: string; description_short?: string; application_description?: string; - image?: { xs: string }; + image?: { xs: string; sm: string }; }; From c7b14556f17834ece02e423e5eca03d271061465 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 12 Aug 2024 16:20:45 +0200 Subject: [PATCH 065/169] will hide applicants for committees before interviews --- lib/mongo/applicants.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/mongo/applicants.ts b/lib/mongo/applicants.ts index e3883136..70e6030a 100644 --- a/lib/mongo/applicants.ts +++ b/lib/mongo/applicants.ts @@ -1,6 +1,7 @@ import { Collection, Db, MongoClient, ObjectId } from "mongodb"; import clientPromise from "./mongodb"; -import { applicantType, preferencesType } from "../types/types"; +import { applicantType, periodType, preferencesType } from "../types/types"; +import { getPeriodById } from "./periods"; let client: MongoClient; let db: Db; @@ -109,6 +110,13 @@ export const getApplicantsForCommittee = async ( // Henter alle søkere for perioden const result = await applicants.find({ periodId: periodId }).toArray(); + const periodData = await getPeriodById(periodId); + const period: periodType | undefined = periodData.period; + + if (!period) { + return null; + } + // Type guard const isPreferencesType = ( preferences: any @@ -153,6 +161,14 @@ export const getApplicantsForCommittee = async ( applicant.optionalCommittees = []; + if (new Date(period.interviewPeriod.start) > new Date()) { + applicant.name = "Skjult "; + applicant.phone = "Skjult"; + applicant.email = "Skjult"; + applicant.about = "Skjult"; + applicant.grade = "-"; + } + const isSelectedCommitteePresent = preferencesArray.includes(selectedCommittee); From 9be2807d0bcf0cadd4c60d4a3ef2648610c3254a Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 12 Aug 2024 16:22:49 +0200 Subject: [PATCH 066/169] . --- lib/mongo/applicants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mongo/applicants.ts b/lib/mongo/applicants.ts index 70e6030a..45a5133e 100644 --- a/lib/mongo/applicants.ts +++ b/lib/mongo/applicants.ts @@ -114,7 +114,7 @@ export const getApplicantsForCommittee = async ( const period: periodType | undefined = periodData.period; if (!period) { - return null; + return { error: "Period not found" }; } // Type guard From 25080c27f1c95a8a4b097d9ef5bad40e4d54d052 Mon Sep 17 00:00:00 2001 From: fredrir Date: Mon, 12 Aug 2024 16:38:18 +0200 Subject: [PATCH 067/169] change welcome description --- pages/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pages/index.tsx b/pages/index.tsx index 5055ff0f..61401f08 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -27,11 +27,12 @@ const Home = () => {

    - opptak.online + Online Opptak

    - Her skal det stå masse kult om komitéopptak og sånn og om at man må - bli med i komité og at dette er det bra opptakssystem og sånn. + Her kan du søke for å bli en del av en av Onlines komitéer. + Komitémedlemmene våre får Online til å gå rundt, og arbeider for at + alle informatikkstudenter skal ha en flott studiehverdag.

    - )} - {committeeHasSubmitedTimes && committeeHasSubmitedMessage && ( -
    - -
    -
    -
    - )} -
    - ); -}; - -export default SendCommitteeMessage; diff --git a/lib/mongo/committees.ts b/lib/mongo/committees.ts index 58747327..eb2df3d6 100644 --- a/lib/mongo/committees.ts +++ b/lib/mongo/committees.ts @@ -39,36 +39,6 @@ const userHasAccessCommittee = ( return userCommittees.includes(dbCommittees); }; -export const updateCommitteeMessage = async ( - committee: string, - periodId: string, - message: string, - userCommittees: string[] -) => { - try { - if (!committees) await init(); - if (!userHasAccessCommittee(userCommittees, committee)) { - return { error: "User does not have access to this committee" }; - } - - const result = await committees.findOneAndUpdate( - { committee: committee, periodId: periodId }, - { $set: { message: message } }, - { returnDocument: "after" } - ); - - const updatedCommittee = result; - - if (updatedCommittee) { - return { updatedMessage: updatedCommittee.message }; - } else { - return { error: "Failed to update message" }; - } - } catch (error) { - return { error: "Failed to update message" }; - } -}; - export const getCommittees = async ( periodId: string, selectedCommittee: string, diff --git a/pages/api/committees/times/[period-id]/[committee].ts b/pages/api/committees/times/[period-id]/[committee].ts index ff20f15e..1705b33a 100644 --- a/pages/api/committees/times/[period-id]/[committee].ts +++ b/pages/api/committees/times/[period-id]/[committee].ts @@ -2,7 +2,6 @@ import { NextApiRequest, NextApiResponse } from "next"; import { getCommittees, deleteCommittee, - updateCommitteeMessage, } from "../../../../../lib/mongo/committees"; import { getServerSession } from "next-auth"; import { authOptions } from "../../../auth/[...nextauth]"; @@ -44,37 +43,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } } - if (req.method === "PUT") { - const { message } = req.body; - - try { - if (typeof message !== "string") { - return res.status(400).json({ error: "Invalid message parameter" }); - } - - const { period } = await getPeriodById(String(periodId)); - if (!period) { - return res.status(400).json({ error: "Invalid periodId" }); - } - - if (new Date() > new Date(period.applicationPeriod.end)) { - return res.status(400).json({ error: "Application period has ended" }); - } - - const { updatedMessage, error } = await updateCommitteeMessage( - selectedCommittee, - periodId, - message, - session!.user?.committees ?? [] - ); - if (error) throw new Error(error); - - return res.status(200).json({ message: updatedMessage }); - } catch (error: any) { - return res.status(500).json({ error: error.message }); - } - } - if (req.method === "DELETE") { try { const { period } = await getPeriodById(String(periodId)); @@ -102,7 +70,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { } } - res.setHeader("Allow", ["GET", "PUT", "DELETE"]); + res.setHeader("Allow", ["GET", "DELETE"]); res.status(405).end(`Method ${req.method} is not allowed.`); }; diff --git a/pages/committee/[period-id]/[committee]/index.tsx b/pages/committee/[period-id]/[committee]/index.tsx index 6c43412c..a2f9bd45 100644 --- a/pages/committee/[period-id]/[committee]/index.tsx +++ b/pages/committee/[period-id]/[committee]/index.tsx @@ -13,7 +13,6 @@ import { UserGroupIcon, } from "@heroicons/react/24/solid"; import { Tabs } from "../../../../components/Tabs"; -import SendCommitteeMessage from "../../../../components/committee/SendCommitteeMessage"; import CommitteeInterviewTimes from "../../../../components/committee/CommitteeInterviewTimes"; import LoadingPage from "../../../../components/LoadingPage"; import { changeDisplayName } from "../../../../lib/utils/toString"; @@ -153,17 +152,7 @@ const CommitteeApplicantOverview: NextPage = () => { /> ), }, - { - title: "Melding", - icon: , - content: ( - - ), - }, + { title: "Søkere", icon: , From 8b121072e44ed93cf19d4fcccfb65b5f47ba2313 Mon Sep 17 00:00:00 2001 From: fredrir Date: Tue, 13 Aug 2024 18:14:02 +0200 Subject: [PATCH 099/169] update imports --- pages/api/committees/times/[period-id]/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pages/api/committees/times/[period-id]/index.ts b/pages/api/committees/times/[period-id]/index.ts index 9cd92f1b..823ae626 100644 --- a/pages/api/committees/times/[period-id]/index.ts +++ b/pages/api/committees/times/[period-id]/index.ts @@ -1,10 +1,5 @@ import { NextApiRequest, NextApiResponse } from "next"; -import { - getCommittees, - createCommittee, - deleteCommittee, - updateCommitteeMessage, -} from "../../../../../lib/mongo/committees"; +import { createCommittee } from "../../../../../lib/mongo/committees"; import { getServerSession } from "next-auth"; import { authOptions } from "../../../auth/[...nextauth]"; import { hasSession, isInCommitee } from "../../../../../lib/utils/apiChecks"; From c60a4a6e9101ff3f79fd3002ee9f703bf56ee5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Tue, 13 Aug 2024 21:18:56 +0000 Subject: [PATCH 100/169] =?UTF-8?q?fix:=20fjern=20overskriving=20av=20komi?= =?UTF-8?q?t=C3=A9-objekter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- algorithm/bridge/fetch_applicants_and_committees.py | 1 - 1 file changed, 1 deletion(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 0b47cbb1..000611de 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -32,7 +32,6 @@ def main(): all_committees = {committee.name: committee for committee in committee_objects} applicant_objects = create_applicant_objects(applicants, all_committees) - committee_objects = create_committee_objects(committee_times) print(applicant_objects) print(committee_objects) From 7161ef3438e0ca2bb7ac1fa1a1b40a0f99558aa0 Mon Sep 17 00:00:00 2001 From: fredrir Date: Tue, 13 Aug 2024 23:56:28 +0200 Subject: [PATCH 101/169] shorten code --- pages/committees.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pages/committees.tsx b/pages/committees.tsx index 138289a4..4db5d6dc 100644 --- a/pages/committees.tsx +++ b/pages/committees.tsx @@ -73,7 +73,7 @@ const Committees = () => { (committee: owCommitteeType) => otherCommittees.includes(committee.name_short) ); - setNodeCommittees(filterNodeCommittees); + setNodeCommittees(shuffleList(filterNodeCommittees)); let filteredCommittees = owCommitteeData.filter( (committee: owCommitteeType) => @@ -81,8 +81,7 @@ const Committees = () => { !otherCommittees.includes(committee.name_short) ); - filteredCommittees = shuffleList(filteredCommittees); - setCommittees(filteredCommittees); + setCommittees(shuffleList(filteredCommittees)); }, [owCommitteeData]); useEffect(() => { From 523f1965d7ad9779b9598fa013c1b5266fef4950 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 00:16:20 +0200 Subject: [PATCH 102/169] responsive image size --- components/CommitteeAboutCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/CommitteeAboutCard.tsx b/components/CommitteeAboutCard.tsx index f2f56093..bf926820 100644 --- a/components/CommitteeAboutCard.tsx +++ b/components/CommitteeAboutCard.tsx @@ -17,7 +17,7 @@ const CommitteeAboutCard = ({ {name_long}
    From 13cfc6daa24991d6d5ef5994df1332859d85e045 Mon Sep 17 00:00:00 2001 From: henrikskog Date: Wed, 14 Aug 2024 09:32:38 +0200 Subject: [PATCH 103/169] Fix illustration not showing in safari --- pages/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/index.tsx b/pages/index.tsx index 61401f08..1d6824e3 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -45,7 +45,7 @@ const Home = () => {
    -
    +
    From 8b3b4ae46c45a79b2aa3fdc3dbecb82abd9dd5b8 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 12:42:47 +0200 Subject: [PATCH 104/169] tweaky tweak --- components/applicantoverview/ApplicantsOverview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/applicantoverview/ApplicantsOverview.tsx b/components/applicantoverview/ApplicantsOverview.tsx index 92db05e9..b48824d3 100644 --- a/components/applicantoverview/ApplicantsOverview.tsx +++ b/components/applicantoverview/ApplicantsOverview.tsx @@ -154,7 +154,7 @@ const ApplicantsOverview = ({ if (applicantsIsError) return ; return ( -
    +
    {showPeriodName && }
    From 0b285d1402297d676a419afd8d2859e48f0a382c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 11:23:09 +0000 Subject: [PATCH 105/169] cleanup: remove unused code --- algorithm/src/mip_matching/Committee.py | 9 +------ algorithm/src/mip_matching/TimeInterval.py | 26 -------------------- algorithm/src/mip_matching/match_meetings.py | 3 --- 3 files changed, 1 insertion(+), 37 deletions(-) diff --git a/algorithm/src/mip_matching/Committee.py b/algorithm/src/mip_matching/Committee.py index c24d529b..8f2c28f5 100644 --- a/algorithm/src/mip_matching/Committee.py +++ b/algorithm/src/mip_matching/Committee.py @@ -1,17 +1,10 @@ from __future__ import annotations from datetime import timedelta -import sys -print(sys.path) -print(__name__) -# sys.path.append("C:\\Users\\Jørgen Galdal\\Documents\\lokalSkoleprogrammering\\appkom\\OnlineOpptak\\algorithm\\mip_matching") from mip_matching.Applicant import Applicant +from mip_matching.TimeInterval import TimeInterval from typing import Iterator -# from typing import TYPE_CHECKING -# if TYPE_CHECKING: -# # Unngår cyclic import -from mip_matching.TimeInterval import TimeInterval class Committee: diff --git a/algorithm/src/mip_matching/TimeInterval.py b/algorithm/src/mip_matching/TimeInterval.py index 990ec4a6..afad5bb7 100644 --- a/algorithm/src/mip_matching/TimeInterval.py +++ b/algorithm/src/mip_matching/TimeInterval.py @@ -85,29 +85,3 @@ def divide_interval(interval: TimeInterval, length: timedelta) -> list[TimeInter local_end += length return result - - -""" -Dette er gammel kode som nå er flyttet til de passende komité-/søker-klassene. -Foreløpig beholdt for referanse. -""" -# class TimeIntervals: -# def __init__(self, initial_list: list[TimeInterval] = None): -# self.list: list[TimeInterval] = initial_list if initial_list else [] - -# def add(self, interval: TimeInterval): -# self.list.append(interval) - -# def recursive_intersection(self, other: TimeIntervals): -# """ -# Returnerer alle tidsintervallene i *other* som er inneholdt i et av *self* sine intervaller""" -# result = TimeIntervals() - -# for self_interval, other_interval in itertools.product(self.list, other.list): -# if self_interval.contains(other_interval): -# result.add(other_interval) - -# return result - -# def __iter__(self): -# return self.list.__iter__() \ No newline at end of file diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index 3e773d10..e8fce2e7 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -5,9 +5,6 @@ from mip_matching.Applicant import Applicant import mip -# from typing import TypedDict - - class MeetingMatch(TypedDict): """Type definition of a meeting match object""" solver_status: mip.OptimizationStatus From 3836c2d95e8bd66f15003be848de9efc367a66fa Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 13:44:25 +0200 Subject: [PATCH 106/169] display UTC date hours instead of local --- lib/mongo/applicants.ts | 21 +++++++++++++++ lib/sendInterviewTimes/formatAndSend.ts | 3 ++- .../formatInterviewEmail.ts | 12 +++++---- lib/sendInterviewTimes/formatInterviewSMS.ts | 4 +-- lib/sendInterviewTimes/sendInterviewTimes.ts | 15 ++++++++--- lib/utils/dateUtils.ts | 27 +++++++++++-------- .../send-interview-times/[period-id].ts | 12 ++++++--- 7 files changed, 69 insertions(+), 25 deletions(-) diff --git a/lib/mongo/applicants.ts b/lib/mongo/applicants.ts index 45a5133e..fa1204e6 100644 --- a/lib/mongo/applicants.ts +++ b/lib/mongo/applicants.ts @@ -84,6 +84,27 @@ export const getApplication = async ( } }; +export const getApplicationByMongoId = async ( + id: string, + periodId: string | ObjectId +) => { + try { + if (!applicants) await init(); + + const objectId = new ObjectId(id); + + const result = await applicants.findOne({ + _id: objectId, + periodId: periodId, + }); + + return { application: result, exists: !!result }; + } catch (error) { + console.error(error); + return { error: "Failed to fetch application", exists: false }; + } +}; + export const getApplications = async (periodId: string) => { try { if (!applicants) await init(); diff --git a/lib/sendInterviewTimes/formatAndSend.ts b/lib/sendInterviewTimes/formatAndSend.ts index e22afceb..cd088381 100644 --- a/lib/sendInterviewTimes/formatAndSend.ts +++ b/lib/sendInterviewTimes/formatAndSend.ts @@ -33,7 +33,7 @@ export const formatAndSendEmails = async ({ await sendEmail({ toEmails: applicantEmail, - + // toEmails: ["fhansteen@gmail.com"], subject: subject, htmlContent: emailBody, }); @@ -55,6 +55,7 @@ export const formatAndSendEmails = async ({ await sendEmail({ toEmails: committeeEmail, + // toEmails: ["fhansteen@gmail.com"], subject: subject, htmlContent: emailBody, }); diff --git a/lib/sendInterviewTimes/formatInterviewEmail.ts b/lib/sendInterviewTimes/formatInterviewEmail.ts index 23ffeeb7..19d564c3 100644 --- a/lib/sendInterviewTimes/formatInterviewEmail.ts +++ b/lib/sendInterviewTimes/formatInterviewEmail.ts @@ -25,8 +25,8 @@ export const formatApplicantInterviewEmail = ( if (committee.interviewTime.start !== "Ikke satt") { emailBody += `Tid: ${formatDateHours( - new Date(committee.interviewTime.start), - new Date(committee.interviewTime.end) + committee.interviewTime.start, + committee.interviewTime.end )}
    `; } if (committee.interviewTime.start === "Ikke satt") { @@ -47,7 +47,9 @@ export const formatCommitteeInterviewEmail = ( ) => { let emailBody = `

    Hei ${changeDisplayName( committee.committeeName - )},

    Her er deres intervjutider:

      `; + )},

      Her er deres intervjutider for ${ + committee.applicants.length + } søkere:

        `; committee.applicants.sort((a, b) => { return ( @@ -62,8 +64,8 @@ export const formatCommitteeInterviewEmail = ( if (applicant.interviewTime.start !== "Ikke satt") { emailBody += `Tid: ${formatDateHours( - new Date(applicant.interviewTime.start), - new Date(applicant.interviewTime.end) + applicant.interviewTime.start, + applicant.interviewTime.end )}
        `; } diff --git a/lib/sendInterviewTimes/formatInterviewSMS.ts b/lib/sendInterviewTimes/formatInterviewSMS.ts index e05636c4..4ece154f 100644 --- a/lib/sendInterviewTimes/formatInterviewSMS.ts +++ b/lib/sendInterviewTimes/formatInterviewSMS.ts @@ -17,8 +17,8 @@ export const formatInterviewSMS = (applicant: emailApplicantInterviewType) => { if (committee.interviewTime.start !== "Ikke satt") { phoneBody += `Tid: ${formatDateHours( - new Date(committee.interviewTime.start), - new Date(committee.interviewTime.end) + committee.interviewTime.start, + committee.interviewTime.end )}\n`; } diff --git a/lib/sendInterviewTimes/sendInterviewTimes.ts b/lib/sendInterviewTimes/sendInterviewTimes.ts index 68c791bf..1dc45294 100644 --- a/lib/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/sendInterviewTimes/sendInterviewTimes.ts @@ -1,4 +1,4 @@ -import { getApplication } from "../mongo/applicants"; +import { getApplication, getApplicationByMongoId } from "../mongo/applicants"; import { getCommitteesByPeriod } from "../mongo/committees"; import { getInterviewsByPeriod } from "../mongo/interviews"; import { getPeriodById, markInterviewsSentByPeriodId } from "../mongo/periods"; @@ -22,12 +22,13 @@ export const sendOutInterviewTimes = async ({ }) => { try { const { period } = await getPeriodById(periodId); - if (!period) { + console.log("Failed to find period"); return { error: "Failed to find period" }; } if (period.hasSentInterviewTimes) { + console.log("Interview times already sent"); return { error: "Interview times already sent" }; } @@ -37,6 +38,7 @@ export const sendOutInterviewTimes = async ({ } const committeeInterviewTimes = committeeInterviewTimesData.result || []; + const committeeEmails = await fetchCommitteeEmails(); const fetchedAlgorithmData = await getInterviewsByPeriod(periodId); @@ -50,6 +52,8 @@ export const sendOutInterviewTimes = async ({ committeeInterviewTimes ); + console.log(applicantsToEmail); + const committeesToEmail = formatCommittees(applicantsToEmail); await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); @@ -69,7 +73,12 @@ const formatApplicants = async ( const applicantsToEmailMap: emailApplicantInterviewType[] = []; for (const app of algorithmData) { - const dbApplication = await getApplication(app.applicantId, periodId); + const dbApplication = await getApplicationByMongoId( + app.applicantId, + periodId + ); + + console.log(dbApplication); if (!dbApplication || !dbApplication.application) continue; diff --git a/lib/utils/dateUtils.ts b/lib/utils/dateUtils.ts index f30587f1..574686d5 100644 --- a/lib/utils/dateUtils.ts +++ b/lib/utils/dateUtils.ts @@ -11,16 +11,19 @@ export const formatDate = (inputDate: undefined | Date) => { }; export const formatDateHours = ( - start: undefined | Date, - end: undefined | Date + start: undefined | string, + end: undefined | string ) => { - const startDate = new Date(start || ""); - const endDate = new Date(end || ""); + const startDate = start ? new Date(Date.parse(start)) : undefined; + const endDate = end ? new Date(Date.parse(end)) : undefined; - const startHour = startDate.getHours().toString().padStart(2, "0"); - const startMinute = startDate.getMinutes().toString().padStart(2, "0"); - const endHour = endDate.getHours().toString().padStart(2, "0"); - const endMinute = endDate.getMinutes().toString().padStart(2, "0"); + const startHour = + startDate?.getUTCHours().toString().padStart(2, "0") || "00"; + const startMinute = + startDate?.getUTCMinutes().toString().padStart(2, "0") || "00"; + const endHour = endDate?.getUTCHours().toString().padStart(2, "0") || "00"; + const endMinute = + endDate?.getUTCMinutes().toString().padStart(2, "0") || "00"; return `${formatDateNorwegian( startDate @@ -28,9 +31,11 @@ export const formatDateHours = ( }; export const formatDateNorwegian = (inputDate?: Date): string => { - const date = new Date(inputDate || ""); + if (!inputDate) return ""; + + const date = new Date(Date.parse(inputDate.toISOString())); - const day = date.getDate().toString().padStart(2); + const day = date.getUTCDate().toString().padStart(2, "0"); const monthsNorwegian = [ "jan", "feb", @@ -45,7 +50,7 @@ export const formatDateNorwegian = (inputDate?: Date): string => { "nov", "des", ]; - const month = monthsNorwegian[date.getMonth()]; + const month = monthsNorwegian[date.getUTCMonth()]; return `${day}. ${month}`; }; diff --git a/pages/api/periods/send-interview-times/[period-id].ts b/pages/api/periods/send-interview-times/[period-id].ts index 640d14dd..94a6f8aa 100644 --- a/pages/api/periods/send-interview-times/[period-id].ts +++ b/pages/api/periods/send-interview-times/[period-id].ts @@ -1,17 +1,23 @@ import { NextApiRequest, NextApiResponse } from "next"; import { sendOutInterviewTimes } from "../../../../lib/sendInterviewTimes/sendInterviewTimes"; -import { isAdmin } from "../../../../lib/utils/apiChecks"; +import { hasSession, isAdmin } from "../../../../lib/utils/apiChecks"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../../auth/[...nextauth]"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { - const periodId = req.query.id; + const periodId = req.query["period-id"]; if (typeof periodId !== "string") { return res.status(400).json({ error: "Invalid ID format" }); } + const session = await getServerSession(req, res, authOptions); + + if (!hasSession(res, session)) return; + try { if (req.method === "POST") { - if (!isAdmin(res, req)) { + if (!isAdmin(res, session)) { return res.status(401).json({ error: "Unauthorized" }); } From d06bb840198c79c36188455a513c7653f295a594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 12:10:55 +0000 Subject: [PATCH 107/169] fix: make committee objects use correct interview_length --- algorithm/bridge/fetch_applicants_and_committees.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 000611de..23e7de67 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -1,6 +1,6 @@ from pymongo import MongoClient from dotenv import load_dotenv -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import os import certifi from typing import List, Dict @@ -164,7 +164,7 @@ def create_applicant_objects(applicants_data: List[dict], all_committees: dict[s def create_committee_objects(committee_data: List[dict]) -> set[Committee]: committees = set() for data in committee_data: - committee = Committee(name=data['committee']) + committee = Committee(name=data['committee'], interview_length=timedelta(minutes=int(data["timeslot"]))) for interval_data in data['availabletimes']: interval = TimeInterval( start=datetime.fromisoformat(interval_data['start'].replace("Z", "+00:00")), From 66953a87eb768c03859d0bb09e7322d266156cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 12:12:31 +0000 Subject: [PATCH 108/169] refactor: simplify code --- algorithm/bridge/fetch_applicants_and_committees.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 23e7de67..f5c9f3ac 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -79,9 +79,7 @@ def connect_to_db(collection_name): def fetch_periods(): collection, client = connect_to_db("periods") - periods = collection.find() - - periods = list(periods) + periods = list(collection.find()) client.close() @@ -90,9 +88,7 @@ def fetch_periods(): def fetch_applicants(periodId): collection, client = connect_to_db("applications") - applicants = collection.find({"periodId": periodId}) - - applicants = list(applicants) + applicants = list(collection.find({"periodId": periodId})) client.close() @@ -101,9 +97,7 @@ def fetch_applicants(periodId): def fetch_committee_times(periodId): collection, client = connect_to_db("committees") - committee_times = collection.find({"periodId": periodId}) - - committee_times = list(committee_times) + committee_times = list(collection.find({"periodId": periodId})) client.close() From d10d6103ad9a55130eb432c8c28d566406feb561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 12:13:10 +0000 Subject: [PATCH 109/169] style: formatting --- algorithm/bridge/fetch_applicants_and_committees.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index f5c9f3ac..b276bedd 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -26,7 +26,6 @@ def main(): applicants = fetch_applicants(periodId) committee_times = fetch_committee_times(periodId) - committee_objects = create_committee_objects(committee_times) all_committees = {committee.name: committee for committee in committee_objects} @@ -41,7 +40,6 @@ def main(): send_to_db(match_result, applicants, periodId) return match_result - def send_to_db(match_result: MeetingMatch, applicants: List[dict], periodId): load_dotenv() formatted_results = format_match_results(match_result, applicants, periodId) @@ -60,8 +58,6 @@ def send_to_db(match_result: MeetingMatch, applicants: List[dict], periodId): client.close() - - def connect_to_db(collection_name): load_dotenv() @@ -110,7 +106,6 @@ def format_match_results(match_results: MeetingMatch, applicants: List[dict], pe for result in match_results['matchings']: applicant_id = str(result[0]) - if applicant_id not in transformed_results: transformed_results[applicant_id] = { "periodId": periodId, @@ -131,7 +126,6 @@ def format_match_results(match_results: MeetingMatch, applicants: List[dict], pe return list(transformed_results.values()) - def create_applicant_objects(applicants_data: List[dict], all_committees: dict[str, Committee]) -> set[Applicant]: applicants = set() for data in applicants_data: From 1d6134a28b976782e12878f1d150941a3f649d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 14:18:05 +0000 Subject: [PATCH 110/169] feat: add applicant buffer --- algorithm/src/Modellering.md | 9 +++++++ algorithm/src/mip_matching/TimeInterval.py | 3 +++ algorithm/src/mip_matching/match_meetings.py | 26 ++++++++++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/algorithm/src/Modellering.md b/algorithm/src/Modellering.md index 3fea01e0..11f127e7 100644 --- a/algorithm/src/Modellering.md +++ b/algorithm/src/Modellering.md @@ -50,3 +50,12 @@ For alle `k`: ## Mål Maksimere `sum(m(p, k, t))` for alle `p`, `k` og `t` + +### Sekundærmål + +- La det være et gitt mellomrom mellom hvert intervju for samme person. + +- [Ikke enda implementert] La det være færrest mulig og minst mulig mellomrom mellom intervjuene for komitéene. + + + diff --git a/algorithm/src/mip_matching/TimeInterval.py b/algorithm/src/mip_matching/TimeInterval.py index f2156d5e..fdc601f6 100644 --- a/algorithm/src/mip_matching/TimeInterval.py +++ b/algorithm/src/mip_matching/TimeInterval.py @@ -64,6 +64,9 @@ def get_contained_slots(self, slots: list[TimeInterval]): def divide(self, length: timedelta) -> list[TimeInterval]: return TimeInterval.divide_interval(self, length) + def is_within_distance(self, other: TimeInterval, distance: timedelta) -> bool: + return (self.end <= other.start and self.end + distance > other.start) or (other.end <= self.start and other.end + distance > self.start) + @staticmethod def divide_interval(interval: TimeInterval, length: timedelta) -> list[TimeInterval]: """ diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index 9bbd512e..813b4955 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -5,7 +5,8 @@ from mip_matching.Applicant import Applicant import mip -# from typing import TypedDict +from datetime import timedelta +from itertools import combinations class MeetingMatch(TypedDict): @@ -58,8 +59,29 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me # type: ignore if (applicant, committee, interval) in m) <= 1 + # Legger til målsetning om at man skal ha mellomrom mellom perioder + distance_variables = set() + + APPLICANT_BUFFER_MODEL_WEIGHT = 0.001 + APPLICANT_BUFFER_LENGTH = timedelta(minutes=15) + + for applicant in applicants: + potential_committees_with_intervals: set[tuple[Committee, TimeInterval]] = set() + for applicant_candidate, committee, interval in m: + if applicant == applicant_candidate: + potential_committees_with_intervals.add((committee, interval)) + + distance_variables.add( + mip.xsum( + APPLICANT_BUFFER_MODEL_WEIGHT * m[(applicant, *a)] * m[(applicant, *b)] + for a, b in combinations(potential_committees_with_intervals) # type: ignore + if a[0] != b[0] + and a[1].is_within_distance(b[1], APPLICANT_BUFFER_LENGTH) + ) + ) + # Setter mål til å være maksimering av antall møter - model.objective = mip.maximize(mip.xsum(m.values())) + model.objective = mip.maximize(mip.xsum(m.values()) + mip.xsum(distance_variables)) # Kjør optimeringen solver_status = model.optimize() From 23c73e6cfdfbb42dd4e55ee3aadfb9477b19d1c3 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 16:33:32 +0200 Subject: [PATCH 111/169] foreach instead of for loop --- lib/sendInterviewTimes/formatAndSend.ts | 64 +++++++++++++------------ 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/lib/sendInterviewTimes/formatAndSend.ts b/lib/sendInterviewTimes/formatAndSend.ts index cd088381..352f7f1d 100644 --- a/lib/sendInterviewTimes/formatAndSend.ts +++ b/lib/sendInterviewTimes/formatAndSend.ts @@ -23,43 +23,47 @@ export const formatAndSendEmails = async ({ }: sendInterviewTimesProps) => { try { // Send email to each applicant - for (const applicant of applicantsToEmail) { - const typedApplicant: emailApplicantInterviewType = applicant; - const applicantEmail = [typedApplicant.applicantEmail]; - const subject = `Hei, ${typedApplicant.applicantName}, her er dine intervjutider:`; + applicantsToEmail.forEach( + async (applicant: emailApplicantInterviewType) => { + const typedApplicant: emailApplicantInterviewType = applicant; + const applicantEmail = [typedApplicant.applicantEmail]; + const subject = `Hei, ${typedApplicant.applicantName}, her er dine intervjutider:`; - const emailBody = formatApplicantInterviewEmail(typedApplicant); - const phoneBody = formatInterviewSMS(typedApplicant); + const emailBody = formatApplicantInterviewEmail(typedApplicant); + const phoneBody = formatInterviewSMS(typedApplicant); - await sendEmail({ - toEmails: applicantEmail, - // toEmails: ["fhansteen@gmail.com"], - subject: subject, - htmlContent: emailBody, - }); + await sendEmail({ + toEmails: applicantEmail, + // toEmails: ["fhansteen@gmail.com"], + subject: subject, + htmlContent: emailBody, + }); - let toPhoneNumber = "+47"; - toPhoneNumber += typedApplicant.applicantPhone; - sendSMS(toPhoneNumber, phoneBody); - } + let toPhoneNumber = "+47"; + toPhoneNumber += typedApplicant.applicantPhone; + sendSMS(toPhoneNumber, phoneBody); + } + ); // Send email to each committee - for (const committee of committeesToEmail) { - const typedCommittee: emailCommitteeInterviewType = committee; - const committeeEmail = [typedCommittee.committeeEmail]; - const subject = `${changeDisplayName( - typedCommittee.committeeName - )} sine intervjutider for ${typedCommittee.period_name}`; + committeesToEmail.forEach( + async (committee: emailCommitteeInterviewType) => { + const typedCommittee: emailCommitteeInterviewType = committee; + const committeeEmail = [typedCommittee.committeeEmail]; + const subject = `${changeDisplayName( + typedCommittee.committeeName + )} sine intervjutider for ${typedCommittee.period_name}`; - const emailBody = formatCommitteeInterviewEmail(typedCommittee); + const emailBody = formatCommitteeInterviewEmail(typedCommittee); - await sendEmail({ - toEmails: committeeEmail, - // toEmails: ["fhansteen@gmail.com"], - subject: subject, - htmlContent: emailBody, - }); - } + await sendEmail({ + toEmails: committeeEmail, + // toEmails: ["fhansteen@gmail.com"], + subject: subject, + htmlContent: emailBody, + }); + } + ); } catch (error) { return { error: "Failed to send out interview times" }; } From 4dc5062355f73582540daf9826c5a1b2661653fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 14:33:55 +0000 Subject: [PATCH 112/169] fix: make weight negative --- algorithm/src/mip_matching/match_meetings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index cb4f97de..6514bd06 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -62,7 +62,7 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me # Legger til målsetning om at man skal ha mellomrom mellom perioder distance_variables = set() - APPLICANT_BUFFER_MODEL_WEIGHT = 0.001 + APPLICANT_BUFFER_MODEL_WEIGHT = -0.001 APPLICANT_BUFFER_LENGTH = timedelta(minutes=15) for applicant in applicants: @@ -73,8 +73,8 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me distance_variables.add( mip.xsum( - APPLICANT_BUFFER_MODEL_WEIGHT * m[(applicant, *a)] * m[(applicant, *b)] - for a, b in combinations(potential_committees_with_intervals) # type: ignore + APPLICANT_BUFFER_MODEL_WEIGHT * (m[(applicant, *a)] and m[(applicant, *b)]) + for a, b in combinations(potential_committees_with_intervals, r=2) # type: ignore if a[0] != b[0] and a[1].is_within_distance(b[1], APPLICANT_BUFFER_LENGTH) ) From 49a6037606b2033ca364ba6f474cafdc53806b28 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 16:34:48 +0200 Subject: [PATCH 113/169] simplify logic --- lib/sendInterviewTimes/formatAndSend.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/sendInterviewTimes/formatAndSend.ts b/lib/sendInterviewTimes/formatAndSend.ts index 352f7f1d..49471850 100644 --- a/lib/sendInterviewTimes/formatAndSend.ts +++ b/lib/sendInterviewTimes/formatAndSend.ts @@ -25,12 +25,11 @@ export const formatAndSendEmails = async ({ // Send email to each applicant applicantsToEmail.forEach( async (applicant: emailApplicantInterviewType) => { - const typedApplicant: emailApplicantInterviewType = applicant; - const applicantEmail = [typedApplicant.applicantEmail]; - const subject = `Hei, ${typedApplicant.applicantName}, her er dine intervjutider:`; + const applicantEmail = [applicant.applicantEmail]; + const subject = `Hei, ${applicant.applicantName}, her er dine intervjutider:`; - const emailBody = formatApplicantInterviewEmail(typedApplicant); - const phoneBody = formatInterviewSMS(typedApplicant); + const emailBody = formatApplicantInterviewEmail(applicant); + const phoneBody = formatInterviewSMS(applicant); await sendEmail({ toEmails: applicantEmail, @@ -40,7 +39,7 @@ export const formatAndSendEmails = async ({ }); let toPhoneNumber = "+47"; - toPhoneNumber += typedApplicant.applicantPhone; + toPhoneNumber += applicant.applicantPhone; sendSMS(toPhoneNumber, phoneBody); } ); @@ -48,13 +47,12 @@ export const formatAndSendEmails = async ({ // Send email to each committee committeesToEmail.forEach( async (committee: emailCommitteeInterviewType) => { - const typedCommittee: emailCommitteeInterviewType = committee; - const committeeEmail = [typedCommittee.committeeEmail]; + const committeeEmail = [committee.committeeEmail]; const subject = `${changeDisplayName( - typedCommittee.committeeName - )} sine intervjutider for ${typedCommittee.period_name}`; + committee.committeeName + )} sine intervjutider for ${committee.period_name}`; - const emailBody = formatCommitteeInterviewEmail(typedCommittee); + const emailBody = formatCommitteeInterviewEmail(committee); await sendEmail({ toEmails: committeeEmail, From 9992e81994ecea3202a9c6e98d10676caf664e61 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 16:35:23 +0200 Subject: [PATCH 114/169] shorten code --- lib/sendInterviewTimes/formatAndSend.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/sendInterviewTimes/formatAndSend.ts b/lib/sendInterviewTimes/formatAndSend.ts index 49471850..8280acf0 100644 --- a/lib/sendInterviewTimes/formatAndSend.ts +++ b/lib/sendInterviewTimes/formatAndSend.ts @@ -38,8 +38,7 @@ export const formatAndSendEmails = async ({ htmlContent: emailBody, }); - let toPhoneNumber = "+47"; - toPhoneNumber += applicant.applicantPhone; + let toPhoneNumber = "+47" + applicant.applicantPhone; sendSMS(toPhoneNumber, phoneBody); } ); From 2d51198bc893ea1bbe4a354c7b30aeb08f14f440 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 16:35:40 +0200 Subject: [PATCH 115/169] remove comment --- lib/sendInterviewTimes/formatAndSend.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/sendInterviewTimes/formatAndSend.ts b/lib/sendInterviewTimes/formatAndSend.ts index 8280acf0..2d63a573 100644 --- a/lib/sendInterviewTimes/formatAndSend.ts +++ b/lib/sendInterviewTimes/formatAndSend.ts @@ -33,7 +33,6 @@ export const formatAndSendEmails = async ({ await sendEmail({ toEmails: applicantEmail, - // toEmails: ["fhansteen@gmail.com"], subject: subject, htmlContent: emailBody, }); @@ -55,7 +54,6 @@ export const formatAndSendEmails = async ({ await sendEmail({ toEmails: committeeEmail, - // toEmails: ["fhansteen@gmail.com"], subject: subject, htmlContent: emailBody, }); From 59be95bae23043b0b1d8a83211004ae2e17a2a7e Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 16:37:03 +0200 Subject: [PATCH 116/169] fix merge --- lib/mongo/committees.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/mongo/committees.ts b/lib/mongo/committees.ts index 68505034..ff8d01d3 100644 --- a/lib/mongo/committees.ts +++ b/lib/mongo/committees.ts @@ -72,6 +72,16 @@ export const getCommittee = async (id: string) => { } }; +export const getCommitteesByPeriod = async (periodId: string) => { + try { + if (!committees) await init(); + const result = await committees.find({ periodId: periodId }).toArray(); + return { result }; + } catch (error) { + return { error: "Failed to fetch committees" }; + } +}; + export const createCommittee = async ( committeeData: commiteeType, userCommittes: string[], From ee9a848375563ffc4bb3868f399e6488a98c2483 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 16:42:06 +0200 Subject: [PATCH 117/169] remove unused comitteType --- lib/mongo/committees.ts | 6 +++--- lib/types/types.ts | 14 -------------- lib/utils/validators.ts | 4 ++-- pages/api/committees/times/[period-id]/index.ts | 4 ++-- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/lib/mongo/committees.ts b/lib/mongo/committees.ts index ff8d01d3..e978fef6 100644 --- a/lib/mongo/committees.ts +++ b/lib/mongo/committees.ts @@ -1,10 +1,10 @@ import { Collection, Db, MongoClient, ObjectId, UpdateResult } from "mongodb"; import clientPromise from "./mongodb"; -import { commiteeType } from "../types/types"; +import { committeeInterviewType } from "../types/types"; let client: MongoClient; let db: Db; -let committees: Collection; +let committees: Collection; async function init() { if (db) return; @@ -83,7 +83,7 @@ export const getCommitteesByPeriod = async (periodId: string) => { }; export const createCommittee = async ( - committeeData: commiteeType, + committeeData: committeeInterviewType, userCommittes: string[], periodId: string ) => { diff --git a/lib/types/types.ts b/lib/types/types.ts index 5776d951..f2022a1e 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -4,20 +4,6 @@ export type DeepPartial = { [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; }; -export type commiteeType = { - periodId: string; - period_name: string; - committee: string; - availableTimes: [ - { - start: string; - end: string; - }, - ]; - timeslot: string; - message: string; -}; - export type preferencesType = { first: string; second: string; diff --git a/lib/utils/validators.ts b/lib/utils/validators.ts index 5f0b4a61..1b6bf1ac 100644 --- a/lib/utils/validators.ts +++ b/lib/utils/validators.ts @@ -1,6 +1,6 @@ import { applicantType, - commiteeType, + committeeInterviewType, periodType, preferencesType, } from "../types/types"; @@ -89,7 +89,7 @@ export const isApplicantType = ( ); }; -export const isCommitteeType = (data: any): data is commiteeType => { +export const isCommitteeType = (data: any): data is committeeInterviewType => { const hasBasicFields = typeof data.period_name === "string" && typeof data.committee === "string" && diff --git a/pages/api/committees/times/[period-id]/index.ts b/pages/api/committees/times/[period-id]/index.ts index 823ae626..de2259ac 100644 --- a/pages/api/committees/times/[period-id]/index.ts +++ b/pages/api/committees/times/[period-id]/index.ts @@ -7,8 +7,8 @@ import { isCommitteeType, validateCommittee, } from "../../../../../lib/utils/validators"; -import { commiteeType } from "../../../../../lib/types/types"; import { getPeriodById } from "../../../../../lib/mongo/periods"; +import { committeeInterviewType } from "../../../../../lib/types/types"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getServerSession(req, res, authOptions); @@ -24,7 +24,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { if (!isInCommitee(res, session)) return; if (req.method === "POST") { - const committeeData: commiteeType = req.body; + const committeeData: committeeInterviewType = req.body; if (!isCommitteeType(req.body)) { return res.status(400).json({ error: "Invalid data format" }); From c3fe19f184f5c2daa78c837e6b02146e231f9e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 14:43:27 +0000 Subject: [PATCH 118/169] docs: la til kommentarer --- algorithm/src/mip_matching/match_meetings.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index 6514bd06..f8c6fe2c 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -9,6 +9,13 @@ from itertools import combinations +# Mål på hvor mye buffer skal vektlegges i matchingen (-1 tilsvarer vekten av ett intervju) +APPLICANT_BUFFER_MODEL_WEIGHT = 0.001 + +# Hvor stort buffer man ønsker å ha mellom intervjuene +APPLICANT_BUFFER_LENGTH = timedelta(minutes=15) + + class MeetingMatch(TypedDict): """Type definition of a meeting match object""" solver_status: mip.OptimizationStatus @@ -59,11 +66,8 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me # type: ignore if (applicant, committee, interval) in m) <= 1 - # Legger til målsetning om at man skal ha mellomrom mellom perioder + # Legger til sekundærmålsetning om at man skal ha mellomrom mellom perioder distance_variables = set() - - APPLICANT_BUFFER_MODEL_WEIGHT = -0.001 - APPLICANT_BUFFER_LENGTH = timedelta(minutes=15) for applicant in applicants: potential_committees_with_intervals: set[tuple[Committee, TimeInterval]] = set() @@ -71,16 +75,18 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me if applicant == applicant_candidate: potential_committees_with_intervals.add((committee, interval)) + # Trekker fra vekten dersom det prøves å planlegges et møte med mindre buffer enn ønsket distance_variables.add( mip.xsum( - APPLICANT_BUFFER_MODEL_WEIGHT * (m[(applicant, *a)] and m[(applicant, *b)]) + -APPLICANT_BUFFER_MODEL_WEIGHT * (m[(applicant, *a)] and m[(applicant, *b)]) for a, b in combinations(potential_committees_with_intervals, r=2) # type: ignore - if a[0] != b[0] - and a[1].is_within_distance(b[1], APPLICANT_BUFFER_LENGTH) + if a[0] != b[0] # Ikke intervju med samme komite (blir ordnet over) + and a[1].is_within_distance(b[1], APPLICANT_BUFFER_LENGTH) # Intervjuene er innenfor bufferlengden ) ) # Setter mål til å være maksimering av antall møter + # med sekundærmål om buffer model.objective = mip.maximize(mip.xsum(m.values()) + mip.xsum(distance_variables)) # Kjør optimeringen From 0d2443b487b043a4901a960b4d6ae19b4c6fdb24 Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 14 Aug 2024 16:50:34 +0200 Subject: [PATCH 119/169] more compact code --- lib/sendInterviewTimes/formatAndSend.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/sendInterviewTimes/formatAndSend.ts b/lib/sendInterviewTimes/formatAndSend.ts index 2d63a573..033c88e8 100644 --- a/lib/sendInterviewTimes/formatAndSend.ts +++ b/lib/sendInterviewTimes/formatAndSend.ts @@ -25,20 +25,16 @@ export const formatAndSendEmails = async ({ // Send email to each applicant applicantsToEmail.forEach( async (applicant: emailApplicantInterviewType) => { - const applicantEmail = [applicant.applicantEmail]; - const subject = `Hei, ${applicant.applicantName}, her er dine intervjutider:`; - - const emailBody = formatApplicantInterviewEmail(applicant); - const phoneBody = formatInterviewSMS(applicant); - await sendEmail({ - toEmails: applicantEmail, - subject: subject, - htmlContent: emailBody, + toEmails: [applicant.applicantEmail], + subject: `Hei, ${applicant.applicantName}, her er dine intervjutider:`, + htmlContent: formatApplicantInterviewEmail(applicant), }); - let toPhoneNumber = "+47" + applicant.applicantPhone; - sendSMS(toPhoneNumber, phoneBody); + sendSMS( + `+47${applicant.applicantPhone}`, + formatInterviewSMS(applicant) + ); } ); From 49e296e2c7478a726a6ed1e460f5ddccfa4237c2 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 16:51:58 +0200 Subject: [PATCH 120/169] change criteria for running algorithm --- algorithm/bridge/fetch_applicants_and_committees.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index b276bedd..7a110ae8 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -15,14 +15,13 @@ def main(): for period in periods: periodId = str(period["_id"]) - interview_end = datetime.fromisoformat(period["interviewPeriod"]["end"].replace("Z", "+00:00")) application_end = datetime.fromisoformat(period["applicationPeriod"]["end"].replace("Z", "+00:00")) now = datetime.now(timezone.utc) #or period["name"] == "Juli Opptak" - if (application_end > now and period["hasSentInterviewTimes"] == False and interview_end < now) or period["name"] == "FAKE TEST OPPTAK!": + if (application_end < now and period["hasSentInterviewTimes"] == False): applicants = fetch_applicants(periodId) committee_times = fetch_committee_times(periodId) From ba3ac351a276a735d26c10d4f4a924028f339766 Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 14 Aug 2024 16:52:18 +0200 Subject: [PATCH 121/169] more compact code --- lib/sendInterviewTimes/formatAndSend.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/sendInterviewTimes/formatAndSend.ts b/lib/sendInterviewTimes/formatAndSend.ts index 033c88e8..72e12033 100644 --- a/lib/sendInterviewTimes/formatAndSend.ts +++ b/lib/sendInterviewTimes/formatAndSend.ts @@ -41,17 +41,14 @@ export const formatAndSendEmails = async ({ // Send email to each committee committeesToEmail.forEach( async (committee: emailCommitteeInterviewType) => { - const committeeEmail = [committee.committeeEmail]; const subject = `${changeDisplayName( committee.committeeName )} sine intervjutider for ${committee.period_name}`; - const emailBody = formatCommitteeInterviewEmail(committee); - await sendEmail({ - toEmails: committeeEmail, + toEmails: [committee.committeeEmail], subject: subject, - htmlContent: emailBody, + htmlContent: formatCommitteeInterviewEmail(committee), }); } ); From f0dd55dddb82cca7e8677e8cb69402ed2da0776b Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 14 Aug 2024 16:53:15 +0200 Subject: [PATCH 122/169] remove unnecesary shit --- lib/sendInterviewTimes/sendInterviewTimes.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/sendInterviewTimes/sendInterviewTimes.ts b/lib/sendInterviewTimes/sendInterviewTimes.ts index 1dc45294..a61e2c33 100644 --- a/lib/sendInterviewTimes/sendInterviewTimes.ts +++ b/lib/sendInterviewTimes/sendInterviewTimes.ts @@ -1,4 +1,4 @@ -import { getApplication, getApplicationByMongoId } from "../mongo/applicants"; +import { getApplicationByMongoId } from "../mongo/applicants"; import { getCommitteesByPeriod } from "../mongo/committees"; import { getInterviewsByPeriod } from "../mongo/interviews"; import { getPeriodById, markInterviewsSentByPeriodId } from "../mongo/periods"; @@ -9,7 +9,6 @@ import { emailCommitteeInterviewType, periodType, algorithmType, - preferencesType, committeePreferenceType, } from "../types/types"; import { fetchCommitteeEmails } from "./fetchFunctions"; @@ -23,12 +22,10 @@ export const sendOutInterviewTimes = async ({ try { const { period } = await getPeriodById(periodId); if (!period) { - console.log("Failed to find period"); return { error: "Failed to find period" }; } if (period.hasSentInterviewTimes) { - console.log("Interview times already sent"); return { error: "Interview times already sent" }; } @@ -52,8 +49,6 @@ export const sendOutInterviewTimes = async ({ committeeInterviewTimes ); - console.log(applicantsToEmail); - const committeesToEmail = formatCommittees(applicantsToEmail); await formatAndSendEmails({ committeesToEmail, applicantsToEmail }); @@ -78,8 +73,6 @@ const formatApplicants = async ( periodId ); - console.log(dbApplication); - if (!dbApplication || !dbApplication.application) continue; const preferencesCommittees: string[] = From cbd0a515df4cede718e928274691df29c66a5002 Mon Sep 17 00:00:00 2001 From: fredrir Date: Wed, 14 Aug 2024 16:53:31 +0200 Subject: [PATCH 123/169] remove comment --- algorithm/bridge/fetch_applicants_and_committees.py | 1 - 1 file changed, 1 deletion(-) diff --git a/algorithm/bridge/fetch_applicants_and_committees.py b/algorithm/bridge/fetch_applicants_and_committees.py index 7a110ae8..e50a4662 100644 --- a/algorithm/bridge/fetch_applicants_and_committees.py +++ b/algorithm/bridge/fetch_applicants_and_committees.py @@ -100,7 +100,6 @@ def fetch_committee_times(periodId): def format_match_results(match_results: MeetingMatch, applicants: List[dict], periodId) -> List[Dict]: transformed_results = {} - # applicant_dict = {str(applicant['_id']): {'name': applicant['name'], 'email': applicant['email'], 'phone': applicant['phone']} for applicant in applicants} for result in match_results['matchings']: applicant_id = str(result[0]) From f692aad9a73e48e96da642a17969d2eab4d96e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 17:36:46 +0200 Subject: [PATCH 124/169] Update algorithm.yml Runs check on every file in algorithm, not just at first sublevel --- .github/workflows/algorithm.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/algorithm.yml b/.github/workflows/algorithm.yml index 96ba20c3..ce22a1e0 100644 --- a/.github/workflows/algorithm.yml +++ b/.github/workflows/algorithm.yml @@ -1,10 +1,10 @@ on: push: paths: - - "algorithm/*" + - "algorithm/**" pull_request: paths: - - "algorithm/*" + - "algorithm/**" workflow_dispatch: jobs: From 56a93c4eb80a0ccfaf3bb380b471100d8c87821f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 19:09:24 +0000 Subject: [PATCH 125/169] test: check if no interviews overlap for same applicant --- algorithm/tests/mip_test.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/algorithm/tests/mip_test.py b/algorithm/tests/mip_test.py index a8b3a9f8..ec173ea2 100644 --- a/algorithm/tests/mip_test.py +++ b/algorithm/tests/mip_test.py @@ -1,6 +1,5 @@ from __future__ import annotations from datetime import datetime, timedelta, date, time -# import ..algorithm.mip_matching.core.Applicant.py as applicant from mip_matching.TimeInterval import TimeInterval from mip_matching.Committee import Committee @@ -12,6 +11,7 @@ import unittest import random +from itertools import combinations def print_matchings(committees: list[Committee], @@ -59,6 +59,20 @@ def check_constraints(self, matchings: list[tuple[Applicant, Committee, TimeInte self.assertGreaterEqual(committee.get_capacity(interval), load, f"Constraint \"Number of interviews per slot per committee cannot exceed capacity\" failed for Committee {committee} and interval {interval}") + # Overlapping interviews per applicant + interviews_per_applicant: dict[Applicant, + set[tuple[Committee, TimeInterval]]] = {} + for applicant, committee, interval in matchings: + if applicant not in interviews_per_applicant: + interviews_per_applicant[applicant] = set() + + interviews_per_applicant[applicant].add((committee, interval)) + + for applicant, interviews in interviews_per_applicant.items(): + for interview_a, interview_b in combinations(interviews, r=2): + self.assertFalse(interview_a[1].intersects(interview_b[1]), f"Constraint \"Applicant cannot have time-overlapping interviews\" failed for { + applicant}'s interviews with {interview_a[0]} ({interview_a[1]}) and {interview_b[0]} ({interview_b[1]})") + def test_fixed_small(self): """Small, fixed test with all capacities set to one""" From 18bf0b3a32f4e2084bff29ed83f2ed130cad24a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 19:49:45 +0000 Subject: [PATCH 126/169] feat: don't allow overlapping interviews for same applicant --- algorithm/src/mip_matching/match_meetings.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index e8fce2e7..fe2c0551 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -5,6 +5,9 @@ from mip_matching.Applicant import Applicant import mip +from itertools import permutations + + class MeetingMatch(TypedDict): """Type definition of a meeting match object""" solver_status: mip.OptimizationStatus @@ -43,17 +46,15 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me # Legger inn begrensninger for at en person kun kan ha ett intervju på hvert tidspunkt for applicant in applicants: - potential_intervals = set() + potential_interviews: set[tuple[Committee, TimeInterval]] = set() for applicant_candidate, committee, interval in m: if applicant == applicant_candidate: - potential_intervals.add(interval) - - for interval in potential_intervals: + potential_interviews.add((committee, interval)) - model += mip.xsum(m[(applicant, committee, interval)] - for committee in applicant.get_committees() - # type: ignore - if (applicant, committee, interval) in m) <= 1 + for interview_a, interview_b in permutations(potential_interviews, r=2): + if interview_a[1].intersects(interview_b[1]): + model += m[(applicant, *interview_a)] + \ + m[(applicant, *interview_b)] <= 1 # Setter mål til å være maksimering av antall møter model.objective = mip.maximize(mip.xsum(m.values())) @@ -80,4 +81,4 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me "matchings": matchings, } - return match_object \ No newline at end of file + return match_object From c0de4f9873cb1fac6a5e7c5d69e5da948198e2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 19:50:51 +0000 Subject: [PATCH 127/169] docs: comments --- algorithm/src/mip_matching/match_meetings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index fe2c0551..af8fc3b7 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -44,7 +44,7 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me # type: ignore for interval in applicant.get_fitting_committee_slots(committee)) <= 1 - # Legger inn begrensninger for at en person kun kan ha ett intervju på hvert tidspunkt + # Legger inn begrensninger for at en søker ikke kan ha overlappende intervjutider for applicant in applicants: potential_interviews: set[tuple[Committee, TimeInterval]] = set() for applicant_candidate, committee, interval in m: From 8f8d5682986695c6338c326f602b3587ef7dc745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Wed, 14 Aug 2024 19:53:58 +0000 Subject: [PATCH 128/169] refactor: small optimization --- algorithm/src/mip_matching/match_meetings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index af8fc3b7..ab5b8372 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -5,7 +5,7 @@ from mip_matching.Applicant import Applicant import mip -from itertools import permutations +from itertools import combinations class MeetingMatch(TypedDict): @@ -51,7 +51,7 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me if applicant == applicant_candidate: potential_interviews.add((committee, interval)) - for interview_a, interview_b in permutations(potential_interviews, r=2): + for interview_a, interview_b in combinations(potential_interviews, r=2): if interview_a[1].intersects(interview_b[1]): model += m[(applicant, *interview_a)] + \ m[(applicant, *interview_b)] <= 1 From 9cda8d15d70d7f47aa6b634ee7088f3456ec7487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Thu, 15 Aug 2024 15:31:30 +0200 Subject: [PATCH 129/169] config: endret vekt til 1.5 --- algorithm/src/mip_matching/match_meetings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index f8c6fe2c..bea33caa 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -9,8 +9,8 @@ from itertools import combinations -# Mål på hvor mye buffer skal vektlegges i matchingen (-1 tilsvarer vekten av ett intervju) -APPLICANT_BUFFER_MODEL_WEIGHT = 0.001 +# Mål på hvor mye buffer skal vektlegges i matchingen (1 tilsvarer vekten av ett intervju) +APPLICANT_BUFFER_MODEL_WEIGHT = 1.5 # Hvor stort buffer man ønsker å ha mellom intervjuene APPLICANT_BUFFER_LENGTH = timedelta(minutes=15) @@ -111,4 +111,4 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me "matchings": matchings, } - return match_object \ No newline at end of file + return match_object From 236cd7c39ae8c752bf3e751cc855617f306459ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Thu, 15 Aug 2024 13:55:20 +0000 Subject: [PATCH 130/169] fix: made buffer a normal constraint --- algorithm/src/Modellering.md | 25 ++++++++--------- algorithm/src/mip_matching/match_meetings.py | 28 +++----------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/algorithm/src/Modellering.md b/algorithm/src/Modellering.md index 11f127e7..30fc6439 100644 --- a/algorithm/src/Modellering.md +++ b/algorithm/src/Modellering.md @@ -10,42 +10,48 @@ ## Variabler `p` + - Person `k` + - Komité `t` + - Timeslot (Må gjøres til intervaller etter hvert) `m(p, k, t)` + - Binær variabel - Person `p` har møte med komité `k` i timeslot `t` ## Hjelpevariabler `c(p, t)` + - Binære variabler - Tidspunkt `t` passer for person `p` `c(k, t)` + - Heltallsvariabel - Kapasitet for komité `k` på tidspunkt `t` (hvor mange intervju de kan ha på det gitte tidspunktet) ## Begrensninger For alle `p`: - -- `m(p, k, t) <= 1` dersom - - `p` har søkt på komité `k` - - `c(p, t) => 1` - - `c(k, t) => 1` + +- `m(p, k, t_1) + m(p, k, t_2) < 2` for alle gyldige `k, t_1` og `k, t_2`, hvor t_1 og t_2 overlapper eller er innenfor et gitt buffer-intervall. +- `m(p, k, t) <= 1` dersom + - `p` har søkt på komité `k` + - `c(p, t) => 1` + - `c(k, t) => 1` - `m(p, k, t) <= 0` ellers For alle `k`: -- `sum(m(p, k, t)) <= c(k, t)` for alle personer `p` og tidspunkt `t` - +- `sum(m(p, k, t)) <= c(k, t)` for alle personer `p` og tidspunkt `t` ## Mål @@ -53,9 +59,4 @@ Maksimere `sum(m(p, k, t))` for alle `p`, `k` og `t` ### Sekundærmål -- La det være et gitt mellomrom mellom hvert intervju for samme person. - - [Ikke enda implementert] La det være færrest mulig og minst mulig mellomrom mellom intervjuene for komitéene. - - - diff --git a/algorithm/src/mip_matching/match_meetings.py b/algorithm/src/mip_matching/match_meetings.py index 1f3a8ac6..44d6cab4 100644 --- a/algorithm/src/mip_matching/match_meetings.py +++ b/algorithm/src/mip_matching/match_meetings.py @@ -9,9 +9,6 @@ from itertools import combinations -# Mål på hvor mye buffer skal vektlegges i matchingen (1 tilsvarer vekten av ett intervju) -APPLICANT_BUFFER_MODEL_WEIGHT = 1.5 - # Hvor stort buffer man ønsker å ha mellom intervjuene APPLICANT_BUFFER_LENGTH = timedelta(minutes=15) @@ -53,6 +50,7 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me for interval in applicant.get_fitting_committee_slots(committee)) <= 1 # Legger inn begrensninger for at en søker ikke kan ha overlappende intervjutider + # og minst har et buffer mellom hvert intervju som angitt for applicant in applicants: potential_interviews: set[tuple[Committee, TimeInterval]] = set() for applicant_candidate, committee, interval in m: @@ -60,32 +58,12 @@ def match_meetings(applicants: set[Applicant], committees: set[Committee]) -> Me potential_interviews.add((committee, interval)) for interview_a, interview_b in combinations(potential_interviews, r=2): - if interview_a[1].intersects(interview_b[1]): + if interview_a[1].intersects(interview_b[1]) or interview_a[1].is_within_distance(interview_b[1], APPLICANT_BUFFER_LENGTH): model += m[(applicant, *interview_a)] + \ m[(applicant, *interview_b)] <= 1 - # Legger til sekundærmålsetning om at man skal ha mellomrom mellom perioder - distance_variables = set() - - for applicant in applicants: - potential_committees_with_intervals: set[tuple[Committee, TimeInterval]] = set() - for applicant_candidate, committee, interval in m: - if applicant == applicant_candidate: - potential_committees_with_intervals.add((committee, interval)) - - # Trekker fra vekten dersom det prøves å planlegges et møte med mindre buffer enn ønsket - distance_variables.add( - mip.xsum( - -APPLICANT_BUFFER_MODEL_WEIGHT * (m[(applicant, *a)] and m[(applicant, *b)]) - for a, b in combinations(potential_committees_with_intervals, r=2) # type: ignore - if a[0] != b[0] # Ikke intervju med samme komite (blir ordnet over) - and a[1].is_within_distance(b[1], APPLICANT_BUFFER_LENGTH) # Intervjuene er innenfor bufferlengden - ) - ) - # Setter mål til å være maksimering av antall møter - # med sekundærmål om buffer - model.objective = mip.maximize(mip.xsum(m.values()) + mip.xsum(distance_variables)) + model.objective = mip.maximize(mip.xsum(m.values())) # Kjør optimeringen solver_status = model.optimize() From 0a74f08872a3008e62f4ce0fcab93da4fd71efb6 Mon Sep 17 00:00:00 2001 From: fredrir Date: Thu, 15 Aug 2024 17:15:49 +0200 Subject: [PATCH 131/169] update "ikke satt" message --- lib/sendInterviewTimes/formatInterviewEmail.ts | 6 ++---- lib/sendInterviewTimes/formatInterviewSMS.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/sendInterviewTimes/formatInterviewEmail.ts b/lib/sendInterviewTimes/formatInterviewEmail.ts index 19d564c3..72ac4377 100644 --- a/lib/sendInterviewTimes/formatInterviewEmail.ts +++ b/lib/sendInterviewTimes/formatInterviewEmail.ts @@ -30,8 +30,7 @@ export const formatApplicantInterviewEmail = ( )}
        `; } if (committee.interviewTime.start === "Ikke satt") { - emailBody += `Start: Ikke satt
        `; - emailBody += `Slutt: Ikke satt
        `; + emailBody += `Tid: Ikke satt. Komitéen vil ta kontakt med deg for å avtale tidspunkt.
        `; } emailBody += `Rom: ${committee.interviewTime.room}
        `; @@ -70,8 +69,7 @@ export const formatCommitteeInterviewEmail = ( } if (applicant.interviewTime.start === "Ikke satt") { - emailBody += `Start: Ikke satt
        `; - emailBody += `Slutt: Ikke satt
        `; + emailBody += `Tid: Ikke satt. Ta kontakt med søker for å avtale tidspunkt.`; } emailBody += `Rom: ${applicant.interviewTime.room}
        `; }); diff --git a/lib/sendInterviewTimes/formatInterviewSMS.ts b/lib/sendInterviewTimes/formatInterviewSMS.ts index 4ece154f..e9b42822 100644 --- a/lib/sendInterviewTimes/formatInterviewSMS.ts +++ b/lib/sendInterviewTimes/formatInterviewSMS.ts @@ -23,7 +23,7 @@ export const formatInterviewSMS = (applicant: emailApplicantInterviewType) => { } if (committee.interviewTime.start === "Ikke satt") { - phoneBody += `Tid: Ikke satt \n`; + phoneBody += `Tid: Ikke satt. Komitéen vil ta kontakt for å avtale tidspunkt. \n`; } phoneBody += `Rom: ${committee.interviewTime.room} \n \n`; From 8f022d5808547a9153ae0dc7342321005b46bfd0 Mon Sep 17 00:00:00 2001 From: fredrir Date: Thu, 15 Aug 2024 17:37:27 +0200 Subject: [PATCH 132/169] fixed removeCell function --- .../committee/CommitteeInterviewTimes.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 4ccb06e6..8c60d793 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -223,13 +223,25 @@ const CommitteeInterviewTimes = ({ } }; - const removeCell = (event: any) => { + const removeCell = (event: Interview) => { setMarkedCells((prevCells) => prevCells.filter( - (cell) => cell.start !== event.startStr && cell.end !== event.endStr + (cell) => + new Date(cell.start).getTime() !== new Date(event.start).getTime() || + new Date(cell.end).getTime() !== new Date(event.end).getTime() || + cell.title !== event.title ) ); - event.remove(); + + setCalendarEvents((prevEvents) => + prevEvents.filter( + (evt) => + new Date(evt.start).getTime() !== new Date(event.start).getTime() || + new Date(evt.end).getTime() !== new Date(event.end).getTime() || + evt.title !== event.title + ) + ); + setUnsavedChanges(true); }; @@ -257,9 +269,9 @@ const CommitteeInterviewTimes = ({ e.stopPropagation(); removeCell({ - startStr: eventContent.event.start.toISOString(), - endStr: eventContent.event.end.toISOString(), - remove: () => eventContent.event.remove(), + start: eventContent.event.start.toISOString(), + end: eventContent.event.end.toISOString(), + title: eventContent.event.title, }); }} > From 8ab10eccc88d570714315c81481344db238750f3 Mon Sep 17 00:00:00 2001 From: fredrir Date: Thu, 15 Aug 2024 17:50:44 +0200 Subject: [PATCH 133/169] use ID instead --- .../committee/CommitteeInterviewTimes.tsx | 62 +++++++------------ 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 8c60d793..014b8877 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -15,6 +15,7 @@ import { useQuery } from "@tanstack/react-query"; import { fetchApplicantsByPeriodIdAndCommittee } from "../../lib/api/applicantApi"; interface Interview { + id: string; title: string; start: string; end: string; @@ -98,6 +99,7 @@ const CommitteeInterviewTimes = ({ setHasAlreadySubmitted(true); const events = committeeInterviewTimes.availabletimes.map( (at: any) => ({ + id: calendarEvents.length.toString(), title: at.room, start: new Date(at.start).toISOString(), end: new Date(at.end).toISOString(), @@ -156,21 +158,18 @@ const CommitteeInterviewTimes = ({ return; } - const event = { + const event: Interview = { + id: calendarEvents.length.toString(), title: roomInput, - start: currentSelection.start, - end: currentSelection.end, + start: currentSelection.start.toISOString(), + end: currentSelection.end.toISOString(), }; const calendarApi = currentSelection.view.calendar; calendarApi.addEvent(event); calendarApi.render(); - addCell([ - roomInput, - currentSelection.start.toISOString(), - currentSelection.end.toISOString(), - ]); + addCell(event); setRoomInput(""); setIsModalOpen(false); @@ -225,31 +224,18 @@ const CommitteeInterviewTimes = ({ const removeCell = (event: Interview) => { setMarkedCells((prevCells) => - prevCells.filter( - (cell) => - new Date(cell.start).getTime() !== new Date(event.start).getTime() || - new Date(cell.end).getTime() !== new Date(event.end).getTime() || - cell.title !== event.title - ) + prevCells.filter((cell) => cell.id !== event.id) ); setCalendarEvents((prevEvents) => - prevEvents.filter( - (evt) => - new Date(evt.start).getTime() !== new Date(event.start).getTime() || - new Date(evt.end).getTime() !== new Date(event.end).getTime() || - evt.title !== event.title - ) + prevEvents.filter((evt) => evt.id !== event.id) ); setUnsavedChanges(true); }; - const addCell = (cell: string[]) => { - setMarkedCells([ - ...markedCells, - { title: cell[0], start: cell[1], end: cell[2] }, - ]); + const addCell = (event: Interview) => { + setMarkedCells([...markedCells, event]); setUnsavedChanges(true); }; @@ -269,6 +255,7 @@ const CommitteeInterviewTimes = ({ e.stopPropagation(); removeCell({ + id: eventContent.event.id, start: eventContent.event.start.toISOString(), end: eventContent.event.end.toISOString(), title: eventContent.event.title, @@ -289,21 +276,16 @@ const CommitteeInterviewTimes = ({ ); }; - const formatEventsForExport = (events: any[]) => { - return events - .map((event) => { - const startDateTimeString = `${event.start}`; - const endDatetimeString = `${event.end}`; - - const startDateTime = new Date(startDateTimeString); - const endDateTime = new Date(endDatetimeString); - return { - room: event.title, - start: startDateTime.toISOString(), - end: endDateTime.toISOString(), - }; - }) - .filter((event) => event !== null); + const formatEventsForExport = (events: Interview[]) => { + return events.map((event) => { + const startDateTime = new Date(event.start); + const endDateTime = new Date(event.end); + return { + room: event.title, + start: startDateTime.toISOString(), + end: endDateTime.toISOString(), + }; + }); }; const handleTimeslotSelection = (e: React.ChangeEvent) => { From e2c1449572383cfae8ecbb65f937f215e702f46d Mon Sep 17 00:00:00 2001 From: fredrir Date: Thu, 15 Aug 2024 17:54:24 +0200 Subject: [PATCH 134/169] remove unused state --- components/committee/CommitteeInterviewTimes.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 014b8877..2bf4c656 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -36,7 +36,6 @@ const CommitteeInterviewTimes = ({ }: Props) => { const { data: session } = useSession(); - const [markedCells, setMarkedCells] = useState([]); const [interviewInterval, setInterviewInterval] = useState(15); const [visibleRange, setVisibleRange] = useState({ start: "", end: "" }); @@ -178,7 +177,7 @@ const CommitteeInterviewTimes = ({ const submit = async (e: BaseSyntheticEvent) => { e.preventDefault(); - const formattedEvents = formatEventsForExport(markedCells); + const formattedEvents = formatEventsForExport(calendarEvents); if (formattedEvents.length === 0) { toast.error("Fyll inn minst et gyldig tidspunkt"); return; @@ -223,10 +222,6 @@ const CommitteeInterviewTimes = ({ }; const removeCell = (event: Interview) => { - setMarkedCells((prevCells) => - prevCells.filter((cell) => cell.id !== event.id) - ); - setCalendarEvents((prevEvents) => prevEvents.filter((evt) => evt.id !== event.id) ); @@ -235,7 +230,7 @@ const CommitteeInterviewTimes = ({ }; const addCell = (event: Interview) => { - setMarkedCells([...markedCells, event]); + setCalendarEvents((prevEvents) => [...prevEvents, event]); setUnsavedChanges(true); }; From efe84a27b6fda6e98331d703c30153160401cd76 Mon Sep 17 00:00:00 2001 From: fredrir Date: Thu, 15 Aug 2024 17:57:11 +0200 Subject: [PATCH 135/169] Revert "remove unused state" This reverts commit e2c1449572383cfae8ecbb65f937f215e702f46d. --- components/committee/CommitteeInterviewTimes.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 2bf4c656..014b8877 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -36,6 +36,7 @@ const CommitteeInterviewTimes = ({ }: Props) => { const { data: session } = useSession(); + const [markedCells, setMarkedCells] = useState([]); const [interviewInterval, setInterviewInterval] = useState(15); const [visibleRange, setVisibleRange] = useState({ start: "", end: "" }); @@ -177,7 +178,7 @@ const CommitteeInterviewTimes = ({ const submit = async (e: BaseSyntheticEvent) => { e.preventDefault(); - const formattedEvents = formatEventsForExport(calendarEvents); + const formattedEvents = formatEventsForExport(markedCells); if (formattedEvents.length === 0) { toast.error("Fyll inn minst et gyldig tidspunkt"); return; @@ -222,6 +223,10 @@ const CommitteeInterviewTimes = ({ }; const removeCell = (event: Interview) => { + setMarkedCells((prevCells) => + prevCells.filter((cell) => cell.id !== event.id) + ); + setCalendarEvents((prevEvents) => prevEvents.filter((evt) => evt.id !== event.id) ); @@ -230,7 +235,7 @@ const CommitteeInterviewTimes = ({ }; const addCell = (event: Interview) => { - setCalendarEvents((prevEvents) => [...prevEvents, event]); + setMarkedCells([...markedCells, event]); setUnsavedChanges(true); }; From 0321406a70a01d6783b8a750bce33acbcb031649 Mon Sep 17 00:00:00 2001 From: fredrir Date: Fri, 16 Aug 2024 13:52:12 +0200 Subject: [PATCH 136/169] remove markedCells --- .../committee/CommitteeInterviewTimes.tsx | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 014b8877..2b8fc9c7 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -35,8 +35,6 @@ const CommitteeInterviewTimes = ({ committeeInterviewTimes, }: Props) => { const { data: session } = useSession(); - - const [markedCells, setMarkedCells] = useState([]); const [interviewInterval, setInterviewInterval] = useState(15); const [visibleRange, setVisibleRange] = useState({ start: "", end: "" }); @@ -60,6 +58,10 @@ const CommitteeInterviewTimes = ({ const [numberOfApplications, setNumberOfApplications] = useState(0); + useEffect(() => { + console.log(calendarEvents); + }, [calendarEvents]); + useEffect(() => { if (period) { setVisibleRange({ @@ -169,8 +171,6 @@ const CommitteeInterviewTimes = ({ calendarApi.addEvent(event); calendarApi.render(); - addCell(event); - setRoomInput(""); setIsModalOpen(false); setCalendarEvents((prevEvents) => [...prevEvents, event]); @@ -178,7 +178,7 @@ const CommitteeInterviewTimes = ({ const submit = async (e: BaseSyntheticEvent) => { e.preventDefault(); - const formattedEvents = formatEventsForExport(markedCells); + const formattedEvents = formatEventsForExport(calendarEvents); if (formattedEvents.length === 0) { toast.error("Fyll inn minst et gyldig tidspunkt"); return; @@ -223,10 +223,6 @@ const CommitteeInterviewTimes = ({ }; const removeCell = (event: Interview) => { - setMarkedCells((prevCells) => - prevCells.filter((cell) => cell.id !== event.id) - ); - setCalendarEvents((prevEvents) => prevEvents.filter((evt) => evt.id !== event.id) ); @@ -234,11 +230,6 @@ const CommitteeInterviewTimes = ({ setUnsavedChanges(true); }; - const addCell = (event: Interview) => { - setMarkedCells([...markedCells, event]); - setUnsavedChanges(true); - }; - const updateInterviewInterval = (e: BaseSyntheticEvent) => { setInterviewInterval(parseInt(e.target.value)); setUnsavedChanges(true); From 9c841761b3a22a03b0c8bc03a37b4d802615275d Mon Sep 17 00:00:00 2001 From: fredrir Date: Fri, 16 Aug 2024 14:03:38 +0200 Subject: [PATCH 137/169] add unique id for events --- .../committee/CommitteeInterviewTimes.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index 2b8fc9c7..f748d4f3 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -58,10 +58,6 @@ const CommitteeInterviewTimes = ({ const [numberOfApplications, setNumberOfApplications] = useState(0); - useEffect(() => { - console.log(calendarEvents); - }, [calendarEvents]); - useEffect(() => { if (period) { setVisibleRange({ @@ -100,12 +96,15 @@ const CommitteeInterviewTimes = ({ if (cleanCommittee === cleanSelectedCommittee) { setHasAlreadySubmitted(true); const events = committeeInterviewTimes.availabletimes.map( - (at: any) => ({ - id: calendarEvents.length.toString(), - title: at.room, - start: new Date(at.start).toISOString(), - end: new Date(at.end).toISOString(), - }) + (at: any) => ( + console.log(at), + { + id: crypto.getRandomValues(new Uint32Array(1))[0].toString(), + title: at.room, + start: new Date(at.start).toISOString(), + end: new Date(at.end).toISOString(), + } + ) ); setCalendarEvents(events); @@ -161,7 +160,7 @@ const CommitteeInterviewTimes = ({ } const event: Interview = { - id: calendarEvents.length.toString(), + id: crypto.getRandomValues(new Uint32Array(1))[0].toString(), title: roomInput, start: currentSelection.start.toISOString(), end: currentSelection.end.toISOString(), @@ -235,7 +234,7 @@ const CommitteeInterviewTimes = ({ setUnsavedChanges(true); }; - const renderEventContent = (eventContent: any) => { + const calendarEventStyle = (eventContent: { event: Interview }) => { return (
        {!hasAlreadySubmitted && ( @@ -247,8 +246,8 @@ const CommitteeInterviewTimes = ({ removeCell({ id: eventContent.event.id, - start: eventContent.event.start.toISOString(), - end: eventContent.event.end.toISOString(), + start: eventContent.event.start, + end: eventContent.event.end, title: eventContent.event.title, }); }} @@ -303,6 +302,7 @@ const CommitteeInterviewTimes = ({ setHasAlreadySubmitted(false); setCalendarEvents([]); + setInterviewsPlanned(0); setUnsavedChanges(false); } catch (error: any) { console.error("Error deleting submission:", error); @@ -445,7 +445,7 @@ const CommitteeInterviewTimes = ({ slotMinTime="08:00" slotMaxTime="18:00" validRange={visibleRange} - eventContent={renderEventContent} + eventContent={calendarEventStyle} eventConstraint={{ startTime: "08:00", endTime: "18:00" }} selectAllow={(selectInfo) => { const start = selectInfo.start; From f32f6c54c337ec4a06eda458864909ff01b71b63 Mon Sep 17 00:00:00 2001 From: fredrir Date: Fri, 16 Aug 2024 14:13:33 +0200 Subject: [PATCH 138/169] remove antipattern design --- .../committee/CommitteeInterviewTimes.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index f748d4f3..abef5e0c 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -13,6 +13,8 @@ import useUnsavedChangesWarning from "../../lib/utils/unSavedChangesWarning"; import { SimpleTitle } from "../Typography"; import { useQuery } from "@tanstack/react-query"; import { fetchApplicantsByPeriodIdAndCommittee } from "../../lib/api/applicantApi"; +import { CheckIcon } from "@heroicons/react/24/outline"; +import { XMarkIcon } from "@heroicons/react/24/solid"; interface Interview { id: string; @@ -499,12 +501,18 @@ const CommitteeInterviewTimes = ({ onChange={(e) => setRoomInput(e.target.value)} />
        - +
    From 34873709e36e6f17734dc9ec258032bcf142d637 Mon Sep 17 00:00:00 2001 From: fredrir Date: Fri, 16 Aug 2024 14:14:51 +0200 Subject: [PATCH 139/169] fix interviewsPlanned --- components/committee/CommitteeInterviewTimes.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index abef5e0c..ba447763 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -147,6 +147,10 @@ const CommitteeInterviewTimes = ({ if (calendarEvents.length > 0) { calculateInterviewsPlanned(); } + + if (!calendarEvents || calendarEvents.length === 0) { + setInterviewsPlanned(0); + } }, [calendarEvents, selectedTimeslot]); const handleDateSelect = (selectionInfo: any) => { From 95432f7ae368712330a3515265cd6738d4de2099 Mon Sep 17 00:00:00 2001 From: fredrir Date: Fri, 16 Aug 2024 14:15:53 +0200 Subject: [PATCH 140/169] remove console.log --- components/committee/CommitteeInterviewTimes.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index ba447763..c4ae3cad 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -98,15 +98,12 @@ const CommitteeInterviewTimes = ({ if (cleanCommittee === cleanSelectedCommittee) { setHasAlreadySubmitted(true); const events = committeeInterviewTimes.availabletimes.map( - (at: any) => ( - console.log(at), - { - id: crypto.getRandomValues(new Uint32Array(1))[0].toString(), - title: at.room, - start: new Date(at.start).toISOString(), - end: new Date(at.end).toISOString(), - } - ) + (at: any) => ({ + id: crypto.getRandomValues(new Uint32Array(1))[0].toString(), + title: at.room, + start: new Date(at.start).toISOString(), + end: new Date(at.end).toISOString(), + }) ); setCalendarEvents(events); From f017755570bb8881d462fadb5f6b7d284d1f5737 Mon Sep 17 00:00:00 2001 From: fredrir Date: Fri, 16 Aug 2024 14:17:36 +0200 Subject: [PATCH 141/169] remove all day --- components/committee/CommitteeInterviewTimes.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/committee/CommitteeInterviewTimes.tsx b/components/committee/CommitteeInterviewTimes.tsx index c4ae3cad..7ba55fa9 100644 --- a/components/committee/CommitteeInterviewTimes.tsx +++ b/components/committee/CommitteeInterviewTimes.tsx @@ -447,6 +447,7 @@ const CommitteeInterviewTimes = ({ weekends={false} slotMinTime="08:00" slotMaxTime="18:00" + allDaySlot={false} validRange={visibleRange} eventContent={calendarEventStyle} eventConstraint={{ startTime: "08:00", endTime: "18:00" }} From 35e6b74097010f784f24d2a8396e0ee5a0b36abf Mon Sep 17 00:00:00 2001 From: fredrir Date: Sat, 17 Aug 2024 14:41:47 +0200 Subject: [PATCH 142/169] add superAdmin :) --- pages/admin/[period-id]/index.tsx | 40 +++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index 37806366..8f0d4082 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -34,6 +34,12 @@ const Admin = () => { }, [data, session?.user?.owId]); const sendOutInterviewTimes = async ({ periodId }: { periodId: string }) => { + const confirm = window.confirm( + "Er du sikker på at du vil sende ut intervju tider?" + ); + + if (!confirm) return; + try { const response = await fetch( `/api/periods/send-interview-times/${periodId}`, @@ -81,19 +87,27 @@ const Admin = () => { /> ), }, - { - title: "Send ut", - icon: , - content: ( -
    -
    - ), - }, + //Super admin :) + ...(session?.user?.email && + ["fhansteen@gmail.com", "jotto0214@gmail.com"].includes( + session.user.email + ) + ? [ + { + title: "Send ut", + icon: , + content: ( +
    +
    + ), + }, + ] + : []), ]} />
    From 974caf289f44ede52b0efcc0da43a7f0ab49a99d Mon Sep 17 00:00:00 2001 From: fredrir Date: Sat, 17 Aug 2024 14:42:49 +0200 Subject: [PATCH 143/169] fix spelling --- pages/admin/[period-id]/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index 8f0d4082..d025954d 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -99,7 +99,7 @@ const Admin = () => { content: (