diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index fc1b27776..bb5718847 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -596,6 +596,7 @@ samfundet__recruitment_application_update_state_gang = 'samfundet:recruitment_application_update_state_gang' samfundet__recruitment_application_update_state_position = 'samfundet:recruitment_application_update_state_position' samfundet__recruitment_applications_recruiter = 'samfundet:recruitment_applications_recruiter' +samfundet__recruitment_application_interview_notes = 'samfundet:recruitment_application_interview_notes' samfundet__recruitment_withdraw_application = 'samfundet:recruitment_withdraw_application' samfundet__recruitment_user_priority_update = 'samfundet:recruitment_user_priority_update' samfundet__recruitment_withdraw_application_recruiter = 'samfundet:recruitment_withdraw_application_recruiter' diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index d19f08ce7..6c4767952 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -1060,6 +1060,7 @@ class RecruitmentApplicationForRecruiterSerializer(serializers.ModelSerializer): recruitment_position = RecruitmentPositionForApplicantSerializer() recruiter_priority = serializers.CharField(source='get_recruiter_priority_display') interview_time = serializers.SerializerMethodField(method_name='get_interview_time', read_only=True) + interview = InterviewSerializer(read_only=True) class Meta: model = RecruitmentApplication @@ -1075,6 +1076,7 @@ class Meta: 'recruiter_priority', 'withdrawn', 'interview_time', + 'interview', 'created_at', ] read_only_fields = [ @@ -1089,6 +1091,7 @@ class Meta: 'applicant_state', 'interview_time', 'withdrawn', + 'interview', 'created_at', ] diff --git a/backend/samfundet/urls.py b/backend/samfundet/urls.py index 940ac346f..546dedc9b 100644 --- a/backend/samfundet/urls.py +++ b/backend/samfundet/urls.py @@ -116,6 +116,11 @@ views.RecruitmentApplicationForRecruitersView.as_view(), name='recruitment_applications_recruiter', ), + path( + 'recruitment-application-interview-notes//', + views.RecruitmentApplicationInterviewNotesView.as_view(), + name='recruitment_application_interview_notes', + ), path('recruitment-withdraw-application//', views.RecruitmentApplicationWithdrawApplicantView.as_view(), name='recruitment_withdraw_application'), path('recruitment-user-priority-update//', views.RecruitmentApplicationApplicantPriorityView.as_view(), name='recruitment_user_priority_update'), path( diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index 59666f8aa..938014829 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -933,6 +933,20 @@ def list(self, request: Request) -> Response: return Response(serializer.data) +class RecruitmentApplicationInterviewNotesView(APIView): + permission_classes = [IsAuthenticated] + serializer_class = InterviewSerializer + + def put(self, request: Request, interview_id: str) -> Response: + interview = get_object_or_404(Interview, pk=interview_id) + update_serializer = self.serializer_class(interview, data=request.data, partial=True) + if update_serializer.is_valid() and 'notes' in update_serializer.validated_data: + interview.notes = update_serializer.validated_data['notes'] + interview.save() + return Response(status=status.HTTP_200_OK) + return Response(update_serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + class RecruitmentApplicationWithdrawApplicantView(APIView): permission_classes = [IsAuthenticated] diff --git a/frontend/src/PagesAdmin/RecruitmentApplicantAdminPage/RecruitmentApplicantAdminPage.tsx b/frontend/src/PagesAdmin/RecruitmentApplicantAdminPage/RecruitmentApplicantAdminPage.tsx index 1585bb699..be8e8d6fc 100644 --- a/frontend/src/PagesAdmin/RecruitmentApplicantAdminPage/RecruitmentApplicantAdminPage.tsx +++ b/frontend/src/PagesAdmin/RecruitmentApplicantAdminPage/RecruitmentApplicantAdminPage.tsx @@ -37,6 +37,7 @@ export function RecruitmentApplicantAdminPage() { const applicant = data?.data.user; const otherRecruitmentApplications = data?.data.other_applications; const interviewNotes = recruitmentApplication?.interview?.notes; + const interviewId = recruitmentApplication?.interview?.id; const adminWithdraw = useMutation({ mutationFn: (id: string) => { @@ -84,6 +85,11 @@ export function RecruitmentApplicantAdminPage() { {recruitmentApplication?.application_text} + {interviewId && ( +
+ +
+ )}
{recruitmentApplication?.withdrawn ? ( @@ -102,10 +108,6 @@ export function RecruitmentApplicantAdminPage() { )}
-
- -
-
{t(KEY.recruitment_all_applications)} diff --git a/frontend/src/PagesAdmin/RecruitmentApplicantAdminPage/RecruitmentInterviewNotesForm.tsx b/frontend/src/PagesAdmin/RecruitmentApplicantAdminPage/RecruitmentInterviewNotesForm.tsx index 39b9de9d4..c9bb0f72b 100644 --- a/frontend/src/PagesAdmin/RecruitmentApplicantAdminPage/RecruitmentInterviewNotesForm.tsx +++ b/frontend/src/PagesAdmin/RecruitmentApplicantAdminPage/RecruitmentInterviewNotesForm.tsx @@ -1,32 +1,54 @@ import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation } from '@tanstack/react-query'; +import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { toast } from 'react-toastify'; import { z } from 'zod'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Textarea } from '~/Components'; +import { putRecrutmentInterviewNotes } from '~/api'; import { KEY } from '~/i18n/constants'; const recruitmentNotesSchema = z.object({ notes: z.string(), + interviewId: z.number(), }); type RecruitmentInterviewNotesFormType = z.infer; interface RecruitmentInterviewNotesFormProps { initialData: Partial; + interviewId?: number; } -export function RecruitmentInterviewNotesForm({ initialData }: RecruitmentInterviewNotesFormProps) { +export function RecruitmentInterviewNotesForm({ initialData, interviewId }: RecruitmentInterviewNotesFormProps) { const { t } = useTranslation(); - + const [currentNotes, setCurrentNotes] = useState(initialData.notes || ''); const form = useForm({ resolver: zodResolver(recruitmentNotesSchema), - defaultValues: initialData, + defaultValues: { + notes: initialData.notes || '', + interviewId: interviewId || 0, + }, + }); + + const handleUpdateNotes = useMutation({ + mutationFn: ({ notes, interviewId }: { notes: string; interviewId: number }) => + putRecrutmentInterviewNotes(notes, interviewId), + onSuccess: () => { + toast.success(t(KEY.common_update_successful)); + }, + onError: (error) => { + toast.error(t(KEY.common_something_went_wrong)); + }, }); - function handleUpdateNotes(value: string) { - // TODO: Update notes using a put request - console.log(value); - } + const handleNotesChange = (newNotes: string) => { + if (newNotes !== currentNotes && interviewId) { + setCurrentNotes(newNotes); + handleUpdateNotes.mutate({ notes: newNotes, interviewId }); + } + }; return (
@@ -43,7 +65,7 @@ export function RecruitmentInterviewNotesForm({ initialData }: RecruitmentInterv {...field} onBlur={(newNotes) => { field.onBlur(); // Call the default onBlur handler from react-hook-form - handleUpdateNotes(newNotes.target.value); // Call your custom function on blur + handleNotesChange(newNotes.target.value); // Call your custom function on blur }} /> diff --git a/frontend/src/api.ts b/frontend/src/api.ts index ed62fcd69..7b12fb0d8 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -978,6 +978,16 @@ export async function putRecruitmentApplication( return response; } +export async function putRecrutmentInterviewNotes(notes: string, interviewId: number): Promise { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__recruitment_application_interview_notes, + urlParams: { interviewId: interviewId }, + }); + return await axios.put(url, { notes: notes }, { withCredentials: true }); +} + export async function getRecruitmentApplicationForPosition( positionId: string, ): Promise> { diff --git a/frontend/src/routes/backend.ts b/frontend/src/routes/backend.ts index 85145b038..dedac6665 100644 --- a/frontend/src/routes/backend.ts +++ b/frontend/src/routes/backend.ts @@ -594,6 +594,7 @@ export const ROUTES_BACKEND = { samfundet__recruitment_application_update_state_gang: '/recruitment-application-update-state-gang/:pk/', samfundet__recruitment_application_update_state_position: '/recruitment-application-update-state-position/:pk/', samfundet__recruitment_applications_recruiter: '/recruitment-application-recruiter/:applicationId/', + samfundet__recruitment_application_interview_notes: '/recruitment-application-interview-notes/:interviewId/', samfundet__recruitment_withdraw_application: '/recruitment-withdraw-application/:pk/', samfundet__recruitment_user_priority_update: '/recruitment-user-priority-update/:pk/', samfundet__recruitment_withdraw_application_recruiter: '/recruitment-withdraw-application-recruiter/:pk/',