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

1355 frontend for submitting interview rooms #1385

Merged
merged 11 commits into from
Sep 24, 2024
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 @@ -102,6 +102,17 @@ 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>

{recruitmentId && <OccupiedFormModal recruitmentId={Number.parseInt(recruitmentId)} isButtonRounded={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';
3 changes: 2 additions & 1 deletion frontend/src/PagesAdmin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ export { RecruitmentGangOverviewPage } from './RecruitmentGangOverviewPage';
export { RecruitmentOverviewPage } from './RecruitmentOverviewPage';
export { RecruitmentPositionFormAdminPage } from './RecruitmentPositionFormAdminPage';
export { RecruitmentPositionOverviewPage } from './RecruitmentPositionOverviewPage';
export { RecruitmentRecruiterDashboardPage } from './RecruitmentRecruiterDashboardPage';
export { RecruitmentUnprocessedApplicantsPage } from './RecruitmentUnprocessedApplicantsPage';
export { RecruitmentUsersWithoutInterviewGangPage } from './RecruitmentUsersWithoutInterviewGangPage';
export { RecruitmentUsersWithoutThreeInterviewCriteriaPage } from './RecruitmentUsersWithoutThreeInterviewCriteriaPage';
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';
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 @@ -893,6 +894,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 @@ -485,6 +485,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 @@ -293,6 +293,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 @@ -357,6 +357,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 @@ -757,6 +758,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
Loading