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..7dd9aefe --- /dev/null +++ b/components/SendOutInterviews.tsx @@ -0,0 +1,172 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import { periodType } from "../lib/types/types"; +import Button from "./Button"; + +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 ( +
+

+ Matching av intervjutider +

+
+

+ Sett opp intervjutider automatisk ved å kjøre matching. Systemet vil + sette op så mange intervjuer som mulig basert på tidspunktene som + komitéene og søkerne har satt opp. +

+

+ I tillegg optimaliseres det for følgende undermål (i prioritert + rekkefølge): +

    +
  • + Intervjutidspunkt er nærmest mulig midt på dagen. Dette hindrer i + tillegg unødvendige tomrom i en intervjubolk. +
  • +
  • + Første dagen unngås forsøksvis. Dette gjøres for å gi søkere best + mulig tid til å planlegge fra intervjutiden blir sendt. +
  • +
+

+ +
+ + {!period?.hasMatchedInterviews && ( +
+
+ ); +}; + +export default SendOutInterviews; diff --git a/lib/types/types.ts b/lib/types/types.ts index 67b7f17c..11ef3693 100644 --- a/lib/types/types.ts +++ b/lib/types/types.ts @@ -62,6 +62,7 @@ export type periodType = { committees: string[]; optionalCommittees: string[]; hasMatchedInterviews: boolean; + matching_status: MatchingStatus | null; hasSentInterviewTimes: boolean; }; @@ -152,3 +153,31 @@ export type emailApplicantInterviewType = { }; }[]; }; + +export type MatchingStatus = { + matched_meetings: number; + status: // ref. https://python-mip.readthedocs.io/en/latest/classes.html#optimizationstatus + | "OptimizationStatus.CUTOFF" + | "OptimizationStatus.ERROR" + | "OptimizationStatus.FEASIBLE" + | "OptimizationStatus.INFEASIBLE" + | "OptimizationStatus.INT_INFEASIBLE" + | "OptimizationStatus.LOADED" + | "OptimizationStatus.NO_SOLUTION_FOUND" + | "OptimizationStatus.OPTIMAL" + | "OptimizationStatus.UNBOUNDED"; + total_wanted_meetings: number; +}; + +export type MatchingResult = MatchingStatus & { + results: { + applicantId: string; + interview: { + committeeName: string; + start: string; + end: string; + room: string; + }[]; + periodId: string; + }[]; +}; 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: , + }, ]} />