Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface Props {
icon?: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
href?: string;
disabled?: boolean;
}

const Button = (props: Props) => {
Expand All @@ -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 {
Expand All @@ -51,7 +56,12 @@ const Button = (props: Props) => {
}

return (
<button type="button" onClick={props.onClick} className={className}>
<button
type="button"
onClick={props.onClick}
className={className}
disabled={props.disabled}
>
{props.title}
{props.icon}
</button>
Expand Down
172 changes: 172 additions & 0 deletions components/SendOutInterviews.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col items-center overflow-hidden w-[50%] m-auto border-gray-300 border-2 bg-zinc-50 rounded-lg">
<h2 className="w-full text-white p-2 shadow- text-lg bg-online-darkTeal m-0 shadow-xl">
Matching av intervjutider
</h2>
<div className="m-4 flex flex-col items-center gap-4">
<p className="w-full">
Sett opp intervjutider automatisk ved å kjøre matching. Systemet vil
sette op så mange intervjuer som mulig basert på tidspunktene som
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sette op mange intervjuer som mulig basert tidspunktene som
sette opp mange intervjuer som mulig basert tidspunktene som

komitéene og søkerne har satt opp.
</p>
<p className="w-full">
I tillegg optimaliseres det for følgende undermål (i prioritert
rekkefølge):
<ul className="w-full list-disc list-inside">
<li>
Intervjutidspunkt er nærmest mulig midt på dagen. Dette hindrer i
tillegg unødvendige tomrom i en intervjubolk.
</li>
<li>
Første dagen unngås forsøksvis. Dette gjøres for å gi søkere best
mulig tid til å planlegge fra intervjutiden blir sendt.
</li>
</ul>
</p>

<hr className="my-2 w-full" />

{!period?.hasMatchedInterviews && (
<Button
title={isWaitingOnMatching ? "Kjører matching..." : "Kjør matching"}
color={"blue"}
disabled={period?.hasMatchedInterviews || isWaitingOnMatching}
onClick={async () => {
setIsWaitingOnMatching(true);
await runMatching({ periodId: period!._id.toString() }).then(
(result) => {
setIsWaitingOnMatching(false);

// refetch state
queryClient.invalidateQueries({
queryKey: ["periods", period?._id],
});
},
);
}}
/>
)}

{period?.hasMatchedInterviews && period.matching_status && (
<div className="flex flex-col items-center p-2 border border-gray-300 gap-4">
<div className="flex flex-row gap-4 border-b p-1">
<h3>Matching gjennomført</h3>
<p
className={
"rounded-xl p-1 w-fit " +
(period.matching_status.status == "OptimizationStatus.OPTIMAL"
? "bg-green-300"
: "bg-red-300")
}
>
Status:{" "}
{period.matching_status.status.replace(
"OptimizationStatus.",
"",
)}
</p>
</div>
<p className="w-fit">
Klarte å matche{" "}
<span className="bg-gray-300 rounded-xl p-1">
{period.matching_status.matched_meetings} av{" "}
{period.matching_status.total_wanted_meetings}
</span>{" "}
intervjuer.
</p>
</div>
)}

{period?.hasMatchedInterviews &&
(!period?.hasSentInterviewTimes ? (
<Button
title={"Send ut intervjutider"}
color={"blue"}
disabled={
!period?.hasMatchedInterviews && !period?.hasSentInterviewTimes
}
onClick={async () =>
await sendOutInterviewTimes({
periodId: period!._id.toString(),
})
}
/>
) : (
<p>Intervjuer er sendt ut!</p>
))}
</div>
</div>
);
};

export default SendOutInterviews;
29 changes: 29 additions & 0 deletions lib/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type periodType = {
committees: string[];
optionalCommittees: string[];
hasMatchedInterviews: boolean;
matching_status: MatchingStatus | null;
hasSentInterviewTimes: boolean;
};

Expand Down Expand Up @@ -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;
}[];
};
116 changes: 13 additions & 103 deletions pages/admin/[period-id]/index.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -22,8 +20,6 @@ const Admin = () => {
const [activeTab, setActiveTab] = useState(0);
const [tabClicked, setTabClicked] = useState<number>(0);

const navigation = useRouter()

const { data, isError, isLoading } = useQuery({
queryKey: ["periods", periodId],
queryFn: fetchPeriodById,
Expand All @@ -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 <NotFound />;
Expand All @@ -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: <InboxIcon className="w-5 h-5" />,
content: (
<div className="flex flex-col items-center">
{period?.hasMatchedInterviews ?
<Button
title={"Send ut intervjutider"}
color={"blue"}
onClick={async () => await sendOutInterviewTimes({ periodId })}
/> :
<Button
title={"Kjør matching"}
color={"blue"}
onClick={async () => {
await runMatching({ periodId });
navigation.refresh();
}}
/>
}
</div>
),
},
]
: []),
{
title: "Send ut intervjutider",
icon: <InboxIcon className="w-5 h-5" />,
content: <SendOutInterviews period={period} />,
},
]}
/>
</div>
Expand Down