diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index 2d1132ab8..4008c6524 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -544,6 +544,8 @@ samfundet__recruitment_list = 'samfundet:recruitment-list' samfundet__recruitment_detail = 'samfundet:recruitment-detail' samfundet__recruitment_gangs = 'samfundet:recruitment-gangs' +samfundet__recruitment_sharedinterviewgroups_list = 'samfundet:recruitment_sharedinterviewgroups-list' +samfundet__recruitment_sharedinterviewgroups_detail = 'samfundet:recruitment_sharedinterviewgroups-detail' samfundet__recruitment_for_recruiter_list = 'samfundet:recruitment_for_recruiter-list' samfundet__recruitment_for_recruiter_detail = 'samfundet:recruitment_for_recruiter-detail' samfundet__recruitment_stats_list = 'samfundet:recruitment_stats-list' @@ -565,6 +567,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' diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 371910607..439ae8708 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -934,8 +934,6 @@ class Meta: class RecruitmentPositionSharedInterviewGroupSerializer(serializers.ModelSerializer): - positions = RecruitmentPositionForApplicantSerializer(many=True, read_only=True) - class Meta: model = RecruitmentPositionSharedInterviewGroup fields = [ @@ -946,6 +944,11 @@ class Meta: 'name_nb', ] + def to_representation(self, instance: RecruitmentPositionSharedInterviewGroup) -> dict: + data = super().to_representation(instance) + data['positions'] = RecruitmentPositionForApplicantSerializer(instance.positions, many=True).data + return data + class RecruitmentApplicationForApplicantSerializer(CustomBaseSerializer): interview = ApplicantInterviewSerializer(read_only=True) diff --git a/backend/samfundet/urls.py b/backend/samfundet/urls.py index 3761d75c3..b2b8ab36e 100644 --- a/backend/samfundet/urls.py +++ b/backend/samfundet/urls.py @@ -41,6 +41,7 @@ ########## Recruitment ########## router.register('recruitment', views.RecruitmentView, 'recruitment') +router.register('recruitment-sharedinterviewgroup', views.RecruitmentSharedInterviewPositionsView, 'recruitment_sharedinterviewgroups') router.register('recruitment-for-recruiter', views.RecruitmentForRecruiterView, 'recruitment_for_recruiter') router.register('recruitment-stats', views.RecruitmentStatisticsView, 'recruitment_stats') router.register('recruitment-separateposition', views.RecruitmentSeparatePositionView, 'recruitment_separateposition') @@ -90,7 +91,7 @@ ), path( 'recruitment-shared-interview-groups//', - views.RecruitmentInterviewGroupView.as_view(), + views.RecruitmentSharedInterviewPositionsRecruitmentView.as_view(), name='recruitment_shared_interviews', ), path('recruitment-positions-gang-for-gangs/', views.RecruitmentPositionsPerGangForGangView.as_view(), name='recruitment_positions_gang_for_gangs'), diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index e4099a47c..6cb22a266 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -653,6 +653,13 @@ class RecruitmentForRecruiterView(ModelViewSet): queryset = Recruitment.objects.all() +@method_decorator(ensure_csrf_cookie, 'dispatch') +class RecruitmentSharedInterviewPositionsView(ModelViewSet): + permission_classes = (DjangoModelPermissionsOrAnonReadOnly,) + serializer_class = RecruitmentPositionSharedInterviewGroupSerializer + queryset = RecruitmentPositionSharedInterviewGroup.objects.all() + + @method_decorator(ensure_csrf_cookie, 'dispatch') class RecruitmentStatisticsView(ModelViewSet): permission_classes = (DjangoModelPermissions,) # Allow read only to permissions on perms @@ -1131,7 +1138,7 @@ def get_queryset(self) -> Response: return Recruitment.objects.filter(visible_from__lte=timezone.now(), actual_application_deadline__gte=timezone.now()) -class RecruitmentInterviewGroupView(APIView): +class RecruitmentSharedInterviewPositionsRecruitmentView(APIView): permission_classes = [IsAuthenticated] def get( diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.module.scss b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.module.scss new file mode 100644 index 000000000..741f3c2dd --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.module.scss @@ -0,0 +1,8 @@ +@import "src/constants"; + +@import "src/mixins"; + +.button { + margin-top: 2rem; +} + diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.tsx b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.tsx index f39c9c12f..93c6fadcb 100644 --- a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.tsx +++ b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/RecruitmentGangOverviewPage.tsx @@ -1,7 +1,7 @@ import { type ReactElement, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; -import { CrudButtons, Link, type Tab, TabView } from '~/Components'; +import { Button, CrudButtons, Link, type Tab, TabView } from '~/Components'; import { Table } from '~/Components/Table'; import { deleteRecruitmentSeparatePosition, getRecruitment, getRecruitmentGangs } from '~/api'; import type { RecruitmentDto, RecruitmentGangDto, RecruitmentSeparatePositionDto } from '~/dto'; @@ -11,7 +11,9 @@ import { reverse } from '~/named-urls'; import { ROUTES } from '~/routes'; import { dbT, lowerCapitalize } from '~/utils'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; -import { AppletContainer, RecruitmentInterviewGroupsList } from './components'; +import { AppletContainer, RecruitmentSharedInterviewPositionsList } from './components'; + +import styles from './RecruitmentGangOverviewPage.module.scss'; export function RecruitmentGangOverviewPage() { const { recruitmentId } = useParams(); @@ -124,7 +126,26 @@ export function RecruitmentGangOverviewPage() { label: t(KEY.recruitment_gangs_with_separate_positions), value: , }, - { key: 3, label: t(KEY.recruitment_interview_groups), value: }, + { + key: 3, + label: t(KEY.recruitment_interview_group_create_header), + value: ( + <> + {' '} + + + ), + }, ]; }, [gangs, recruitment, t, recruitmentId, navigate, deleteSeparatePositionHandler]); diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/RecruitmentInterviewGroupsList.module.scss b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/RecruitmentSharedInterviewPositionsList.module.scss similarity index 100% rename from frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/RecruitmentInterviewGroupsList.module.scss rename to frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/RecruitmentSharedInterviewPositionsList.module.scss diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/RecruitmentInterviewGroupsList.tsx b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/RecruitmentSharedInterviewPositionsList.tsx similarity index 52% rename from frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/RecruitmentInterviewGroupsList.tsx rename to frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/RecruitmentSharedInterviewPositionsList.tsx index 9f0ef8c96..229119c7f 100644 --- a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/RecruitmentInterviewGroupsList.tsx +++ b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/RecruitmentSharedInterviewPositionsList.tsx @@ -2,20 +2,20 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { toast } from 'react-toastify'; -import { getRecruitmentSharedInterviewGroups, getRecruitmentStats } from '~/api'; -import type { RecruitmentSharedInterviewGroupDto, RecruitmentStatsDto } from '~/dto'; +import { getRecruitmentSharedInterviewPositionss, getRecruitmentStats } from '~/api'; +import type { RecruitmentSharedInterviewPositionsDto, RecruitmentStatsDto } from '~/dto'; import { KEY } from '~/i18n/constants'; -import styles from './RecruitmentInterviewGroupsList.module.scss'; -import { RecruitmentInterviewGroupComponent } from './components/RecruitmentInterviewGroupComponent/RecruitmentInterviewGroupComponent'; +import styles from './RecruitmentSharedInterviewPositionsList.module.scss'; +import { RecruitmentSharedInterviewPositionsComponent } from './components/RecruitmentSharedInterviewPositionsComponent/RecruitmentSharedInterviewPositionsComponent'; -export function RecruitmentInterviewGroupsList() { +export function RecruitmentSharedInterviewPositionsList() { const { recruitmentId } = useParams(); - const [interviewGroups, setInterviewGroups] = useState(); + const [interviewGroups, setInterviewGroups] = useState(); const { t } = useTranslation(); useEffect(() => { if (recruitmentId) { - getRecruitmentSharedInterviewGroups(recruitmentId) + getRecruitmentSharedInterviewPositionss(recruitmentId) .then((response) => { setInterviewGroups(response.data); }) @@ -28,8 +28,8 @@ export function RecruitmentInterviewGroupsList() { return (
- {interviewGroups?.map((interviewGroup: RecruitmentSharedInterviewGroupDto) => { - return ; + {interviewGroups?.map((interviewGroup: RecruitmentSharedInterviewPositionsDto) => { + return ; })}
); diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentInterviewGroupComponent/RecruitmentInterviewGroupComponent.tsx b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentInterviewGroupComponent/RecruitmentInterviewGroupComponent.tsx deleted file mode 100644 index 471c8260f..000000000 --- a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentInterviewGroupComponent/RecruitmentInterviewGroupComponent.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ExpandableHeader, Table } from '~/Components'; -import type { RecruitmentSharedInterviewGroupDto, RecruitmentStatsDto } from '~/dto'; -import { dbT } from '~/utils'; -import styles from './RecruitmentInterviewGroupComponent.module.scss'; - -type RecruitmentInterviewGroupComponentProps = { - interviewGroup: RecruitmentSharedInterviewGroupDto; -}; - -export function RecruitmentInterviewGroupComponent({ interviewGroup }: RecruitmentInterviewGroupComponentProps) { - const interviewGroupHeader = dbT(interviewGroup, 'name') ?? 'N/A'; - return ( - -
{ - return { cells: [dbT(position, 'name'), dbT(position.gang, 'name')] }; - })} - /> - - ); -} diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentInterviewGroupComponent/index.ts b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentInterviewGroupComponent/index.ts deleted file mode 100644 index 82bb123f8..000000000 --- a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentInterviewGroupComponent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { RecruitmentInterviewGroupComponent } from './RecruitmentInterviewGroupComponent'; diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentInterviewGroupComponent/RecruitmentInterviewGroupComponent.module.scss b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentSharedInterviewPositionsComponent/RecruitmentSharedInterviewPositionsComponent.module.scss similarity index 77% rename from frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentInterviewGroupComponent/RecruitmentInterviewGroupComponent.module.scss rename to frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentSharedInterviewPositionsComponent/RecruitmentSharedInterviewPositionsComponent.module.scss index ebe1ecd19..cc4f4fb6b 100644 --- a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentInterviewGroupComponent/RecruitmentInterviewGroupComponent.module.scss +++ b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentSharedInterviewPositionsComponent/RecruitmentSharedInterviewPositionsComponent.module.scss @@ -16,3 +16,9 @@ width: 100%; flex: 1; } + +.footer { + justify-content: end; + display: flex; + padding: 0.5em; +} diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentSharedInterviewPositionsComponent/RecruitmentSharedInterviewPositionsComponent.tsx b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentSharedInterviewPositionsComponent/RecruitmentSharedInterviewPositionsComponent.tsx new file mode 100644 index 000000000..5c61c6930 --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentSharedInterviewPositionsComponent/RecruitmentSharedInterviewPositionsComponent.tsx @@ -0,0 +1,48 @@ +import { useTranslation } from 'react-i18next'; +import { Button, ExpandableHeader, Table } from '~/Components'; +import type { RecruitmentSharedInterviewPositionsDto, RecruitmentStatsDto } from '~/dto'; +import { KEY } from '~/i18n/constants'; +import { reverse } from '~/named-urls'; +import { ROUTES } from '~/routes'; +import { dbT } from '~/utils'; +import styles from './RecruitmentSharedInterviewPositionsComponent.module.scss'; + +type RecruitmentSharedInterviewPositionsComponentProps = { + interviewGroup: RecruitmentSharedInterviewPositionsDto; +}; + +export function RecruitmentSharedInterviewPositionsComponent({ + interviewGroup, +}: RecruitmentSharedInterviewPositionsComponentProps) { + const interviewGroupHeader = dbT(interviewGroup, 'name') ?? 'N/A'; + const { t } = useTranslation(); + + return ( + +
{ + return { cells: [dbT(position, 'name'), dbT(position.gang, 'name')] }; + })} + /> +
+ +
+ + ); +} diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentSharedInterviewPositionsComponent/index.ts b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentSharedInterviewPositionsComponent/index.ts new file mode 100644 index 000000000..710d4fe2e --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/RecruitmentInterviewGroupsList/components/RecruitmentSharedInterviewPositionsComponent/index.ts @@ -0,0 +1 @@ +export { RecruitmentSharedInterviewPositionsComponent } from './RecruitmentSharedInterviewPositionsComponent'; diff --git a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/index.ts b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/index.ts index 3e6c4124e..5cf490d94 100644 --- a/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/index.ts +++ b/frontend/src/PagesAdmin/RecruitmentGangOverviewPage/components/index.ts @@ -1,3 +1,3 @@ export { AppletCard } from './AppletCard/AppletCard'; export { AppletContainer } from './AppletContainer/AppletContainer'; -export { RecruitmentInterviewGroupsList } from './RecruitmentInterviewGroupsList/RecruitmentInterviewGroupsList'; +export { RecruitmentSharedInterviewPositionsList } from './RecruitmentInterviewGroupsList/RecruitmentSharedInterviewPositionsList'; diff --git a/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/RecruitmentSharedInterviewPositionsForm.tsx b/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/RecruitmentSharedInterviewPositionsForm.tsx new file mode 100644 index 000000000..4e7f3c164 --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/RecruitmentSharedInterviewPositionsForm.tsx @@ -0,0 +1,168 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useEffect, useMemo, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { z } from 'zod'; +import { + Button, + Checkbox, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Input, + Textarea, +} from '~/Components'; +import type { DropdownOption } from '~/Components/Dropdown/Dropdown'; +import { MultiSelect } from '~/Components/MultiSelect'; +import { + getRecruitmentPositions, + postRecruitmentSharedInterviewPositions, + putRecruitmentPosition, + putRecruitmentSharedInterviewPositions, +} from '~/api'; +import { KEY } from '~/i18n/constants'; +import { reverse } from '~/named-urls'; +import { ROUTES } from '~/routes'; +import { NON_EMPTY_STRING } from '~/schema/strings'; +import styles from './RecruitmentSharedInterviewPositionsFormAdminPage.module.scss'; + +const schema = z.object({ + name_nb: NON_EMPTY_STRING, + name_en: NON_EMPTY_STRING, + recruitment: NON_EMPTY_STRING, + positions: z.number().array(), +}); + +type SchemaType = z.infer; + +interface FormProps { + initialData: Partial; + sharedInterviewGroupId?: string; + recruitmentId?: string; +} + +export function RecruitmentSharedInterviewPositionsForm({ + initialData, + recruitmentId, + sharedInterviewGroupId, +}: FormProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: initialData, + }); + + const [positionOptions, setPositionOptions] = useState[]>([]); + + const submitText = sharedInterviewGroupId ? t(KEY.common_save) : t(KEY.common_create); + + useEffect(() => { + if (recruitmentId) { + getRecruitmentPositions(recruitmentId).then((response) => { + setPositionOptions( + response.data + .filter( + (position) => + !position.shared_interview_group || + (sharedInterviewGroupId && Number.parseInt(sharedInterviewGroupId) === position.shared_interview_group), + ) + .map((position) => { + return { value: position.id, label: `${position.name_nb} ${position.gang.name_nb}` }; + }), + ); + }); + } + }, [recruitmentId, sharedInterviewGroupId]); + + const onSubmit = (data: SchemaType) => { + const updatedSharedInterviewGroup = { + ...data, + recruitment: recruitmentId ?? '', + }; + + const action = sharedInterviewGroupId + ? putRecruitmentSharedInterviewPositions(sharedInterviewGroupId, updatedSharedInterviewGroup) + : postRecruitmentSharedInterviewPositions(updatedSharedInterviewGroup); + + action + .then(() => { + toast.success(sharedInterviewGroupId ? t(KEY.common_update_successful) : t(KEY.common_creation_successful)); + navigate( + reverse({ + pattern: ROUTES.frontend.admin_recruitment_gang_overview, + urlParams: { recruitmentId }, + }), + ); + }) + .catch((error) => { + toast.error(t(KEY.common_something_went_wrong)); + console.error(error); + }); + }; + + useEffect(() => { + form.reset(initialData); + }, [initialData, form]); + + return ( +
+ +
+
+ ( + + {`${t(KEY.common_name)} ${t(KEY.common_norwegian)}`} + + + + + + )} + /> + ( + + {`${t(KEY.common_name)} ${t(KEY.common_english)}`} + + + + + + )} + /> +
+
+ ( + + {t(KEY.recruitment_positions)} + + + + + + )} + /> +
+ +
+ + + ); +} diff --git a/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/RecruitmentSharedInterviewPositionsFormAdminPage.module.scss b/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/RecruitmentSharedInterviewPositionsFormAdminPage.module.scss new file mode 100644 index 000000000..16d66675c --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/RecruitmentSharedInterviewPositionsFormAdminPage.module.scss @@ -0,0 +1,32 @@ +@import 'src/mixins'; + +@import 'src/constants'; + +.wrapper { + margin: 0; + width: 100%; + display: flex; + flex-direction: column; +} + +.row { + display: flex; + flex-direction: column; + margin-top: 1em; + margin-bottom: 1em; + gap: 1em; + @include for-tablet-up { + flex-direction: row; + } +} + +.spinner { + display: flex; + justify-content: center; + margin-top: 20%; +} + +.item { + flex-grow: 1; + max-width: 50%; +} diff --git a/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/RecruitmentSharedInterviewPositionsFormAdminPage.tsx b/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/RecruitmentSharedInterviewPositionsFormAdminPage.tsx new file mode 100644 index 000000000..1d8695afc --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/RecruitmentSharedInterviewPositionsFormAdminPage.tsx @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { getRecruitmentSharedInterviewPositions } from '~/api'; +import type { RecruitmentSharedInterviewPositionsDto, RecruitmentSharedInterviewPositionsPostDto } from '~/dto'; +import { useTitle } from '~/hooks'; +import { KEY } from '~/i18n/constants'; +import { reverse } from '~/named-urls'; +import { ROUTES } from '~/routes'; +import { dbT } from '~/utils'; +import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; +import { RecruitmentSharedInterviewPositionsForm } from './RecruitmentSharedInterviewPositionsForm'; + +export function RecruitmentSharedInterviewPositionsFormAdminPage() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { recruitmentId, sharedInterviewGroupId } = useParams(); + const [sharedInterview, setSharedInterview] = useState>(); + + useEffect(() => { + if (sharedInterviewGroupId && recruitmentId) { + getRecruitmentSharedInterviewPositions(sharedInterviewGroupId) + .then((data) => { + setSharedInterview(data.data); + }) + .catch(() => { + toast.error(t(KEY.common_something_went_wrong)); + navigate( + reverse({ + pattern: ROUTES.frontend.admin_recruitment_gang_overview, + urlParams: { recruitmentId }, + }), + { replace: true }, + ); + }); + } + }, [sharedInterviewGroupId, recruitmentId, navigate, t]); + + const initialData: Partial = { + name_nb: sharedInterview?.name_nb || '', + name_en: sharedInterview?.name_en || '', + recruitment: recruitmentId, + positions: sharedInterview?.positions?.map((pos) => pos.id) || [], + }; + + const title = sharedInterviewGroupId + ? t(KEY.recruitment_interview_group_edit_header) + : t(KEY.recruitment_interview_group_create_header); + + useTitle(title); + + return ( + + + + ); +} diff --git a/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/index.ts b/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/index.ts new file mode 100644 index 000000000..a2eec0a50 --- /dev/null +++ b/frontend/src/PagesAdmin/RecruitmentSharedInterviewPositionsFormAdminPage/index.ts @@ -0,0 +1 @@ +export { RecruitmentSharedInterviewPositionsFormAdminPage } from './RecruitmentSharedInterviewPositionsFormAdminPage'; diff --git a/frontend/src/PagesAdmin/index.ts b/frontend/src/PagesAdmin/index.ts index d1ef78eba..8b69912e2 100644 --- a/frontend/src/PagesAdmin/index.ts +++ b/frontend/src/PagesAdmin/index.ts @@ -20,6 +20,7 @@ export { RecruitmentGangAllApplicantsAdminPage } from './RecruitmentGangAllAppli export { RecruitmentGangOverviewPage } from './RecruitmentGangOverviewPage'; export { RecruitmentOpenToOtherPositionsPage } from './RecruitmentOpenToOtherPositionsPage'; export { RecruitmentOverviewPage } from './RecruitmentOverviewPage'; +export { RecruitmentSharedInterviewPositionsFormAdminPage } from './RecruitmentSharedInterviewPositionsFormAdminPage'; export { RecruitmentPositionFormAdminPage } from './RecruitmentPositionFormAdminPage'; export { RecruitmentPositionOverviewPage } from './RecruitmentPositionOverviewPage'; export { RecruitmentRecruiterDashboardPage } from './RecruitmentRecruiterDashboardPage'; diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 9a7997519..e13985980 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -33,7 +33,8 @@ import type { RecruitmentPositionPostDto, RecruitmentPositionPutDto, RecruitmentSeparatePositionDto, - RecruitmentSharedInterviewGroupDto, + RecruitmentSharedInterviewPositionsDto, + RecruitmentSharedInterviewPositionsPostDto, RecruitmentStatsDto, RecruitmentUnprocessedApplicationsDto, RecruitmentUserDto, @@ -819,9 +820,9 @@ export async function getRecruitmentApplicationsForGang( return await axios.get(url, { withCredentials: true }); } -export async function getRecruitmentSharedInterviewGroups( +export async function getRecruitmentSharedInterviewPositionss( recruitmentId: string, -): Promise> { +): Promise> { const url = BACKEND_DOMAIN + reverse({ @@ -833,6 +834,47 @@ export async function getRecruitmentSharedInterviewGroups( return await axios.get(url, { withCredentials: true }); } +export async function getRecruitmentSharedInterviewPositions( + sharedInterviewGroupId: string, +): Promise> { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__recruitment_sharedinterviewgroups_detail, + urlParams: { + pk: sharedInterviewGroupId, + }, + }); + return await axios.get(url, { withCredentials: true }); +} + +export async function putRecruitmentSharedInterviewPositions( + sharedInterviewGroupId: string, + sharedInterviewGroup: Partial, +): Promise { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__recruitment_sharedinterviewgroups_detail, + urlParams: { + pk: sharedInterviewGroupId, + }, + }); + return await axios.put(url, sharedInterviewGroup, { withCredentials: true }); +} + +export async function postRecruitmentSharedInterviewPositions( + sharedInterviewGroup: Partial, +): Promise { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__recruitment_sharedinterviewgroups_list, + }); + console.log(sharedInterviewGroup); + return await axios.post(url, sharedInterviewGroup, { withCredentials: true }); +} + export async function downloadCSVGangRecruitment(recruitmentId: string, gangId: string): Promise { const url = BACKEND_DOMAIN + diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 3a538bd24..dcd5f6400 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -428,7 +428,7 @@ export type RecruitmentSeparatePositionDto = { recruitment?: string; }; -export type RecruitmentSharedInterviewGroupDto = { +export type RecruitmentSharedInterviewPositionsDto = { id?: number; recruitment?: string; name_nb: string; @@ -436,6 +436,10 @@ export type RecruitmentSharedInterviewGroupDto = { positions: RecruitmentPositionDto[]; }; +export type RecruitmentSharedInterviewPositionsPostDto = Omit & { + positions: number[]; +}; + export type UserPriorityDto = { direction: number; }; @@ -465,6 +469,8 @@ export type RecruitmentPositionDto = { interviewers?: UserDto[]; + shared_interview_group?: number; + total_applicants?: number; processed_applicants?: number; accepted_applicants?: number; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index a237bf674..cf5e00ee3 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -285,6 +285,9 @@ export const KEY = { recruitment_interview_set: 'recruitment_interview_set', recruitment_interview_groups: 'recruitment_interview_groups', recruitment_interview_group: 'recruitment_interview_group', + recruitment_interview_group_create_header: 'recruitment_interview_group_create_header', + recruitment_interview_group_edit_header: 'recruitment_interview_group_edit_header', + recruitment_search_position: 'recruitment_search_position', recruitment_applicants: 'recruitment_applicants', recruitment_interview_time: 'recruitment_interview_time', recruitment_interview_location: 'recruitment_interview_location', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index 5e693cd43..4c4f556ce 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -268,7 +268,10 @@ export const nb = prepareTranslations({ [KEY.recruitment_no_interviews]: 'Ingen intervjuer', [KEY.recruitment_interview_set]: 'Sett intervju', [KEY.recruitment_interview_groups]: 'Intervjugrupper', - [KEY.recruitment_interview_group]: 'Intervjugrupper', + [KEY.recruitment_interview_group]: 'Intervjugruppe', + [KEY.recruitment_interview_group_create_header]: 'Opprett stillingsgruppe for felles intervju', + [KEY.recruitment_interview_group_edit_header]: 'Endre stillingsgruppe for felles intervju', + [KEY.recruitment_search_position]: 'Søk på stillingstittel', [KEY.recruitment_interview_time]: 'Intervjutid', [KEY.recruitment_interview_location]: 'Intervjusted', [KEY.recruitment_no_positions]: 'Ingen verv', @@ -755,6 +758,9 @@ export const en = prepareTranslations({ [KEY.recruitment_interview_set]: 'Set Interview', [KEY.recruitment_interview_groups]: 'Interview groups', [KEY.recruitment_interview_group]: 'Interview group', + [KEY.recruitment_interview_group_create_header]: 'Create group for shared interview', + [KEY.recruitment_interview_group_edit_header]: 'Edit group for shared interview', + [KEY.recruitment_search_position]: 'Search for position', [KEY.recruitment_interview_time]: 'Interview Time', [KEY.recruitment_interview_location]: 'Interview Location', [KEY.recruitment_interview_notes]: 'Interview notes', diff --git a/frontend/src/router/router.tsx b/frontend/src/router/router.tsx index 662f7098f..2be93f8cb 100644 --- a/frontend/src/router/router.tsx +++ b/frontend/src/router/router.tsx @@ -56,6 +56,7 @@ import { RecruitmentPositionFormAdminPage, RecruitmentPositionOverviewPage, RecruitmentSeparatePositionFormAdminPage, + RecruitmentSharedInterviewPositionsFormAdminPage, RecruitmentUnprocessedApplicantsPage, RecruitmentUsersWithoutInterviewGangPage, RecruitmentUsersWithoutThreeInterviewCriteriaPage, @@ -375,6 +376,35 @@ export const router = createBrowserRouter( } /> } /> + ( + + {t(KEY.common_create)} {t(KEY.recruitment_interview_group)} + + ), + }} + element={} />} + /> + ( + + {t(KEY.common_edit)} {t(KEY.recruitment_interview_group)} + + ), + }} + element={} />} + /> + } + handle={{ + crumb: ({ pathname }: UIMatch) => {t(KEY.recruitment_applet_room_overview)}, + }} + />