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

Add interview group forms #1573

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions backend/root/utils/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,8 @@
samfundet__saksdokument_detail = 'samfundet:saksdokument-detail'
samfundet__profile_list = 'samfundet:profile-list'
samfundet__profile_detail = 'samfundet:profile-detail'
samfundet__permissions_list = 'samfundet:permissions-list'
samfundet__permissions_detail = 'samfundet:permissions-detail'
samfundet__menu_list = 'samfundet:menu-list'
samfundet__menu_detail = 'samfundet:menu-detail'
samfundet__menu_items_list = 'samfundet:menu_items-list'
Expand Down Expand Up @@ -541,6 +543,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'
Expand All @@ -562,6 +566,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'
Expand Down
7 changes: 5 additions & 2 deletions backend/samfundet/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -860,8 +860,6 @@ class Meta:


class RecruitmentPositionSharedInterviewGroupSerializer(serializers.ModelSerializer):
positions = RecruitmentPositionForApplicantSerializer(many=True, read_only=True)

class Meta:
model = RecruitmentPositionSharedInterviewGroup
fields = [
Expand All @@ -872,6 +870,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)
Expand Down
3 changes: 2 additions & 1 deletion backend/samfundet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

########## Recruitment ##########
router.register('recruitment', views.RecruitmentView, 'recruitment')
router.register('recruitmentSharedInterviewGroup', views.RecruitmentSharedInterviewGroupView, 'recruitment_sharedinterviewgroups')
Snorre98 marked this conversation as resolved.
Show resolved Hide resolved
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')
Expand Down Expand Up @@ -90,7 +91,7 @@
),
path(
'recruitment-shared-interview-groups/<int:recruitment_id>/',
views.RecruitmentInterviewGroupView.as_view(),
views.RecruitmentInterviewGroupRecruitmentView.as_view(),
name='recruitment_shared_interviews',
),
path('recruitment-positions-gang-for-gangs/', views.RecruitmentPositionsPerGangForGangView.as_view(), name='recruitment_positions_gang_for_gangs'),
Expand Down
9 changes: 8 additions & 1 deletion backend/samfundet/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,13 @@ class RecruitmentForRecruiterView(ModelViewSet):
queryset = Recruitment.objects.all()


@method_decorator(ensure_csrf_cookie, 'dispatch')
class RecruitmentSharedInterviewGroupView(ModelViewSet):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kunne man kalt dette noe annet? Er det ikke snakk om en "gruppe" med stillinger som har delt intervju, så kanskje: RecruitmentSharedInterviewPositionsView

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

og da det samme med modellen RecruitmentPositionsSharedInterviewGroup blir RecruitmentSharedInterviewPositions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

permission_classes = (DjangoModelPermissionsOrAnonReadOnly,)
serializer_class = RecruitmentPositionSharedInterviewGroupSerializer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RecruitmentSharedInterviewPositionsSerializer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

queryset = RecruitmentPositionSharedInterviewGroup.objects.all()


@method_decorator(ensure_csrf_cookie, 'dispatch')
class RecruitmentStatisticsView(ModelViewSet):
permission_classes = (DjangoModelPermissions,) # Allow read only to permissions on perms
Expand Down Expand Up @@ -1111,7 +1118,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 RecruitmentInterviewGroupRecruitmentView(APIView):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RecruitmentSharedInterviewPositionsRecruitmentView

??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

permission_classes = [IsAuthenticated]

