From bcd8a6713c5fb62f9eda2e8b0063b05071c46c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Mon, 9 Mar 2026 18:41:31 +0100 Subject: [PATCH 1/6] update state on matching --- components/Button.tsx | 12 +++- components/SendOutInterviews.tsx | 104 +++++++++++++++++++++++++++ pages/admin/[period-id]/index.tsx | 116 ++++-------------------------- 3 files changed, 128 insertions(+), 104 deletions(-) create mode 100644 components/SendOutInterviews.tsx diff --git a/components/Button.tsx b/components/Button.tsx index 9091b19e..df4d0f47 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -8,6 +8,7 @@ interface Props { icon?: React.ReactNode; onClick?: (e: React.MouseEvent) => void; href?: string; + disabled?: boolean; } const Button = (props: Props) => { @@ -31,6 +32,10 @@ const Button = (props: Props) => { break; } + // Add style for disabled button + colorClasses += + " disabled:bg-gray-500 disabled:text-gray-400 disabled:cursor-not-allowed"; + if (props.size === "small") { sizeClasses = "px-5 py-2.5 text-sm"; } else { @@ -51,7 +56,12 @@ const Button = (props: Props) => { } return ( - diff --git a/components/SendOutInterviews.tsx b/components/SendOutInterviews.tsx new file mode 100644 index 00000000..ce9e900b --- /dev/null +++ b/components/SendOutInterviews.tsx @@ -0,0 +1,104 @@ +import toast from "react-hot-toast"; +import { periodType } from "../lib/types/types"; +import Button from "./Button"; +import { useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; + +interface Props { + period: periodType | null; +} + +const SendOutInterviews = ({ period }: Props) => { + const queryClient = useQueryClient(); + + const [isWaitingOnMatching, setIsWaitingOnMatching] = useState(false); + + const runMatching = async ({ periodId }: { periodId: string }) => { + const confirm = window.confirm( + "Er du sikker på at du vil matche intervjuer?", + ); + + if (!confirm) return; + + try { + const response = await fetch(`/api/periods/match-interviews/${periodId}`); + + if (!response.ok) { + throw new Error("Failed to match interviews"); + } + + const data = await response.json(); + if (data.error) { + throw new Error(data.error); + } + toast.success("Intervjuene ble matchet!"); + + return data; + } catch (error) { + toast.error("Mathcing av intervjuer feilet"); + console.error(error); + } + }; + + const sendOutInterviewTimes = async ({ periodId }: { periodId: string }) => { + const confirm = window.confirm( + "Er du sikker på at du vil sende ut intervjutider?", + ); + + if (!confirm) return; + + try { + const response = await fetch( + `/api/periods/send-interview-times/${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("Klarte ikke å sende ut intervjutider"); + } + }; + + return ( +
+
+ ); +}; + +export default SendOutInterviews; diff --git a/pages/admin/[period-id]/index.tsx b/pages/admin/[period-id]/index.tsx index 51d24a7e..464df74b 100644 --- a/pages/admin/[period-id]/index.tsx +++ b/pages/admin/[period-id]/index.tsx @@ -1,18 +1,16 @@ +import { CalendarIcon, InboxIcon } from "@heroicons/react/24/solid"; +import { useQuery } from "@tanstack/react-query"; import { useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; import router from "next/router"; -import { useRouter } from "next/navigation"; -import { periodType } from "../../../lib/types/types"; -import NotFound from "../../404"; +import { useEffect, useState } from "react"; import ApplicantsOverview from "../../../components/applicantoverview/ApplicantsOverview"; +import ErrorPage from "../../../components/ErrorPage"; +import LoadingPage from "../../../components/LoadingPage"; +import SendOutInterviews from "../../../components/SendOutInterviews"; 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"; +import { periodType } from "../../../lib/types/types"; +import NotFound from "../../404"; const Admin = () => { const { data: session } = useSession(); @@ -22,8 +20,6 @@ const Admin = () => { const [activeTab, setActiveTab] = useState(0); const [tabClicked, setTabClicked] = useState(0); - const navigation = useRouter() - const { data, isError, isLoading } = useQuery({ queryKey: ["periods", periodId], queryFn: fetchPeriodById, @@ -36,63 +32,6 @@ const Admin = () => { ); }, [data, session?.user?.owId]); - const runMatching = async ({ periodId }: {periodId: string}) => { - const confirm = window.confirm( - "Er du sikker på at du vil matche intervjuer?" - ); - - if (!confirm) return; - - try { - const response = await fetch( - `/api/periods/match-interviews/${periodId}`, - ); - - if (!response.ok) { - throw new Error("Failed to match interviews"); - } - - const data = await response.json() - if (data.error) { - throw new Error(data.error) - } - toast.success("Intervjuene ble matchet!"); - - return data; - } catch (error) { - toast.error("Mathcing av intervjuer feilet") - console.error(error); - } - } - - const sendOutInterviewTimes = async ({ periodId }: { periodId: string }) => { - const confirm = window.confirm( - "Er du sikker på at du vil sende ut intervjutider?", - ); - - if (!confirm) return; - - try { - const response = await fetch( - `/api/periods/send-interview-times/${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 ; @@ -119,40 +58,11 @@ const Admin = () => { /> ), }, - //Super admin :) - ...(session?.user?.email && - [ - "fhansteen@gmail.com", - "jotto0214@gmail.com", - "sindreeh@stud.ntnu.no", - "jorgen.4@live.no", - ].includes(session.user.email) - ? [ - { - title: "Send ut", - icon: , - content: ( -
- {period?.hasMatchedInterviews ? -
- ), - }, - ] - : []), + { + title: "Send ut intervjutider", + icon: , + content: , + }, ]} /> From befc7a40893c1b56347b671b5255ccc658d3d8a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Galdal?= Date: Mon, 9 Mar 2026 20:03:43 +0100 Subject: [PATCH 2/6] wip: give status of matching --- components/SendOutInterviews.tsx | 53 ++++++++++++++++++++++++++------ lib/api/interviewApi.ts | 32 +++++++++++++++++++ lib/types/types.ts | 16 ++++++++++ 3 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 lib/api/interviewApi.ts diff --git a/components/SendOutInterviews.tsx b/components/SendOutInterviews.tsx index ce9e900b..af1ae818 100644 --- a/components/SendOutInterviews.tsx +++ b/components/SendOutInterviews.tsx @@ -1,8 +1,10 @@ import toast from "react-hot-toast"; -import { periodType } from "../lib/types/types"; +import { MatchingResult, periodType } from "../lib/types/types"; import Button from "./Button"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; +import { getInterviewsByPeriod } from "../lib/mongo/interviews"; +import { fetchInterviewsByPeriod } from "../lib/api/interviewApi"; interface Props { period: periodType | null; @@ -12,6 +14,25 @@ const SendOutInterviews = ({ period }: Props) => { const queryClient = useQueryClient(); const [isWaitingOnMatching, setIsWaitingOnMatching] = useState(false); + const [matchingResult, setMatchingResult] = useState( + null, + ); + + useEffect(() => { + if (!period?._id) return; + + fetchInterviewsByPeriod(period?._id.toString()).then((result) => { + const total_number_of_interviews = + result.reduce( + (current_sum, { interviews }) => current_sum + interviews.length, + 0, + ) ?? 0; + setMatchingResult({ + matched_meetings: total_number_of_interviews, + total_wanted_meetings: 3, + }); + }); + }, [period]); const runMatching = async ({ periodId }: { periodId: string }) => { const confirm = window.confirm( @@ -76,17 +97,29 @@ const SendOutInterviews = ({ period }: Props) => { disabled={period?.hasMatchedInterviews || isWaitingOnMatching} onClick={async () => { setIsWaitingOnMatching(true); - await runMatching({ periodId: period!._id.toString() }).then(() => { - setIsWaitingOnMatching(false); - - // refetch state - queryClient.invalidateQueries({ - queryKey: ["periods", period?._id], - }); - }); + await runMatching({ periodId: period!._id.toString() }).then( + (result) => { + setIsWaitingOnMatching(false); + setMatchingResult(result); + + // refetch state + queryClient.invalidateQueries({ + queryKey: ["periods", period?._id], + }); + }, + ); }} /> + {period?.hasMatchedInterviews && matchingResult != null && ( +
+

+ Klarte å matche {matchingResult.matched_meetings} av{" "} + {matchingResult.total_wanted_meetings} +

+
+ )} +