Skip to content

Commit

Permalink
1355 frontend for submitting interview rooms (#1385)
Browse files Browse the repository at this point in the history
* Create page for room overview

* Add table of interview rooms

* Create page for creating rooms

* Add edit and delete room functionality
---------

Co-authored-by: Snorre Sæther <[email protected]>
  • Loading branch information
Mathias-a and Snorre98 committed Sep 24, 2024
1 parent c4253c3 commit b66ea93
Show file tree
Hide file tree
Showing 14 changed files with 315 additions and 7 deletions.
2 changes: 0 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"build-storybook-dev": "storybook build",
"cypress:open": "cypress open",
"cypress:run": "yarn run cypress run",

"biome//": "echo Biome is configured for entire repo.",
"biome:check": "biome check",
"biome:ci": "biome ci",
Expand All @@ -34,7 +33,6 @@
"lint:fix-unsafe": "biome lint --write --unsafe",
"format:check": "biome format",
"format:fix": "biome format --write",

"stylelint:check": "stylelint --config .stylelintrc src/**/*.{css,scss}",
"tsc:check": "tsc",
"tsc:watch": "tsc --watch",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ export function RecruitmentGangOverviewPage() {
>
{t(KEY.common_edit)}
</Button>
<Button
theme="samf"
rounded={true}
link={reverse({
pattern: ROUTES.frontend.admin_recruitment_room_overview,
urlParams: { recruitmentId },
})}
>
{t(KEY.recruitment_create_room)}
</Button>
<Button
theme="success"
rounded={true}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
import { SamfundetLogoSpinner } from '~/Components';
import { SamfForm } from '~/Forms/SamfForm';
import { SamfFormField } from '~/Forms/SamfFormField';
import { AdminPageLayout } from '~/PagesAdmin/AdminPageLayout/AdminPageLayout';
import { getInterviewRoom, postInterviewRoom, putInterviewRoom } from '~/api';
import type { InterviewRoomDto } from '~/dto';
import { STATUS } from '~/http_status_codes';
import { KEY } from '~/i18n/constants';
import { reverse } from '~/named-urls';
import { ROUTES } from '~/routes';

type FormType = {
name: string;
location: string;
start_time: string;
end_time: string;
};

export function CreateInterviewRoomPage() {
const { t } = useTranslation();
const navigate = useNavigate();

const { recruitmentId, roomId } = useParams();
const [showSpinner, setShowSpinner] = useState<boolean>(true);
const [room, setRoom] = useState<Partial<InterviewRoomDto>>();

useEffect(() => {
if (roomId) {
getInterviewRoom(roomId)
.then((data) => {
setRoom(data.data);
setShowSpinner(false);
})
.catch((data) => {
if (data.request.status === STATUS.HTTP_404_NOT_FOUND) {
navigate(
reverse({
pattern: ROUTES.frontend.admin_recruitment_room_overview,
urlParams: { recruitmentId: recruitmentId },
}),
{ replace: true },
);
}
toast.error(t(KEY.common_something_went_wrong));
});
} else {
setShowSpinner(false);
}
}, [roomId, recruitmentId, navigate, t]);

const initialData: Partial<InterviewRoomDto> = {
name: room?.name,
location: room?.location,
start_time: room?.start_time,
end_time: room?.end_time,
};

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

if (showSpinner) {
return (
<div>
<SamfundetLogoSpinner />
</div>
);
}

function handleOnSubmit(data: InterviewRoomDto) {
const updatedRoom = {
...data,
recruitment: recruitmentId,
};

if (roomId) {
putInterviewRoom(roomId, updatedRoom)
.then(() => {
toast.success(t(KEY.common_update_successful));
navigate(
reverse({
pattern: ROUTES.frontend.admin_recruitment_room_overview,
urlParams: { recruitmentId: recruitmentId },
}),
);
})
.catch((error) => {
toast.error(t(KEY.common_something_went_wrong));
console.error(error);
});
} else {
postInterviewRoom(updatedRoom)
.then(() => {
navigate(
reverse({
pattern: ROUTES.frontend.admin_recruitment_room_overview,
urlParams: { recruitmentId: recruitmentId },
}),
);
toast.success(t(KEY.common_creation_successful));
})
.catch((error) => {
toast.error(t(KEY.common_something_went_wrong));
console.error(error);
});
}
}

return (
<AdminPageLayout title={`${roomId ? t(KEY.common_edit) : t(KEY.common_create)}`} header={true}>
<div>
<SamfForm<FormType> onSubmit={handleOnSubmit} initialData={initialData} submitText={submitText}>
<div>
<SamfFormField<string, FormType> field="name" type="text" label={t(KEY.common_name)} required={true} />
</div>
<div>
<SamfFormField<string, FormType>
field="location"
type="text"
label={t(KEY.recruitment_interview_location)}
required={true}
/>
</div>
<div>
<SamfFormField<string, FormType>
field="start_time"
type="date_time"
label={t(KEY.start_time)}
required={true}
/>
</div>
<div>
<SamfFormField<string, FormType>
field="end_time"
type="date_time"
label={t(KEY.end_time)}
required={true}
/>
</div>
</SamfForm>
</div>
</AdminPageLayout>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CreateInterviewRoomPage } from './CreateInterviewRoomPage';
86 changes: 86 additions & 0 deletions frontend/src/PagesAdmin/RoomAdminPage/RoomAdminPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useRouteLoaderData } from 'react-router-dom';
import { toast } from 'react-toastify';
import { Button, CrudButtons, Table } from '~/Components';
import { deleteInterviewRoom, getInterviewRoomsForRecruitment } from '~/api';
import type { InterviewRoomDto } from '~/dto';
import { useCustomNavigate } from '~/hooks';
import { KEY } from '~/i18n/constants';
import { reverse } from '~/named-urls';
import type { RecruitmentLoader } from '~/router/loaders';
import { ROUTES } from '~/routes';

export function RoomAdminPage() {
const [interviewRooms, setInterviewRooms] = useState<InterviewRoomDto[] | undefined>();
const data = useRouteLoaderData('recruitment') as RecruitmentLoader | undefined;
const navigate = useCustomNavigate();
const { t } = useTranslation();

useEffect(() => {
if (data?.recruitment?.id) {
getInterviewRoomsForRecruitment(data.recruitment.id.toString()).then((response) =>
setInterviewRooms(response.data),
);
}
}, [data?.recruitment?.id]);

if (!interviewRooms) {
return <p>No rooms found</p>;
}

const columns = [
{ content: 'Room Name', sortable: true },
{ content: 'Location', sortable: true },
{ content: 'Start Time', sortable: true },
{ content: 'End Time', sortable: true },
{ content: 'Recruitment', sortable: true },
{ content: 'Gang', sortable: true },
{ content: 'Actions', sortable: false },
];

const tableData = interviewRooms.map((room) => [
room.name,
room.location,
new Date(room.start_time),
new Date(room.end_time),
room.recruitment,
room.gang !== undefined ? room.gang : 'N/A',
{
content: (
<CrudButtons
key={`edit-room-${room.id}`}
onEdit={() =>
navigate({
url: reverse({
pattern: ROUTES.frontend.admin_recruitment_room_edit,
urlParams: { recruitmentId: data?.recruitment?.id, roomId: room.id.toString() },
}),
})
}
onDelete={() => {
deleteInterviewRoom(room.id.toString()).then(() => {
toast.success('Interview room deleted');
setInterviewRooms(interviewRooms.filter((r) => r.id !== room.id));
});
}}
/>
),
},
]);

return (
<>
<Button
link={reverse({
pattern: ROUTES.frontend.admin_recruitment_room_create,
urlParams: { recruitmentId: data?.recruitment?.id },
})}
theme="samf"
>
{t(KEY.common_create)}
</Button>
<Table columns={columns} data={tableData} defaultSortColumn={0} />
</>
);
}
2 changes: 2 additions & 0 deletions frontend/src/PagesAdmin/RoomAdminPage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { CreateInterviewRoomPage } from './CreateInterviewRoomPage';
export { RoomAdminPage } from './RoomAdminPage';
5 changes: 3 additions & 2 deletions frontend/src/PagesAdmin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ export { RecruitmentGangOverviewPage } from './RecruitmentGangOverviewPage';
export { RecruitmentOverviewPage } from './RecruitmentOverviewPage';
export { RecruitmentPositionFormAdminPage } from './RecruitmentPositionFormAdminPage';
export { RecruitmentPositionOverviewPage } from './RecruitmentPositionOverviewPage';
export { RecruitmentRecruiterDashboardPage } from './RecruitmentRecruiterDashboardPage';
export { RecruitmentSeparatePositionFormAdminPage } from './RecruitmentSeparatePositionFormAdminPage';
export { RecruitmentUnprocessedApplicantsPage } from './RecruitmentUnprocessedApplicantsPage';
export { RecruitmentUsersWithoutInterviewGangPage } from './RecruitmentUsersWithoutInterviewGangPage';
export { RecruitmentUsersWithoutThreeInterviewCriteriaPage } from './RecruitmentUsersWithoutThreeInterviewCriteriaPage';
export { RolesAdminPage } from './RolesAdminPage';
export { CreateInterviewRoomPage, RoomAdminPage } from './RoomAdminPage';
export { SaksdokumentAdminPage } from './SaksdokumentAdminPage';
export { SaksdokumentFormAdminPage } from './SaksdokumentFormAdminPage';
export { SultenMenuAdminPage } from './SultenMenuAdminPage';
export { SultenMenuItemFormAdminPage } from './SultenMenuItemFormAdminPage';
export { SultenReservationAdminPage } from './SultenReservationAdminPage';
export { UsersAdminPage } from './UsersAdminPage';
export { RecruitmentRecruiterDashboardPage } from './RecruitmentRecruiterDashboardPage';
export { RecruitmentSeparatePositionFormAdminPage } from './RecruitmentSeparatePositionFormAdminPage';
41 changes: 41 additions & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ImagePostDto,
InformationPageDto,
InterviewDto,
InterviewRoomDto,
KeyValueDto,
MenuDto,
MenuItemDto,
Expand Down Expand Up @@ -937,6 +938,46 @@ export async function putRecruitmentApplicationInterview(
const response = await axios.put<InterviewDto>(url, interview, { withCredentials: true });
return response;
}

// ############################################################
// Interview rooms
// ############################################################

export async function getInterviewRoomsForRecruitment(
recruitmentId: string,
): Promise<AxiosResponse<InterviewRoomDto[]>> {
const url =
BACKEND_DOMAIN +
reverse({
pattern: ROUTES.backend.samfundet__interview_rooms_list,
queryParams: { recruitment: recruitmentId },
});
return await axios.get(url, { withCredentials: true });
}

export async function getInterviewRoom(id: string): Promise<AxiosResponse<InterviewRoomDto>> {
const url =
BACKEND_DOMAIN + reverse({ pattern: ROUTES.backend.samfundet__interview_rooms_detail, urlParams: { pk: id } });
return await axios.get(url, { withCredentials: true });
}

export async function postInterviewRoom(data: Partial<InterviewRoomDto>): Promise<AxiosResponse> {
const url = BACKEND_DOMAIN + ROUTES.backend.samfundet__interview_rooms_list;
return await axios.post(url, data, { withCredentials: true });
}

export async function putInterviewRoom(id: string, data: Partial<InterviewRoomDto>): Promise<AxiosResponse> {
const url =
BACKEND_DOMAIN + reverse({ pattern: ROUTES.backend.samfundet__interview_rooms_detail, urlParams: { pk: id } });
return await axios.put(url, data, { withCredentials: true });
}

export async function deleteInterviewRoom(id: string): Promise<AxiosResponse> {
const url =
BACKEND_DOMAIN + reverse({ pattern: ROUTES.backend.samfundet__interview_rooms_detail, urlParams: { pk: id } });
return await axios.delete(url, { withCredentials: true });
}

// ############################################################
// Purchase Feedback
// ############################################################
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,16 @@ export type RecruitmentStatsDto = {
campus_stats: RecruitmentCampusStatDto[];
};

export type InterviewRoomDto = {
id: number;
name: string;
location: string;
start_time: string;
end_time: string;
recruitment: string;
gang?: number;
};

// ############################################################
// Purchase Feedback
// ############################################################
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ export const KEY = {
recruitment_all_applications: 'recruitment_all_applications',
recruitment_not_applied: 'recruitment_not_applied',
recruitment_will_be_anonymized: 'recruitment_will_be_anonymized',
recruitment_create_room: 'recruitment_create_room',
shown_application_deadline: 'shown_application_deadline',
actual_application_deadline: 'actual_application_deadline',
recruitment_number_of_applications: 'recruitment_number_of_applications',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ export const nb = prepareTranslations({
[KEY.admin_information_confirm_delete]: 'Er du sikker du vil slette denne informasjonssiden?',
[KEY.admin_information_confirm_cancel]: 'Er du sikker på at du vil gå tilbake uten å lagre?',
[KEY.admin_gangsadminpage_abbreviation]: 'Forkortelse',
[KEY.recruitment_create_room]: 'Opprett rom',

// CommandMenu:
[KEY.command_menu_label]: 'Global kommando meny',
Expand Down Expand Up @@ -763,6 +764,8 @@ export const en = prepareTranslations({
[KEY.error_recruitment_form_4]: 'Group reprioritization deadline cannot be before the reprioritization deadline',
[KEY.recruitment_dashboard_description]:
'Here you have an overview of your job as a recruiter for the recruitment, here you can see your upcomming interviews, the positions you have a responsibility for, and setting the time you are available to host an interview',
[KEY.recruitment_create_room]: 'Create room',

// Admin:
[KEY.admin_organizer]: 'Organizer',
[KEY.admin_saksdokument]: 'Case document',
Expand Down
Loading

0 comments on commit b66ea93

Please sign in to comment.