Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

341 add functionality for editing applications #349

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
39 changes: 37 additions & 2 deletions components/form/ApplicationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import RadioInput from "./RadioInput";
import TextAreaInput from "./TextAreaInput";
import SelectInput from "./SelectInput";
import Line from "./Line";
import { DeepPartial, applicantType } from "../../lib/types/types";
import {
DeepPartial,
applicantType,
preferencesType,
} from "../../lib/types/types";
import { changeDisplayName } from "../../lib/utils/toString";
import { useEffect, useState } from "react";
import CustomPhoneInput from "./CustomPhoneInput";
Expand All @@ -14,6 +18,7 @@ interface Props {
setApplicationData: Function;
availableCommittees: string[];
optionalCommittees: string[];
isEditing?: boolean;
}

export const ApplicationForm = (props: Props) => {
Expand Down Expand Up @@ -121,6 +126,7 @@ export const ApplicationForm = (props: Props) => {
<Line />
<TextAreaInput
label="Skriv litt om deg selv"
value={props.applicationData.about}
updateInputValues={(value: any) =>
props.setApplicationData({ ...props.applicationData, about: value })
}
Expand All @@ -136,6 +142,9 @@ export const ApplicationForm = (props: Props) => {
</div>
<SelectInput
required
defaultValue={
(props.applicationData.preferences as preferencesType)?.first || ""
}
values={availableCommittees}
label={availableCommittees.length > 2 ? "Førstevalg" : "Velg komite"}
updateInputValues={(value: string) =>
Expand All @@ -152,6 +161,10 @@ export const ApplicationForm = (props: Props) => {
{availableCommittees.length > 2 && (
<SelectInput
values={availableCommittees}
defaultValue={
(props.applicationData.preferences as preferencesType)?.second ||
""
}
label="Andrevalg"
updateInputValues={(value: string) =>
props.setApplicationData({
Expand All @@ -167,6 +180,10 @@ export const ApplicationForm = (props: Props) => {
{availableCommittees.length > 3 && (
<SelectInput
values={availableCommittees}
defaultValue={
(props.applicationData.preferences as preferencesType)?.third ||
""
}
label="Tredjevalg"
updateInputValues={(value: string) =>
props.setApplicationData({
Expand All @@ -186,8 +203,17 @@ export const ApplicationForm = (props: Props) => {
["Nei", "nei"],
["Usikker (gjerne spør om mer info på intervjuet)", "kanskje"],
]}
defaultValue={
props.isEditing
? props.applicationData.bankom === "ja"
? "ja"
: props.applicationData.bankom === "nei"
? "nei"
: "kanskje"
: undefined
}
label="Er du interessert i å være økonomiansvarlig i komiteen (tilleggsverv i Bankom)?"
updateInputValues={(value: boolean) =>
updateInputValues={(value: string) =>
props.setApplicationData({
...props.applicationData,
bankom: value,
Expand All @@ -197,6 +223,15 @@ export const ApplicationForm = (props: Props) => {
{optionalCommittees.map((committee) => (
<div key={committee}>
<RadioInput
defaultValue={
props.isEditing
? props.applicationData.optionalCommittees?.includes(
committee
)
? "ja"
: "nei"
: undefined
}
values={[
["Ja", "ja"],
["Nei", "nei"],
Expand Down
2 changes: 1 addition & 1 deletion components/form/CheckboxInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { InformationCircleIcon } from "@heroicons/react/24/outline";

interface CheckboxOption {
Expand Down
150 changes: 150 additions & 0 deletions components/form/EditApplicationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { useState } from "react";
import { ApplicationForm } from "./ApplicationForm";
import { applicantType, DeepPartial, periodType } from "../../lib/types/types";
import Button from "../Button";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { editApplicant } from "../../lib/api/applicantApi";
import toast from "react-hot-toast";
import { validateApplication } from "../../lib/utils/validateApplication";
import { Tabs } from "../Tabs";
import CheckBoxIcon from "../icons/icons/CheckBoxIcon";
import { CalendarIcon } from "@heroicons/react/24/outline";
import Schedule from "../committee/Schedule";

interface Props {
originalApplicationData: applicantType;
period: periodType;
availableCommittees: string[];
optionalCommittees: string[];
setIsEditing: (isEditing: boolean) => void;
}

const ApplicationEditModal = ({
availableCommittees,
optionalCommittees,
period,
originalApplicationData,
setIsEditing,
}: Props) => {
const queryClient = useQueryClient();
const [activeTab, setActiveTab] = useState(0);
const [applicationData, setApplicationData] = useState<
DeepPartial<applicantType>
>({
owId: originalApplicationData.owId,
name: originalApplicationData.name,
email: originalApplicationData.email,
phone: originalApplicationData.phone,
grade: originalApplicationData.grade,
about: originalApplicationData.about,
bankom: originalApplicationData.bankom,

optionalCommittees: originalApplicationData.optionalCommittees,
preferences: originalApplicationData.preferences,
selectedTimes: originalApplicationData.selectedTimes,
});

const submitEdit = () => {
if (!validateApplication(applicationData)) return;

editApplicationMutation.mutate({
applicant: applicationData as applicantType,
periodId: period._id.toString(),
owId: originalApplicationData.owId,
});
};

const editApplicationMutation = useMutation({
mutationFn: editApplicant,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [
"applicant",
period._id.toString(),
originalApplicationData.owId,
],
});
toast.success("Søknad oppdatert successfully");
setIsEditing(false);
},
onError: () => toast.error("Det skjedde en feil, vennligst prøv igjen"),
});

return (
<>
<div className="flex flex-col items-center justify-center min-h-screen pt-4 px-5 pb-20 text-center">
<div className="bg-white">
<div className="text-center">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Rediger søknad
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Du kan redigere søknaden din her. Husk å lagre endringene før du
lukker.
</p>
</div>
</div>
<div className="mt-5 sm:mt-4 max-w-md">
<Tabs
activeTab={activeTab}
setActiveTab={setActiveTab}
content={[
{
title: "Søknad",
icon: <CheckBoxIcon className="w-5 h-5" />,
content: (
<>
<ApplicationForm
applicationData={applicationData}
setApplicationData={setApplicationData}
availableCommittees={availableCommittees}
optionalCommittees={optionalCommittees}
isEditing={true}
/>
<div className="flex justify-center w-full">
<Button
title="Videre"
color="blue"
onClick={() => {
if (!validateApplication(applicationData)) return;
setActiveTab(activeTab + 1);
}}
size="small"
/>
</div>
</>
),
},
{
title: "Intervjutider",
icon: <CalendarIcon className="w-5 h-5" />,
content: (
<div className="flex flex-col items-center justify-center">
<Schedule
interviewLength={Number(30)}
periodTime={period?.interviewPeriod}
setApplicationData={setApplicationData}
applicationData={applicationData}
/>
</div>
),
},
]}
/>
</div>
<div className="flex flex-row gap-6 justify-center">
<Button
title="Lukk"
color="white"
onClick={() => setIsEditing(false)}
/>
<Button title="Lagre endringer" color="blue" onClick={submitEdit} />
</div>
</div>
</div>
</>
);
};

export default ApplicationEditModal;
65 changes: 40 additions & 25 deletions components/form/RadioInput.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
interface Props {
updateInputValues: Function;
label: string;
values: any[][];
values: [string, string][];
defaultValue?: string;
}

const RadioInput = (props: Props) => {
const handleInputChange = (e: React.BaseSyntheticEvent) => {
props.updateInputValues(e.target.value);
const RadioInput = ({
updateInputValues,
label,
values,
defaultValue,
}: Props) => {
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
updateInputValues(e.target.value);
};

return (
<div className="max-w-xs mx-auto my-6">
<label className="inline-block mb-2 text-gray-700 dark:text-white form-label">
{props.label}
</label>
<div onChange={(e) => handleInputChange(e)}>
{props.values.map((option, index) => {
return (
<div key={index} className="flex">
<input
required
className="float-left w-4 h-4 mt-1 mr-2 align-top transition duration-200 bg-center bg-no-repeat bg-contain border rounded-full appearance-none cursor-pointer focus:outline-none border-gray-300 checked:bg-online-darkTeal dark:bg-online-darkBlue dark:border-gray-700 dark:checked:bg-online-darkTeal"
type="radio"
value={option[1]}
name={props.label}
/>
<label className="inline-block text-gray-800 dark:text-white">
{option[0]}
</label>
</div>
);
})}
</div>
<fieldset>
<legend className="inline-block mb-2 text-gray-700 dark:text-white form-label">
{label}
</legend>
<div>
{values.map(([displayText, value], index) => {
const inputId = `${label}-${value}-${index}`;
return (
<div key={value} className="flex items-center mb-2">
<input
id={inputId}
required
className="w-4 h-4 mt-1 mr-2 align-top transition duration-200 bg-center bg-no-repeat bg-contain border rounded-full appearance-none cursor-pointer focus:outline-none border-gray-300 checked:bg-online-darkTeal dark:bg-online-darkBlue dark:border-gray-700 dark:checked:bg-online-darkTeal"
type="radio"
value={value}
name={label}
defaultChecked={defaultValue === value}
onChange={handleInputChange}
/>
<label
htmlFor={inputId}
className="inline-block text-gray-800 dark:text-white"
>
{displayText}
</label>
</div>
);
})}
</div>
</fieldset>
</div>
);
};
Expand Down
24 changes: 24 additions & 0 deletions lib/api/applicantApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ export const createApplicant = async (applicant: applicantType) => {
return data;
};

export const editApplicant = async ({
applicant,
periodId,
owId,
}: {
applicant: applicantType;
periodId: string;
owId: string;
}) => {
const response = await fetch(`/api/applicants/${periodId}/${owId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ application: 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,
Expand Down
25 changes: 25 additions & 0 deletions lib/mongo/applicants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,31 @@ export const getApplicantsForCommittee = async (
}
};

export const editApplication = async (
owId: string,
periodId: string | ObjectId,
newApplicationData: applicantType
) => {
try {
if (!applicants) await init();

const result = await applicants.findOneAndUpdate(
{ owId: owId, periodId: periodId },
{ $set: newApplicationData },
{ returnDocument: "after" }
);

if (result) {
return { applicant: result };
} else {
return { error: "Application not found" };
}
} catch (error) {
console.error(error);
return { error: "Failed to edit application" };
}
};

export const deleteApplication = async (
owId: string,
periodId: string | ObjectId
Expand Down
Loading