diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index 2d1132ab8..30079e0fb 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -565,6 +565,7 @@ samfundet__interview_list = 'samfundet:interview-list' samfundet__interview_detail = 'samfundet:interview-detail' samfundet__api_root = 'samfundet:api-root' +samfundet__api_root = 'samfundet:api-root' samfundet__schema = 'samfundet:schema' samfundet__swagger_ui = 'samfundet:swagger_ui' samfundet__redoc = 'samfundet:redoc' @@ -608,5 +609,6 @@ samfundet__recruitment_availability = 'samfundet:recruitment_availability' samfundet__feedback = 'samfundet:feedback' samfundet__purchase_feedback = 'samfundet:purchase_feedback' +samfundet__gang_application_stats = 'samfundet:gang-application-stats' static__path = '' media__path = '' diff --git a/backend/samfundet/urls.py b/backend/samfundet/urls.py index 3761d75c3..0fab9f6d5 100644 --- a/backend/samfundet/urls.py +++ b/backend/samfundet/urls.py @@ -145,4 +145,5 @@ path('recruitment//availability/', views.RecruitmentAvailabilityView.as_view(), name='recruitment_availability'), path('feedback/', views.UserFeedbackView.as_view(), name='feedback'), path('purchase-feedback/', views.PurchaseFeedbackView.as_view(), name='purchase_feedback'), + path('recruitment//gang//stats/', views.GangApplicationCountView.as_view(), name='gang-application-stats'), ] diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index 4f3f8c509..a566688b0 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -137,6 +137,7 @@ Recruitment, InterviewRoom, OccupiedTimeslot, + RecruitmentGangStat, RecruitmentPosition, RecruitmentStatistics, RecruitmentApplication, @@ -1366,3 +1367,21 @@ def post(self, request: Request) -> Response: form=purchase_model, ) return Response(status=status.HTTP_201_CREATED, data={'message': 'Feedback submitted successfully!'}) + + +class GangApplicationCountView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request: Request, recruitment_id: int, gang_id: int) -> Response: + # Get total applications from RecruitmentGangStat + gang_stat = get_object_or_404(RecruitmentGangStat, gang_id=gang_id, recruitment_stats__recruitment_id=recruitment_id) + + return Response( + { + 'total_applications': gang_stat.application_count, + 'total_applicants': gang_stat.applicant_count, + 'average_priority': gang_stat.average_priority, + 'total_accepted': gang_stat.total_accepted, + 'total_rejected': gang_stat.total_rejected, + } + ) diff --git a/frontend/src/Components/RejectionMail/RejectionMail.tsx b/frontend/src/Components/RejectionMail/RejectionMail.tsx index 7109290e1..a97215347 100644 --- a/frontend/src/Components/RejectionMail/RejectionMail.tsx +++ b/frontend/src/Components/RejectionMail/RejectionMail.tsx @@ -2,7 +2,7 @@ import { t } from 'i18next'; import { useState } from 'react'; import { useParams } from 'react-router-dom'; import { toast } from 'react-toastify'; -import { postRejectionMail } from '~/api'; +//import { postRejectionMail } from '~/api'; import { KEY } from '~/i18n/constants'; import { Button } from '../Button'; import { InputField } from '../InputField'; @@ -15,7 +15,7 @@ export function RejectionMail() { function handleSubmit() { if (recruitmentId) { - postRejectionMail(recruitmentId, { subject, text }); + //postRejectionMail(recruitmentId, { subject, text }); toast.success(t(KEY.common_save_successful)); } else { toast.error(t(KEY.common_something_went_wrong)); diff --git a/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterviewGangPage/RecruitmentUsersWithoutInterviewGangPage.tsx b/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterviewGangPage/RecruitmentUsersWithoutInterviewGangPage.tsx index 3098a460e..719029554 100644 --- a/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterviewGangPage/RecruitmentUsersWithoutInterviewGangPage.tsx +++ b/frontend/src/PagesAdmin/RecruitmentUsersWithoutInterviewGangPage/RecruitmentUsersWithoutInterviewGangPage.tsx @@ -1,10 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { toast } from 'react-toastify'; import { RecruitmentWithoutInterviewTable } from '~/Components'; import { Text } from '~/Components/Text/Text'; -import { getApplicantsWithoutInterviews, getGang, getRecruitment } from '~/api'; +import { getApplicantsWithoutInterviews, getGang, getRecruitment, getRecruitmentGangStats } from '~/api'; import type { GangDto, RecruitmentDto, RecruitmentUserDto } from '~/dto'; import { useCustomNavigate, useTitle } from '~/hooks'; import { STATUS } from '~/http_status_codes'; @@ -18,6 +19,7 @@ export function RecruitmentUsersWithoutInterviewGangPage() { const { recruitmentId, gangId } = useParams(); const [users, setUsers] = useState([]); const [recruitment, setRecruitment] = useState(); + const [withoutInterviewCount, setWithoutInterviewCount] = useState(); const [gang, setGang] = useState(); const [showSpinner, setShowSpinner] = useState(true); const { t } = useTranslation(); @@ -29,6 +31,7 @@ export function RecruitmentUsersWithoutInterviewGangPage() { getApplicantsWithoutInterviews(recruitmentId, gangId) .then((response) => { setUsers(response.data); + setWithoutInterviewCount(response.data.length); setShowSpinner(false); }) .catch((error) => { @@ -62,16 +65,21 @@ export function RecruitmentUsersWithoutInterviewGangPage() { setRecruitment(resp.data); }) .catch((data) => { - // TODO add error pop up message? if (data.request.status === STATUS.HTTP_404_NOT_FOUND) { + toast.error(t(KEY.common_something_went_wrong)); + console.error(data); navigate({ url: ROUTES.frontend.not_found, replace: true }); } - toast.error(t(KEY.common_something_went_wrong)); - console.error(data); }); } }, [recruitmentId]); + const { data: gangStats } = useQuery({ + queryKey: ['recruitmentGangStats', recruitmentId, gangId], + queryFn: () => getRecruitmentGangStats(recruitmentId as string, gangId as string), + enabled: Boolean(typeof recruitmentId === 'string' && typeof gangId === 'string'), + }); + const title = t(KEY.recruitment_applicants_without_interview); useTitle(title); const header = ( @@ -88,6 +96,19 @@ export function RecruitmentUsersWithoutInterviewGangPage() { ); return ( + {gangStats && ( + + {[ + withoutInterviewCount, + t(KEY.common_out_of), + t(KEY.common_total).toLowerCase(), + gangStats.data.total_applicants, + t(KEY.recruitment_applications), + t(KEY.common_have), + t(KEY.recruitment_interview), + ].join(' ')} + + )} ); diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 9a7997519..f959486cf 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -15,7 +15,6 @@ import type { InterviewDto, InterviewRoomDto, KeyValueDto, - MailDto, MenuDto, MenuItemDto, OccupiedTimeslotDto, @@ -29,6 +28,7 @@ import type { RecruitmentAvailabilityDto, RecruitmentDto, RecruitmentGangDto, + RecruitmentGangStatDto, RecruitmentPositionDto, RecruitmentPositionPostDto, RecruitmentPositionPutDto, @@ -1088,16 +1088,18 @@ export async function postFeedback(feedbackData: FeedbackDto): Promise { +export async function getRecruitmentGangStats( + recruitmentId: string, + gangId: string, +): Promise> { const url = BACKEND_DOMAIN + reverse({ - pattern: ROUTES.backend.samfundet__rejected_applicants, - queryParams: { - recruitment: recruitmentId, + pattern: ROUTES.backend.samfundet__gang_application_stats, + urlParams: { + recruitmentId: recruitmentId, + gangId: gangId, }, }); - const response = await axios.post(url, rejectionMail, { withCredentials: true }); - - return response; + return await axios.get(url, { withCredentials: true }); } diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 3a538bd24..c7e5fd1f4 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -556,9 +556,8 @@ export type RecruitmentCampusStatDto = { }; export type RecruitmentGangStatDto = { - gang: string; - application_count: number; - applicant_count: number; + total_applications: number; + total_applicants: number; average_priority: number; total_accepted: number; total_rejected: number; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index a237bf674..e8642e9c8 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -124,6 +124,7 @@ export const KEY = { common_email: 'common_email', common_email_subject: 'common_email_subject', common_total: 'common_total', + common_out_of: 'common_out_of', common_roles: 'common_roles', common_role: 'common_role', common_guests: 'common_guests', @@ -169,6 +170,7 @@ export const KEY = { common_something_went_wrong: 'common_something_went_wrong', common_click_here: 'common_click_here', common_have: 'common_have', + recruitment_interview: 'recruitment_interview', common_been: 'common_been', common_processed: 'common_processed', common_rejected: 'common_rejected', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index 5e693cd43..7fb9b2861 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -150,6 +150,7 @@ export const nb = prepareTranslations({ [KEY.common_something_went_wrong]: 'Noe gikk galt', [KEY.common_click_here]: 'klikk her', [KEY.common_total]: 'Totalt', + [KEY.common_out_of]: 'av', [KEY.common_count]: 'Antall', [KEY.common_guests]: 'Gjester', [KEY.common_occasion]: 'Annledning', @@ -261,6 +262,7 @@ export const nb = prepareTranslations({ [KEY.recruitment_all_applications]: 'Alle søknader', [KEY.recruitment_not_applied]: 'Du har ikke sendt søknader til noen stillinger ennå', [KEY.recruitment_will_be_anonymized]: 'All info relatert til dine søknader vil bli slettet 3 uker etter opptaket', + [KEY.recruitment_interview]: 'intervju', [KEY.recruitment_interviews]: 'Intervjuer', [KEY.recruitment_interview_planned]: 'Intervjuer planlagt', [KEY.recruitment_interviewer]: 'Intervjuer', @@ -637,6 +639,7 @@ export const en = prepareTranslations({ [KEY.common_click_here]: 'click here', [KEY.common_total]: 'In total', [KEY.common_count]: 'Number of', + [KEY.common_out_of]: 'out of', [KEY.common_guests]: 'Guests', [KEY.common_occasion]: 'Occasion', [KEY.common_have]: 'have', @@ -747,6 +750,7 @@ export const en = prepareTranslations({ [KEY.recruitment_not_applied]: 'You have not applied to any positions yet', [KEY.recruitment_will_be_anonymized]: 'All info related to the applications will be anonymized three weeks after the recruitment is over', + [KEY.recruitment_interview]: 'interview', [KEY.recruitment_interviews]: 'Interviews', [KEY.recruitment_interview_planned]: 'Interviews planned', [KEY.recruitment_interviewer]: 'Interviewer', diff --git a/frontend/src/routes/backend.ts b/frontend/src/routes/backend.ts index cea34268e..b10b02f78 100644 --- a/frontend/src/routes/backend.ts +++ b/frontend/src/routes/backend.ts @@ -607,6 +607,7 @@ export const ROUTES_BACKEND = { samfundet__recruitment_availability: '/recruitment/:id/availability/', samfundet__feedback: '/feedback/', samfundet__purchase_feedback: '/purchase-feedback/', + samfundet__gang_application_stats: '/recruitment/:recruitmentId/gang/:gangId/stats/', static__path: '/static/:path', media__path: '/media/:path', -} as const; \ No newline at end of file +} as const;