Skip to content

Commit

Permalink
Merge branch 'master' into 1494-add-search-to-admin-users-page
Browse files Browse the repository at this point in the history
  • Loading branch information
Lidavic authored Feb 6, 2025
2 parents 6e03c38 + 39f65fc commit e2447f3
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 17 deletions.
1 change: 1 addition & 0 deletions backend/root/utils/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,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'
Expand Down
3 changes: 3 additions & 0 deletions backend/samfundet/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,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
Expand All @@ -1093,6 +1094,7 @@ class Meta:
'recruiter_priority',
'withdrawn',
'interview_time',
'interview',
'created_at',
]
read_only_fields = [
Expand All @@ -1107,6 +1109,7 @@ class Meta:
'applicant_state',
'interview_time',
'withdrawn',
'interview',
'created_at',
]

Expand Down
5 changes: 5 additions & 0 deletions backend/samfundet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@
views.RecruitmentApplicationForRecruitersView.as_view(),
name='recruitment_applications_recruiter',
),
path(
'recruitment-application-interview-notes/<int:interview_id>/',
views.RecruitmentApplicationInterviewNotesView.as_view(),
name='recruitment_application_interview_notes',
),
path('recruitment-withdraw-application/<int:pk>/', views.RecruitmentApplicationWithdrawApplicantView.as_view(), name='recruitment_withdraw_application'),
path('recruitment-user-priority-update/<slug:pk>/', views.RecruitmentApplicationApplicantPriorityView.as_view(), name='recruitment_user_priority_update'),
path(
Expand Down
14 changes: 14 additions & 0 deletions backend/samfundet/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,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]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function RecruitmentApplicantAdminPage() {
const recruitmentApplication = data?.data.application;
const applicant = data?.data.user;
const interviewNotes = recruitmentApplication?.interview?.notes;
const interviewId = recruitmentApplication?.interview?.id;

if (isLoading) {
return (
Expand All @@ -64,25 +65,21 @@ export function RecruitmentApplicantAdminPage() {
</Text>
<Text>{recruitmentApplication?.application_text}</Text>
</div>
{interviewId && (
<div className={classNames(styles.infoContainer)}>
<RecruitmentInterviewNotesForm initialData={initialData} interviewId={interviewId} />
</div>
)}
<div className={styles.withdrawContainer}>
<RecruitmentApplicantWithdraw application={data?.data.application} />
</div>
<div className={classNames(styles.infoContainer)}>
<RecruitmentInterviewNotesForm initialData={initialData} />
</div>
<div className={classNames(styles.infoContainer)}>
<Text size="l" as="strong" className={styles.textBottom}>
{t(KEY.recruitment_applications)} {t(KEY.common_in)}{' '}
{dbT(data?.data.application.recruitment_position.gang, 'name')}
</Text>
<RecruitmentApplicantAllApplications applications={data?.data.other_applications} />
</div>
<div className={classNames(styles.infoContainer)}>
<Text size="l" as="strong" className={styles.textBottom}>
{t(KEY.recruitment_all_applications)}
</Text>
<RecruitmentApplicantAllApplications applications={data?.data.other_applications} />
</div>
</AdminPage>
);
}
Original file line number Diff line number Diff line change
@@ -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<typeof recruitmentNotesSchema>;

interface RecruitmentInterviewNotesFormProps {
initialData: Partial<RecruitmentInterviewNotesFormType>;
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<RecruitmentInterviewNotesFormType>({
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 (
<Form {...form}>
Expand All @@ -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
}}
/>
</FormControl>
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,16 @@ export async function putRecruitmentApplication(
return response;
}

export async function putRecrutmentInterviewNotes(notes: string, interviewId: number): Promise<AxiosResponse> {
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<AxiosResponse<RecruitmentApplicationDto>> {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/routes/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/',
Expand Down

0 comments on commit e2447f3

Please sign in to comment.