def get(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@import "src/constants";

@import "src/mixins";

.button {
margin-top: 2rem;
}

Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,6 +13,8 @@ import { dbT, lowerCapitalize } from '~/utils';
import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout';
import { AppletContainer, RecruitmentInterviewGroupsList } from './components';

import styles from './RecruitmentGangOverviewPage.module.scss';

export function RecruitmentGangOverviewPage() {
const { recruitmentId } = useParams();
const navigate = useCustomNavigate();
Expand Down Expand Up @@ -115,7 +117,26 @@ export function RecruitmentGangOverviewPage() {
label: t(KEY.recruitment_gangs_with_separate_positions),
value: <Table columns={tableSeparatePositionColumns} data={tableSeparatePositionData ?? []} />,
},
{ key: 3, label: t(KEY.recruitment_interview_groups), value: <RecruitmentInterviewGroupsList /> },
{
key: 3,
label: t(KEY.recruitment_interview_groups),
value: (
<>
<RecruitmentInterviewGroupsList />
<Button
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Knappen burde være over listen. Den blir litt gjemt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

className={styles.button}
theme="success"
rounded={true}
link={reverse({
pattern: ROUTES.frontend.admin_recruitment_sharedinterviewgroup_create,
urlParams: { recruitmentId: recruitmentId },
})}
>
{lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.recruitment_interview_group)}`)}
</Button>{' '}
</>
),
},
];
}, [gangs, recruitment, t, recruitmentId, navigate, deleteSeparatePositionHandler]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@
width: 100%;
flex: 1;
}

.footer {
justify-content: end;
display: flex;
padding: 0.5em;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { ExpandableHeader, Table } from '~/Components';
import { useTranslation } from 'react-i18next';
import { Button, ExpandableHeader, Table } from '~/Components';
import type { RecruitmentSharedInterviewGroupDto, RecruitmentStatsDto } from '~/dto';
import { KEY } from '~/i18n/constants';
import { reverse } from '~/named-urls';
import { ROUTES } from '~/routes';
import { dbT } from '~/utils';
import styles from './RecruitmentInterviewGroupComponent.module.scss';

Expand All @@ -9,6 +13,8 @@ type RecruitmentInterviewGroupComponentProps = {

export function RecruitmentInterviewGroupComponent({ interviewGroup }: RecruitmentInterviewGroupComponentProps) {
const interviewGroupHeader = dbT(interviewGroup, 'name') ?? 'N/A';
const { t } = useTranslation();

return (
<ExpandableHeader
showByDefault={true}
Expand All @@ -22,6 +28,19 @@ export function RecruitmentInterviewGroupComponent({ interviewGroup }: Recruitme
return { cells: [dbT(position, 'name'), dbT(position.gang, 'name')] };
})}
/>
<div className={styles.footer}>
<Button
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fåre "something wrong" error når jeg trykker på denne. Har seeda

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funker for meg, noe galt med din side

display="basic"
theme="blue"
rounded={true}
link={reverse({
pattern: ROUTES.frontend.admin_recruitment_sharedinterviewgroup_edit,
urlParams: { recruitmentId: interviewGroup.recruitment, sharedInterviewGroupId: interviewGroup.id },
})}
>
{t(KEY.common_edit)}
</Button>
</div>
</ExpandableHeader>
);
}
Original file line number Diff line number Diff line change
@@ -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,
postRecruitmentSharedInterviewGroup,
putRecruitmentPosition,
putRecruitmentSharedInterviewGroup,
} 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 './RecruitmentInterviewGroupFormAdminPage.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<typeof schema>;

interface FormProps {
initialData: Partial<SchemaType>;
sharedInterviewGroupId?: string;
recruitmentId?: string;
}

export function RecruitmentInterviewGroupForm({ initialData, recruitmentId, sharedInterviewGroupId }: FormProps) {
const { t } = useTranslation();
const navigate = useNavigate();

const form = useForm<SchemaType>({
resolver: zodResolver(schema),
defaultValues: initialData,
});

const [positionOptions, setPositionOptions] = useState<DropDownOption<number>[]>([]);

const submitText = sharedInterviewGroupId ? t(KEY.common_save) : t(KEY.common_create);

useEffect(() => {
console.log(initialData);
}, [initialData]);

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
? putRecruitmentSharedInterviewGroup(sharedInterviewGroupId, updatedSharedInterviewGroup)
: postRecruitmentSharedInterviewGroup(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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className={styles.form}>
<div className={styles.wrapper}>
<div className={styles.row}>
<FormField
control={form.control}
name="name_nb"
render={({ field }) => (
<FormItem className={styles.item}>
<FormLabel>{`${t(KEY.common_name)} ${t(KEY.common_norwegian)}`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name_en"
render={({ field }) => (
<FormItem className={styles.item}>
<FormLabel>{`${t(KEY.common_name)} ${t(KEY.common_english)}`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className={styles.row}>
<FormField
control={form.control}
name="positions"
render={({ field }) => (
<FormItem className={styles.item}>
<FormLabel>{`${t(KEY.common_name)} ${t(KEY.common_english)}`}</FormLabel>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Label burde være noe annet. F.eks. "Søk på stillingstittel"/""Search position title"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

<FormControl>
<MultiSelect {...field} options={positionOptions} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit" rounded={true} theme="green">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ikke mulig å submite. Får toast error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nei

{submitText}
</Button>
</div>
</form>
</Form>
);
}
Original file line number Diff line number Diff line change
@@ -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%;
}
Loading