From af9d7f11bb51ee247ba8ac0fe68b90bd879fb408 Mon Sep 17 00:00:00 2001 From: dobroillya Date: Thu, 28 Sep 2023 11:20:50 +0300 Subject: [PATCH 01/75] bt-716: + file upload api --- mobile/src/bundles/file-upload/enums/enums.ts | 1 + .../bundles/file-upload/file-upload-api.ts | 42 +++++++++++++++++++ mobile/src/bundles/file-upload/file-upload.ts | 13 ++++++ mobile/src/bundles/file-upload/types/types.ts | 1 + 4 files changed, 57 insertions(+) create mode 100644 mobile/src/bundles/file-upload/enums/enums.ts create mode 100644 mobile/src/bundles/file-upload/file-upload-api.ts create mode 100644 mobile/src/bundles/file-upload/file-upload.ts create mode 100644 mobile/src/bundles/file-upload/types/types.ts diff --git a/mobile/src/bundles/file-upload/enums/enums.ts b/mobile/src/bundles/file-upload/enums/enums.ts new file mode 100644 index 000000000..3f47bd38b --- /dev/null +++ b/mobile/src/bundles/file-upload/enums/enums.ts @@ -0,0 +1 @@ +export { FileApiPath } from 'shared/build/index.js'; diff --git a/mobile/src/bundles/file-upload/file-upload-api.ts b/mobile/src/bundles/file-upload/file-upload-api.ts new file mode 100644 index 000000000..32872af4e --- /dev/null +++ b/mobile/src/bundles/file-upload/file-upload-api.ts @@ -0,0 +1,42 @@ +import { ApiPath, ContentType } from '~/bundles/common/enums/enums'; +import { HttpApiBase } from '~/framework/api/http-api-base'; +import { type Http } from '~/framework/http/http'; +import { type Storage } from '~/framework/storage/storage'; + +import { FileApiPath } from './enums/enums'; +import { type FileUploadResponse } from './types/types'; + +type Constructor = { + baseUrl: string; + http: Http; + storage: Storage; +}; + +class FileUploadApi extends HttpApiBase { + public constructor({ baseUrl, http, storage }: Constructor) { + super({ path: ApiPath.FILES, baseUrl, http, storage }); + } + + public async upload(payload: { + files: File[]; + }): Promise { + const formData = new FormData(); + for (const file of payload.files) { + formData.append('files', file); + } + + const response = await this.load( + this.getFullEndpoint(FileApiPath.UPLOAD, {}), + { + method: 'POST', + contentType: ContentType.MULTI_PART_FORM, + payload: formData, + hasAuth: true, + }, + ); + + return response.json(); + } +} + +export { FileUploadApi }; diff --git a/mobile/src/bundles/file-upload/file-upload.ts b/mobile/src/bundles/file-upload/file-upload.ts new file mode 100644 index 000000000..c0873f82e --- /dev/null +++ b/mobile/src/bundles/file-upload/file-upload.ts @@ -0,0 +1,13 @@ +import { config } from '~/framework/config/config'; +import { http } from '~/framework/http/http'; +import { storage } from '~/framework/storage/storage'; + +import { FileUploadApi } from './file-upload-api'; + +const fileUploadApi = new FileUploadApi({ + baseUrl: config.ENV.API.ORIGIN_URL, + storage, + http, +}); + +export { fileUploadApi }; diff --git a/mobile/src/bundles/file-upload/types/types.ts b/mobile/src/bundles/file-upload/types/types.ts new file mode 100644 index 000000000..4021ea0a8 --- /dev/null +++ b/mobile/src/bundles/file-upload/types/types.ts @@ -0,0 +1 @@ +export { type FileUploadResponse } from 'shared/build/index.js'; From c84c612d0194545949260383aa54a953a168f60c Mon Sep 17 00:00:00 2001 From: dobroillya Date: Thu, 28 Sep 2023 11:49:20 +0300 Subject: [PATCH 02/75] bt-716: * action to upload files --- mobile/src/bundles/common/store/actions.ts | 14 ++++++++++++++ mobile/src/framework/store/store.package.ts | 3 +++ 2 files changed, 17 insertions(+) diff --git a/mobile/src/bundles/common/store/actions.ts b/mobile/src/bundles/common/store/actions.ts index 8a4884d8f..50b487a29 100644 --- a/mobile/src/bundles/common/store/actions.ts +++ b/mobile/src/bundles/common/store/actions.ts @@ -42,6 +42,20 @@ const updateOnboardingData = createAsyncThunk< stepPayload; const talentHardSkills = hardSkills?.map((skill) => skill.value); + // if (cv && photo) { + + // try { + // const { document, image } = await fileUploadApi.upload({ + // files: [cv, photo], + // }); + // payload.photoId = image.id; + // payload.cvId = document.id; + // } catch (error) { + // console.log(error); + + // } + // } + if (Object.keys(payload).length === 0) { return stepPayload; } diff --git a/mobile/src/framework/store/store.package.ts b/mobile/src/framework/store/store.package.ts index e7cb830ac..a02940bca 100644 --- a/mobile/src/framework/store/store.package.ts +++ b/mobile/src/framework/store/store.package.ts @@ -16,6 +16,7 @@ import { commonDataApi } from '~/bundles/common-data/common-data'; import { reducer as commonDataReducer } from '~/bundles/common-data/store'; import { employerApi } from '~/bundles/employer/employer'; import { reducer as employeesReducer } from '~/bundles/employer/store'; +import { fileUploadApi } from '~/bundles/file-upload/file-upload'; import { type Config } from '~/framework/config/config'; import { notifications } from '~/framework/notifications/notifications'; import { socketMiddleware, storage } from '~/framework/storage/storage'; @@ -35,6 +36,7 @@ type ExtraArguments = { storage: typeof storage; employerApi: typeof employerApi; commonDataApi: typeof commonDataApi; + fileUploadApi: typeof fileUploadApi; }; class Store { @@ -84,6 +86,7 @@ class Store { notifications, storage, commonDataApi, + fileUploadApi, }; } } From beb46ff30f434a710cc38ed0242eed51edd8a8a3 Mon Sep 17 00:00:00 2001 From: dobroillya Date: Thu, 28 Sep 2023 14:28:35 +0300 Subject: [PATCH 03/75] bt-716: + upload photo --- .../components/image-picker/image-picker.tsx | 5 +++- .../components/photo-picker/photo-picker.tsx | 6 ++--- mobile/src/bundles/common/store/actions.ts | 27 +++++++++---------- .../components/steps/steps.tsx | 4 +-- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/mobile/src/bundles/common/components/image-picker/image-picker.tsx b/mobile/src/bundles/common/components/image-picker/image-picker.tsx index 798345193..092f70c51 100644 --- a/mobile/src/bundles/common/components/image-picker/image-picker.tsx +++ b/mobile/src/bundles/common/components/image-picker/image-picker.tsx @@ -48,7 +48,10 @@ const ImagePicker: React.FC = ({ const getImageFromLibrary = useCallback((): void => { toggleImagePickerVisibility?.(); handleToggleVisibility(); - const result = launchImageLibrary({ mediaType: 'photo' }); + const result = launchImageLibrary({ + mediaType: 'photo', + includeBase64: true, + }); onImageLoad(result); }, [handleToggleVisibility, onImageLoad, toggleImagePickerVisibility]); diff --git a/mobile/src/bundles/common/components/photo-picker/photo-picker.tsx b/mobile/src/bundles/common/components/photo-picker/photo-picker.tsx index b0bb90249..be78fe674 100644 --- a/mobile/src/bundles/common/components/photo-picker/photo-picker.tsx +++ b/mobile/src/bundles/common/components/photo-picker/photo-picker.tsx @@ -79,9 +79,9 @@ const PhotoPicker = ({ if (isSizeValid && isTypeValid) { onChange({ - size: image.fileSize, - uri: image.uri ?? uri, - type: image.type, + uri: `data:image/png;base64,${image.base64}`, + name: image.fileName, + type: 'image/jpeg', }); setAvatar(image.uri ?? uri); } else { diff --git a/mobile/src/bundles/common/store/actions.ts b/mobile/src/bundles/common/store/actions.ts index 50b487a29..2a3e59855 100644 --- a/mobile/src/bundles/common/store/actions.ts +++ b/mobile/src/bundles/common/store/actions.ts @@ -37,24 +37,23 @@ const updateOnboardingData = createAsyncThunk< UserDetailsGeneralRequestDto, AsyncThunkConfig >(`${sliceName}/updateOnboardingData`, async (stepPayload, { extra }) => { - const { commonApi, notifications } = extra; + const { commonApi, notifications, fileUploadApi } = extra; const { badges, hardSkills, photo, cv, companyLogo, ...payload } = stepPayload; const talentHardSkills = hardSkills?.map((skill) => skill.value); - // if (cv && photo) { - - // try { - // const { document, image } = await fileUploadApi.upload({ - // files: [cv, photo], - // }); - // payload.photoId = image.id; - // payload.cvId = document.id; - // } catch (error) { - // console.log(error); - - // } - // } + if (photo) { + try { + const { image } = await fileUploadApi.upload({ + files: [photo], + }); + payload.photoId = image.id; + } catch (error) { + const errorMessage = getErrorMessage(error); + notifications.showError({ title: errorMessage }); + throw error; + } + } if (Object.keys(payload).length === 0) { return stepPayload; diff --git a/mobile/src/navigations/onboarding-navigator/talent-onboarding-navigator/components/steps/steps.tsx b/mobile/src/navigations/onboarding-navigator/talent-onboarding-navigator/components/steps/steps.tsx index 312715682..bb0d5bcba 100644 --- a/mobile/src/navigations/onboarding-navigator/talent-onboarding-navigator/components/steps/steps.tsx +++ b/mobile/src/navigations/onboarding-navigator/talent-onboarding-navigator/components/steps/steps.tsx @@ -68,9 +68,7 @@ const Steps: React.FC = (props) => { if (stepNumber === activeStepNumber) { return TalentOnboardingStepState.FOCUSED; } - return stepNumber > activeStepNumber - ? TalentOnboardingStepState.DISABLED - : TalentOnboardingStepState.COMPLETED; + return TalentOnboardingStepState.COMPLETED; }, [activeStepNumber], ); From d0d13a3f4a2fbcbc886019e94386feaf24e56e36 Mon Sep 17 00:00:00 2001 From: dobroillya Date: Thu, 28 Sep 2023 17:05:03 +0300 Subject: [PATCH 04/75] bt-716: + cv upload --- .../components/file-picker/file-picker.tsx | 2 +- mobile/src/bundles/common/store/actions.ts | 54 ++++++++++++++++++- .../cv-and-contacts-form.tsx | 6 ++- .../profile-preview/profile-preview.tsx | 1 - 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/mobile/src/bundles/common/components/file-picker/file-picker.tsx b/mobile/src/bundles/common/components/file-picker/file-picker.tsx index 12dde011c..caf15f3a7 100644 --- a/mobile/src/bundles/common/components/file-picker/file-picker.tsx +++ b/mobile/src/bundles/common/components/file-picker/file-picker.tsx @@ -59,7 +59,7 @@ const FilePicker = ({ if (isSizeValid) { onChange({ name: document.name, - size: document.size, + type: document.type, uri: document.uri, }); } else { diff --git a/mobile/src/bundles/common/store/actions.ts b/mobile/src/bundles/common/store/actions.ts index 2a3e59855..be05e9b05 100644 --- a/mobile/src/bundles/common/store/actions.ts +++ b/mobile/src/bundles/common/store/actions.ts @@ -15,8 +15,34 @@ const createUserDetails = createAsyncThunk< UserDetailsGeneralCreateRequestDto, AsyncThunkConfig >(`${sliceName}/createUserDetails`, async (onboardingPayload, { extra }) => { - const { commonApi, notifications } = extra; + const { commonApi, notifications, fileUploadApi } = extra; const { photo, companyLogo, ...payload } = onboardingPayload; + + if (photo) { + try { + const { image } = await fileUploadApi.upload({ + files: [photo], + }); + payload.photoId = image.id; + } catch (error) { + const errorMessage = getErrorMessage(error); + notifications.showError({ title: errorMessage }); + throw error; + } + } + if (companyLogo) { + try { + const { image } = await fileUploadApi.upload({ + files: [companyLogo], + }); + payload.companyLogoId = image.id; + } catch (error) { + const errorMessage = getErrorMessage(error); + notifications.showError({ title: errorMessage }); + throw error; + } + } + try { const response = await commonApi.completeUserDetails(payload); return { @@ -54,6 +80,31 @@ const updateOnboardingData = createAsyncThunk< throw error; } } + if (companyLogo) { + try { + const { image } = await fileUploadApi.upload({ + files: [companyLogo], + }); + payload.companyLogoId = image.id; + } catch (error) { + const errorMessage = getErrorMessage(error); + notifications.showError({ title: errorMessage }); + throw error; + } + } + + // if (cv) { + // try { + // const { document } = await fileUploadApi.upload({ + // files: [cv], + // }); + // payload.cvId = document.id; + // } catch (error) { + // const errorMessage = getErrorMessage(error); + // notifications.showError({ title: errorMessage }); + // throw error; + // } + // } if (Object.keys(payload).length === 0) { return stepPayload; @@ -63,6 +114,7 @@ const updateOnboardingData = createAsyncThunk< ...payload, talentHardSkills: talentHardSkills, }); + return { ...response, //TODO remove when it is ready at the backend diff --git a/mobile/src/bundles/talent/components/cv-and-contacts-form/cv-and-contacts-form.tsx b/mobile/src/bundles/talent/components/cv-and-contacts-form/cv-and-contacts-form.tsx index 40ae425b1..ef030c930 100644 --- a/mobile/src/bundles/talent/components/cv-and-contacts-form/cv-and-contacts-form.tsx +++ b/mobile/src/bundles/talent/components/cv-and-contacts-form/cv-and-contacts-form.tsx @@ -50,7 +50,11 @@ const CVAndContactsForm: React.FC = ({ name="photo" containerStyle={globalStyles.alignItemsCenter} > - + { if (!onboardingData) { return null; } - const { jobTitle, salaryExpectation, From aaef62e808d0a7e323bb31eddd793a6f00b20de2 Mon Sep 17 00:00:00 2001 From: dobroillya Date: Thu, 28 Sep 2023 18:24:49 +0300 Subject: [PATCH 05/75] bt-716: * resolve conflicts --- backend/src/bundles/auth/auth.service.ts | 30 ++- backend/src/bundles/auth/auth.ts | 4 +- backend/src/bundles/auth/enums/enums.ts | 2 +- .../chat-messages/chat-message.entity.ts | 9 + .../chat-messages/chat-messages.controller.ts | 5 + .../chat-messages/chat-messages.repository.ts | 3 +- .../types/chat-message-properties.type.ts | 1 + .../bundles/contacts/contacts.controller.ts | 170 +++++++++++++++++ .../src/bundles/contacts/contacts.entity.ts | 58 ++++++ .../src/bundles/contacts/contacts.model.ts | 43 +++++ .../bundles/contacts/contacts.repository.ts | 70 +++++++ .../src/bundles/contacts/contacts.service.ts | 57 ++++++ backend/src/bundles/contacts/contacts.ts | 13 ++ backend/src/bundles/contacts/enums/enums.ts | 1 + .../types/contacts-properties.type.ts | 7 + backend/src/bundles/contacts/types/types.ts | 6 + .../contacts-create.validation-schema.ts | 13 ++ .../validation-schemas/validation-schemas.ts | 1 + backend/src/bundles/files/enums/enums.ts | 1 + backend/src/bundles/files/file.controller.ts | 25 +++ backend/src/bundles/files/file.entity.ts | 9 +- backend/src/bundles/files/file.repository.ts | 22 ++- backend/src/bundles/files/file.service.ts | 5 + .../helpers/generate-random-id.helper.ts | 4 +- .../files/helpers/get-file-role.helper.ts | 8 + .../files/helpers/get-file-type.helper.ts | 21 --- backend/src/bundles/files/helpers/helpers.ts | 1 + backend/src/bundles/files/types/types.ts | 3 + .../hiring-info/hiring-info.controller.ts | 61 +++++- .../hiring-info/hiring-info.repository.ts | 28 ++- .../hiring-info/hiring-info.service.ts | 8 +- .../src/bundles/hiring-info/types/types.ts | 2 + .../src/bundles/lms-data/lms-data.service.ts | 47 +++-- .../src/bundles/user-details/types/types.ts | 1 + .../types/user-details-properties.type.ts | 1 + .../types/user-details-with-files.type.ts | 11 ++ .../user-details/user-details.entity.ts | 10 + .../user-details/user-details.model.ts | 2 +- .../user-details/user-details.repository.ts | 55 +++++- .../user-details/user-details.service.ts | 10 +- backend/src/bundles/users/users.ts | 3 +- .../src/common/packages/database/database.ts | 1 + .../enums/database-table-name.enum.ts | 1 + .../common/packages/database/enums/enums.ts | 1 + .../contacts-table-column.enum.ts | 9 + .../database/enums/table-columns/enums.ts | 1 + .../hiring-info-table-column.enum.ts | 9 - .../server-application/server-application.ts | 2 + .../20230926173256_add_contacts_table.ts | 64 +++++++ ...7022842_update_talent_hard_skills_table.ts | 33 ++++ .../pages/connections/connections-panel.tsx | 18 +- .../src/bundles/admin-panel/store/actions.ts | 33 ---- .../src/bundles/admin-panel/types/types.ts | 6 - .../candidate-details/candidate-api.ts | 57 ++++++ .../candidate.ts} | 6 +- .../candidate-profile/candidate-profile.tsx | 50 +++-- .../profile-second-section.tsx | 70 +++++-- .../profile-second-section/styles.module.scss | 6 +- .../summary-preview/summary-preview.tsx | 4 +- .../candidate-details/store/actions.ts | 47 ++++- .../candidate-details/store/candidate.ts | 4 + .../bundles/candidate-details/types/types.ts | 3 + .../components/chat-header/chat-header.tsx | 8 +- .../components/company-info/company-info.tsx | 18 +- .../message-input/message-input.tsx | 62 +++--- .../components/message-list/message-list.tsx | 3 +- .../src/bundles/chat/constants/constants.ts | 4 +- .../bundles/chat/pages/chats/chats-page.tsx | 172 ++++++++++------- .../chat/pages/chats/styles.module.scss | 16 ++ frontend/src/bundles/chat/store/actions.ts | 2 + frontend/src/bundles/chat/store/slice.ts | 16 ++ .../common/components/dropdown/dropdown.tsx | 14 +- .../header-user-menu/header-user-menu.tsx | 5 +- .../protected-route/protected-route.tsx | 49 ++++- .../sidebar/sidebar-item/sidebar-item.tsx | 56 ++++++ .../sidebar-notification.tsx | 25 +++ .../sidebar-notification/styles.module.scss | 65 +++++++ .../common/components/sidebar/sidebar.tsx | 38 ++-- .../components/sidebar/styles.module.scss | 12 ++ frontend/src/bundles/common/enums/enums.ts | 1 + frontend/src/bundles/common/pages/home.tsx | 19 +- .../components/employer-file-upload.tsx | 1 - .../onboarding-form/onboarding-form.tsx | 94 ++++++---- .../employer-onboarding-api.ts | 1 + .../onboarding-form.validation-rule.ts | 2 +- .../employer-onboarding/helpers/helpers.ts | 1 + .../helpers/map-files-to-payload.ts | 31 +++ .../pages/onboarding/onboarding.tsx | 20 +- .../employer-onboarding/store/actions.ts | 59 +++++- .../employer-onboarding/store/slice.ts | 3 + ...general-update-user-details-request-dto.ts | 4 + .../types/onboarding-form/onboarding-dto.ts | 2 + .../onboarding-form.validation-schema.ts | 9 + .../bundles/file-upload/file-upload-api.ts | 31 ++- .../file-upload/types/file-dto.type.ts | 7 + .../src/bundles/file-upload/types/types.ts | 7 +- .../hiring-info-api.ts} | 27 ++- .../src/bundles/hiring-info/hiring-info.ts | 13 ++ .../src/bundles/hiring-info/store/actions.ts | 44 +++++ .../store/hiring-info.ts} | 9 +- .../store/slice.ts | 4 +- .../src/bundles/hiring-info/types/types.ts | 7 + .../profile-cabinet/pages/profile-cabinet.tsx | 114 ++++++++--- .../profile-cabinet/pages/styles.module.scss | 18 +- .../components/filters/employee-filters.tsx | 4 +- .../pages/candidate-page/candidate-page.tsx | 15 +- .../search-candidates/pages/candidates.tsx | 5 +- .../search-candidates/store/actions.ts | 16 +- .../bundles/search-candidates/store/slice.ts | 45 ++++- .../contacts-cv-step/contacts-cv-step.tsx | 24 ++- .../contacts-cv-step/styles.module.scss | 12 +- .../components/step-content/step-content.tsx | 28 ++- .../talent-onboarding/constants/constants.ts | 2 + .../talent-onboarding/helpers/helpers.ts | 1 - .../map-experience-to-string.ts | 12 -- .../pages/onboarding/onboarding.tsx | 25 ++- .../talent-onboarding/store/actions.ts | 56 +++++- .../bundles/talent-onboarding/store/slice.ts | 11 ++ .../store/talent-onboarding.ts | 2 + .../talent-onboarding-api.ts | 24 ++- ...general-update-user-details-request-dto.ts | 1 + .../bundles/talent-onboarding/types/types.ts | 1 + .../error-handler/error-handler.middleware.ts | 8 +- frontend/src/framework/store/store.package.ts | 11 +- mobile/package.json | 3 + mobile/src/bundles/auth/store/slice.ts | 11 +- mobile/src/bundles/chat/chat-api.ts | 100 ++++++++++ mobile/src/bundles/chat/chat.ts | 13 ++ .../components/chat-header/chat-header.tsx | 27 ++- .../chat-info-button/chat-info-button.tsx | 34 ++++ .../chat/components/chat-item/chat-item.tsx | 26 ++- .../chat/components/chat-item/styles.ts | 2 + .../chat-list-item/chat-list-item.tsx | 32 ++-- .../src/bundles/chat/components/components.ts | 1 + .../message-entry-field.tsx | 32 ++-- .../bundles/chat/components/search/search.tsx | 2 +- .../src/bundles/chat/constants/constants.ts | 139 ++------------ mobile/src/bundles/chat/enums/enums.ts | 1 + .../find-user-in-chat/find-user-in-chat.ts | 42 ----- .../get-elapsed-time/constants/constants.ts | 1 - .../get-elapsed-time/get-elapsed-time.ts | 6 +- .../get-partner-info/get-partner-info.ts | 30 +++ mobile/src/bundles/chat/helpers/helpers.ts | 3 +- .../set-partner-avatar/set-partner-avatar.ts | 40 ++++ .../sort-chats-by-date/sort-chats-by-date.ts | 8 +- .../chat/screens/chat-list/chat-list.tsx | 117 ++++++------ .../bundles/chat/screens/chat-list/styles.ts | 3 + .../chat-user-details/chat-user-details.tsx} | 37 ++-- .../screens/chat-user-details}/styles.ts | 9 + mobile/src/bundles/chat/screens/chat/chat.tsx | 70 +++---- mobile/src/bundles/chat/screens/screens.ts | 1 + mobile/src/bundles/chat/store/actions.ts | 101 ++++++++-- mobile/src/bundles/chat/store/index.ts | 17 +- mobile/src/bundles/chat/store/slice.ts | 149 ++++++++++++--- .../chat/types/chat-data-request-dto-type.ts | 16 -- .../src/bundles/chat/types/chat-item-type.ts | 8 - .../bundles/chat/types/current-chat-type.ts | 9 + .../chat/types/employer-details-type.ts | 12 ++ mobile/src/bundles/chat/types/types.ts | 13 +- .../chat/types/user-information-type.ts | 7 - .../bundles/common-data/common-data-api.ts | 29 +++ .../src/bundles/common-data/store/actions.ts | 38 +++- mobile/src/bundles/common-data/store/index.ts | 9 +- mobile/src/bundles/common-data/store/slice.ts | 39 +++- mobile/src/bundles/common-data/types/types.ts | 4 + mobile/src/bundles/common/common-api.ts | 16 ++ .../autocomplete-selector.tsx | 26 ++- .../common/components/avatar/avatar.tsx | 28 +-- .../bundles/common/components/badge/badge.tsx | 10 +- .../common/components/button/button.tsx | 10 +- .../bundles/common/components/components.ts | 2 + .../bundles/common/components/input/input.tsx | 5 +- .../common/components/loader/loader.tsx | 13 +- .../logout-button/logout-button.tsx | 34 ++++ .../components/photo-picker/photo-picker.tsx | 1 + .../common/components/selector/selector.tsx | 24 ++- .../src/bundles/common/components/tag/tag.tsx | 5 +- .../preview-tabs/preview-tabs.tsx | 107 +++++++++++ .../preview-tabs/styles.ts | 18 ++ .../components/talent-info-details/styles.ts | 28 +++ .../talent-info-details.tsx | 177 ++++++++++++++++++ .../verification-message.tsx | 3 +- .../src/bundles/common/constants/constants.ts | 1 - .../src/bundles/common/constants/icon-size.ts | 3 - mobile/src/bundles/common/enums/enums.ts | 3 +- .../employer-bottom-tab-screen-name.enum.ts | 3 +- .../enums/navigation/root-screen-name.enum.ts | 4 + .../talent-bottom-tab-screen-name.enum.ts | 2 +- .../bundles/common/enums/ui/icon-name.enum.ts | 1 + .../bundles/common/enums/ui/icon-size.enum.ts | 10 + mobile/src/bundles/common/enums/ui/ui.ts | 1 + .../get-avatar-style/get-avatar-style.ts | 54 ++++++ mobile/src/bundles/common/helpers/helpers.ts | 2 + mobile/src/bundles/common/store/actions.ts | 10 + mobile/src/bundles/common/store/index.ts | 2 + mobile/src/bundles/common/store/slice.ts | 4 + .../chat-navigation-properties.type.ts | 3 + ...ntact-talent-navigation-properties.type.ts | 5 + ...ttom-tab-navigation-parameter-list.type.ts | 2 + .../common/types/navigation/navigation.ts | 1 + .../root-navigation-parameter-list.type.ts | 5 + ...ttom-tab-navigation-parameter-list.type.ts | 2 +- mobile/src/bundles/common/types/types.ts | 5 + .../user-details-general-dto.type.ts | 10 +- .../candidate-card/candidate-card.tsx | 53 ++++-- .../components/candidate-card/styles.ts | 1 - .../candidate-filter-form.tsx | 31 ++- .../constants/constants.ts | 18 +- .../candidates-header/candidates-header.tsx | 20 +- .../constants/constants.ts | 5 +- .../employer-onboarding-form.tsx | 54 ++++-- .../search-talents/search-talents.tsx | 20 +- .../candidate-details/candidate-details.tsx | 88 +++++++++ .../screens/candidate-details/styles.ts | 16 ++ .../candidates-filter/candidates-filter.tsx | 38 +++- .../screens/candidates-filter}/styles.ts | 0 .../screens/candidates/candidates.tsx | 8 +- .../screens/constants/mock-constants.ts | 35 ---- .../contact-candidate/contact-candidate.tsx | 16 +- .../employer-onboarding.tsx | 21 +-- .../employer-profile/employer-profile.tsx | 56 +++--- .../src/bundles/employer/screens/screens.ts | 1 + mobile/src/bundles/employer/store/actions.ts | 3 +- mobile/src/bundles/employer/store/slice.ts | 13 +- ...loyer-onboarding-form.validation-schema.ts | 7 +- .../badges-form-data/badges-form-data.tsx | 37 ++++ .../badges-group/badges-group.tsx | 18 +- .../styles.ts | 0 .../components/badges-form/badges-form.tsx | 117 ------------ .../badges-form/constants/constants.ts | 9 - .../bundles/talent/components/components.ts | 12 +- .../contacts-form-data/contacts-form-data.tsx | 98 ++++++++++ .../components/contacts-form-data/styles.ts | 17 ++ .../cv-and-contacts-form.tsx | 4 +- .../onboarding-buttons/onboarding-buttons.tsx | 32 ++++ .../constants/constants.ts | 17 +- .../profile-form-data.tsx} | 93 ++++----- .../profile-preview/profile-preview.tsx | 163 +--------------- .../profile-screen-buttons.tsx | 54 ++++++ .../profile-screen-buttons/styles.ts | 12 ++ .../profile-tab-bar/profile-tab-bar.tsx | 99 ++++++++++ .../profile-top-bar-item.tsx | 25 +++ .../profile-top-bar-item/styles.ts} | 7 +- .../components/profile-tab-bar/styles.ts | 12 ++ .../project-container/constants/constants.ts | 12 ++ .../project-container/project-container.tsx | 27 ++- .../constants/constants.ts | 27 --- .../skills-form-data/constants/constants.ts | 3 + .../skills-form-data.tsx} | 79 ++++---- .../styles.ts | 4 - .../components/with-profile-form/styles.ts | 10 + .../with-profile-form/with-profile-form.tsx | 121 ++++++++++++ ...d-contacts-form-validation-message.enum.ts | 3 +- mobile/src/bundles/talent/enums/enums.ts | 5 +- .../profile-details-screen-name.ts | 9 + .../talent-form-type/talent-form-type.enum.ts | 6 + .../use-onboarding-form-submit.hook.ts | 7 +- .../talent/screens/bsa-badges/bsa-badges.tsx | 74 +++++++- .../screens/bsa-badges/constants/constants.ts | 7 + .../talent/screens/bsa-badges/styles.ts | 13 ++ .../chat-user-details/chat-user-details.tsx | 19 -- .../cv-and-contacts/constants/constants.ts | 11 ++ .../cv-and-contacts/cv-and-contacts.tsx | 32 +++- .../talent/screens/preview/preview.tsx | 26 ++- .../constants/constants.ts | 7 + .../profile-screen-badges.tsx | 84 +++++++++ .../profile-screen-contacts.tsx | 67 +++++++ .../profile-screen.profile.tsx | 70 +++++++ .../screens/profile-screen.profile/styles.ts | 12 ++ .../profile-screen-skills.tsx | 68 +++++++ .../screens/profile/constants/constants.ts | 13 ++ .../talent/screens/profile/profile.tsx | 15 +- mobile/src/bundles/talent/screens/screens.ts | 6 +- .../constants/constants.ts | 11 ++ .../skills-and-projects.tsx | 30 ++- .../screens/talent-profile/talent-profile.tsx | 66 ------- .../company-info/company-info-dto.type.ts | 10 - mobile/src/bundles/talent/types/types.ts | 3 +- ...cv-and-contacts-form-validation.schema.ts} | 6 +- .../validation-schemas/validation-schemas.ts | 2 +- .../storage/middlewares/socket/socket.ts | 4 +- mobile/src/framework/store/store.package.ts | 5 +- .../employer-bottom-tab.tsx | 16 +- .../talent-bottom-tab/talent-bottom-tab.tsx | 15 +- mobile/src/navigations/root/root.tsx | 35 +++- .../talent-profile-navigator.tsx | 45 +++++ package-lock.json | 44 +++++ .../types/message-response-dto.type.ts | 2 +- shared/src/bundles/contacts/contacts.ts | 7 + .../contacts/enums/contacts-api-path.enum.ts | 5 + shared/src/bundles/contacts/enums/enums.ts | 1 + .../types/contacts-create-request-dto.ts | 6 + .../types/contacts-find-request-dto.type.ts | 6 + .../contacts-get-all-response-dto.type.ts | 7 + .../types/contacts-response-dto.type.ts | 7 + shared/src/bundles/contacts/types/types.ts | 4 + shared/src/bundles/file/enums/enums.ts | 1 + .../bundles/file/enums/file-api-path.enum.ts | 2 + .../src/bundles/file/enums/file-role.enum.ts | 8 + shared/src/bundles/file/file.ts | 10 +- .../file/types/file-role-value.type.ts | 7 + .../file/types/file-upload-response.type.ts | 9 +- .../file/types/get-file-request-dto.type.ts | 5 + .../file/types/get-file-response-dto.type.ts | 8 + shared/src/bundles/file/types/types.ts | 3 + .../enums/hiring-info-api-path.enum.ts | 1 + shared/src/bundles/hiring-info/hiring-info.ts | 1 + .../hiring-info-find-request-dto.type.ts | 9 - .../hiring-info-find-response-dto.type.ts | 15 ++ .../hiring-info-get-all-response-dto.type.ts | 4 +- shared/src/bundles/hiring-info/types/types.ts | 1 + .../enums/skills-step.validation-message.ts | 25 --- .../enums/skills-step.validation-rule.ts | 19 -- .../skills-step.validation-rule.ts | 2 +- .../profile-step.validation-schema.ts | 1 - .../enums/experience-years-list.enum.ts | 18 +- .../types/user-details-response-dto.type.ts | 7 +- shared/src/enums/api-path.enum.ts | 1 + shared/src/enums/error-message.enum.ts | 4 +- .../talent-onboarding/create-number-range.ts | 6 +- shared/src/index.ts | 12 ++ 321 files changed, 5348 insertions(+), 1868 deletions(-) create mode 100644 backend/src/bundles/contacts/contacts.controller.ts create mode 100644 backend/src/bundles/contacts/contacts.entity.ts create mode 100644 backend/src/bundles/contacts/contacts.model.ts create mode 100644 backend/src/bundles/contacts/contacts.repository.ts create mode 100644 backend/src/bundles/contacts/contacts.service.ts create mode 100644 backend/src/bundles/contacts/contacts.ts create mode 100644 backend/src/bundles/contacts/enums/enums.ts create mode 100644 backend/src/bundles/contacts/types/contacts-properties.type.ts create mode 100644 backend/src/bundles/contacts/types/types.ts create mode 100644 backend/src/bundles/contacts/validation-schemas/contacts-create.validation-schema.ts create mode 100644 backend/src/bundles/contacts/validation-schemas/validation-schemas.ts create mode 100644 backend/src/bundles/files/enums/enums.ts create mode 100644 backend/src/bundles/files/helpers/get-file-role.helper.ts delete mode 100644 backend/src/bundles/files/helpers/get-file-type.helper.ts create mode 100644 backend/src/bundles/user-details/types/user-details-with-files.type.ts create mode 100644 backend/src/common/packages/database/enums/table-columns/contacts-table-column.enum.ts create mode 100644 backend/src/migrations/20230926173256_add_contacts_table.ts create mode 100644 backend/src/migrations/20230927022842_update_talent_hard_skills_table.ts delete mode 100644 frontend/src/bundles/admin-panel/store/actions.ts create mode 100644 frontend/src/bundles/candidate-details/candidate-api.ts rename frontend/src/bundles/{admin-panel/admin.ts => candidate-details/candidate.ts} (66%) create mode 100644 frontend/src/bundles/common/components/sidebar/sidebar-item/sidebar-item.tsx create mode 100644 frontend/src/bundles/common/components/sidebar/sidebar-notification/sidebar-notification.tsx create mode 100644 frontend/src/bundles/common/components/sidebar/sidebar-notification/styles.module.scss create mode 100644 frontend/src/bundles/employer-onboarding/helpers/map-files-to-payload.ts create mode 100644 frontend/src/bundles/file-upload/types/file-dto.type.ts rename frontend/src/bundles/{admin-panel/admin-api.ts => hiring-info/hiring-info-api.ts} (66%) create mode 100644 frontend/src/bundles/hiring-info/hiring-info.ts create mode 100644 frontend/src/bundles/hiring-info/store/actions.ts rename frontend/src/bundles/{admin-panel/store/admin.ts => hiring-info/store/hiring-info.ts} (56%) rename frontend/src/bundles/{admin-panel => hiring-info}/store/slice.ts (81%) create mode 100644 frontend/src/bundles/hiring-info/types/types.ts delete mode 100644 frontend/src/bundles/talent-onboarding/helpers/map-experience-to-string/map-experience-to-string.ts create mode 100644 mobile/src/bundles/chat/chat-api.ts create mode 100644 mobile/src/bundles/chat/chat.ts create mode 100644 mobile/src/bundles/chat/components/chat-info-button/chat-info-button.tsx create mode 100644 mobile/src/bundles/chat/enums/enums.ts delete mode 100644 mobile/src/bundles/chat/helpers/find-user-in-chat/find-user-in-chat.ts create mode 100644 mobile/src/bundles/chat/helpers/get-partner-info/get-partner-info.ts create mode 100644 mobile/src/bundles/chat/helpers/set-partner-avatar/set-partner-avatar.ts rename mobile/src/bundles/{talent/components/company-info/company-info.tsx => chat/screens/chat-user-details/chat-user-details.tsx} (84%) rename mobile/src/bundles/{talent/components/company-info => chat/screens/chat-user-details}/styles.ts (72%) delete mode 100644 mobile/src/bundles/chat/types/chat-data-request-dto-type.ts delete mode 100644 mobile/src/bundles/chat/types/chat-item-type.ts create mode 100644 mobile/src/bundles/chat/types/current-chat-type.ts create mode 100644 mobile/src/bundles/chat/types/employer-details-type.ts delete mode 100644 mobile/src/bundles/chat/types/user-information-type.ts create mode 100644 mobile/src/bundles/common/components/logout-button/logout-button.tsx create mode 100644 mobile/src/bundles/common/components/talent-info-details/preview-tabs/preview-tabs.tsx create mode 100644 mobile/src/bundles/common/components/talent-info-details/preview-tabs/styles.ts create mode 100644 mobile/src/bundles/common/components/talent-info-details/styles.ts create mode 100644 mobile/src/bundles/common/components/talent-info-details/talent-info-details.tsx delete mode 100644 mobile/src/bundles/common/constants/icon-size.ts create mode 100644 mobile/src/bundles/common/enums/ui/icon-size.enum.ts create mode 100644 mobile/src/bundles/common/helpers/get-avatar-style/get-avatar-style.ts create mode 100644 mobile/src/bundles/common/types/navigation/contact-talent-navigation-properties.type.ts create mode 100644 mobile/src/bundles/employer/screens/candidate-details/candidate-details.tsx create mode 100644 mobile/src/bundles/employer/screens/candidate-details/styles.ts rename mobile/src/bundles/{talent/components/profile-form => employer/screens/candidates-filter}/styles.ts (100%) delete mode 100644 mobile/src/bundles/employer/screens/constants/mock-constants.ts create mode 100644 mobile/src/bundles/talent/components/badges-form-data/badges-form-data.tsx rename mobile/src/bundles/talent/components/{badges-form => badges-form-data}/badges-group/badges-group.tsx (74%) rename mobile/src/bundles/talent/components/{badges-form => badges-form-data}/styles.ts (100%) delete mode 100644 mobile/src/bundles/talent/components/badges-form/badges-form.tsx delete mode 100644 mobile/src/bundles/talent/components/badges-form/constants/constants.ts create mode 100644 mobile/src/bundles/talent/components/contacts-form-data/contacts-form-data.tsx create mode 100644 mobile/src/bundles/talent/components/contacts-form-data/styles.ts create mode 100644 mobile/src/bundles/talent/components/onboarding-buttons/onboarding-buttons.tsx rename mobile/src/bundles/talent/components/{profile-form => profile-form-data}/constants/constants.ts (70%) rename mobile/src/bundles/talent/components/{profile-form/profile-form.tsx => profile-form-data/profile-form-data.tsx} (65%) create mode 100644 mobile/src/bundles/talent/components/profile-screen-buttons/profile-screen-buttons.tsx create mode 100644 mobile/src/bundles/talent/components/profile-screen-buttons/styles.ts create mode 100644 mobile/src/bundles/talent/components/profile-tab-bar/profile-tab-bar.tsx create mode 100644 mobile/src/bundles/talent/components/profile-tab-bar/profile-top-bar-item/profile-top-bar-item.tsx rename mobile/src/bundles/talent/{screens/talent-profile/style.ts => components/profile-tab-bar/profile-top-bar-item/styles.ts} (60%) create mode 100644 mobile/src/bundles/talent/components/profile-tab-bar/styles.ts create mode 100644 mobile/src/bundles/talent/components/project-container/constants/constants.ts delete mode 100644 mobile/src/bundles/talent/components/skills-and-projects-form/constants/constants.ts create mode 100644 mobile/src/bundles/talent/components/skills-form-data/constants/constants.ts rename mobile/src/bundles/talent/components/{skills-and-projects-form/skills-and-projects-form.tsx => skills-form-data/skills-form-data.tsx} (74%) rename mobile/src/bundles/talent/components/{skills-and-projects-form => skills-form-data}/styles.ts (77%) create mode 100644 mobile/src/bundles/talent/components/with-profile-form/styles.ts create mode 100644 mobile/src/bundles/talent/components/with-profile-form/with-profile-form.tsx create mode 100644 mobile/src/bundles/talent/enums/profile-details-screen-name/profile-details-screen-name.ts create mode 100644 mobile/src/bundles/talent/enums/talent-form-type/talent-form-type.enum.ts create mode 100644 mobile/src/bundles/talent/screens/bsa-badges/constants/constants.ts create mode 100644 mobile/src/bundles/talent/screens/bsa-badges/styles.ts delete mode 100644 mobile/src/bundles/talent/screens/chat-user-details/chat-user-details.tsx create mode 100644 mobile/src/bundles/talent/screens/cv-and-contacts/constants/constants.ts create mode 100644 mobile/src/bundles/talent/screens/profile-screen.badges/constants/constants.ts create mode 100644 mobile/src/bundles/talent/screens/profile-screen.badges/profile-screen-badges.tsx create mode 100644 mobile/src/bundles/talent/screens/profile-screen.contacts/profile-screen-contacts.tsx create mode 100644 mobile/src/bundles/talent/screens/profile-screen.profile/profile-screen.profile.tsx create mode 100644 mobile/src/bundles/talent/screens/profile-screen.profile/styles.ts create mode 100644 mobile/src/bundles/talent/screens/profile-screen.skills/profile-screen-skills.tsx create mode 100644 mobile/src/bundles/talent/screens/profile/constants/constants.ts create mode 100644 mobile/src/bundles/talent/screens/skills-and-projects/constants/constants.ts delete mode 100644 mobile/src/bundles/talent/screens/talent-profile/talent-profile.tsx delete mode 100644 mobile/src/bundles/talent/types/company-info/company-info-dto.type.ts rename mobile/src/bundles/talent/validation-schemas/cv-and-contacts-form/{cv-and-contacts-form.validation-schema.ts => cv-and-contacts-form-validation.schema.ts} (93%) create mode 100644 mobile/src/navigations/talent-profile-navigator/talent-profile-navigator.tsx create mode 100644 shared/src/bundles/contacts/contacts.ts create mode 100644 shared/src/bundles/contacts/enums/contacts-api-path.enum.ts create mode 100644 shared/src/bundles/contacts/enums/enums.ts create mode 100644 shared/src/bundles/contacts/types/contacts-create-request-dto.ts create mode 100644 shared/src/bundles/contacts/types/contacts-find-request-dto.type.ts create mode 100644 shared/src/bundles/contacts/types/contacts-get-all-response-dto.type.ts create mode 100644 shared/src/bundles/contacts/types/contacts-response-dto.type.ts create mode 100644 shared/src/bundles/contacts/types/types.ts create mode 100644 shared/src/bundles/file/enums/file-role.enum.ts create mode 100644 shared/src/bundles/file/types/file-role-value.type.ts create mode 100644 shared/src/bundles/file/types/get-file-request-dto.type.ts create mode 100644 shared/src/bundles/file/types/get-file-response-dto.type.ts create mode 100644 shared/src/bundles/hiring-info/types/hiring-info-find-response-dto.type.ts delete mode 100644 shared/src/bundles/talent-onboarding/enums/skills-step.validation-message.ts delete mode 100644 shared/src/bundles/talent-onboarding/enums/skills-step.validation-rule.ts diff --git a/backend/src/bundles/auth/auth.service.ts b/backend/src/bundles/auth/auth.service.ts index e6fee98b3..ec9525ef3 100644 --- a/backend/src/bundles/auth/auth.service.ts +++ b/backend/src/bundles/auth/auth.service.ts @@ -1,3 +1,4 @@ +import { type LMSDataService } from '~/bundles/lms-data/lms-data.service.js'; import { type UserFindResponseDto, type UserForgotPasswordRequestDto, @@ -8,7 +9,7 @@ import { type UserSignUpResponseDto, } from '~/bundles/users/types/types.js'; import { type UserService } from '~/bundles/users/user.service.js'; -import { ErrorMessage } from '~/common/enums/enums.js'; +import { ErrorMessage, UserRole } from '~/common/enums/enums.js'; import { HttpCode, HttpError } from '~/common/http/http.js'; import { type Encrypt } from '~/common/packages/encrypt/encrypt.js'; import { token } from '~/common/packages/packages.js'; @@ -17,10 +18,16 @@ import { TOKEN_EXPIRY } from './constants/constants.js'; class AuthService { private userService: UserService; + private lmsDataService: LMSDataService; private encrypt: Encrypt; - public constructor(userService: UserService, encrypt: Encrypt) { + public constructor( + userService: UserService, + lmsDataService: LMSDataService, + encrypt: Encrypt, + ) { this.userService = userService; + this.lmsDataService = lmsDataService; this.encrypt = encrypt; } @@ -38,7 +45,7 @@ class AuthService { public async signUp( userRequestDto: UserSignUpRequestDto, ): Promise { - const { email } = userRequestDto; + const { email, role } = userRequestDto; const userByEmail = await this.userService.findByEmail(email); @@ -49,8 +56,25 @@ class AuthService { }); } + const isUserTalent = role === UserRole.TALENT; + + const dataFromLMS = isUserTalent + ? await this.lmsDataService.getUserDataFromLMSServerbyEmail(email) + : null; + + if (!dataFromLMS && isUserTalent) { + throw new HttpError({ + message: ErrorMessage.NOT_FOUND_ON_LMS, + status: HttpCode.BAD_REQUEST, + }); + } + const user = await this.userService.create(userRequestDto); + if (dataFromLMS && isUserTalent) { + await this.lmsDataService.addUserLMSDataToDB(user.id, dataFromLMS); + } + return { ...user, token: await token.create({ id: user.id }), diff --git a/backend/src/bundles/auth/auth.ts b/backend/src/bundles/auth/auth.ts index c80605aaf..ef7e417ad 100644 --- a/backend/src/bundles/auth/auth.ts +++ b/backend/src/bundles/auth/auth.ts @@ -1,4 +1,4 @@ -import { userService } from '~/bundles/users/users.js'; +import { lmsDataService, userService } from '~/bundles/users/users.js'; import { encrypt, logger } from '~/common/packages/packages.js'; import { EmailService } from '../email/email.js'; @@ -6,7 +6,7 @@ import { AuthController } from './auth.controller.js'; import { AuthService } from './auth.service.js'; const emailService = new EmailService(); -const authService = new AuthService(userService, encrypt); +const authService = new AuthService(userService, lmsDataService, encrypt); const authController = new AuthController(logger, authService, emailService); export { authController, authService }; diff --git a/backend/src/bundles/auth/enums/enums.ts b/backend/src/bundles/auth/enums/enums.ts index 266ddc0f4..9da6ab197 100644 --- a/backend/src/bundles/auth/enums/enums.ts +++ b/backend/src/bundles/auth/enums/enums.ts @@ -1 +1 @@ -export { AuthApiPath } from 'shared/build/index.js'; +export { AuthApiPath, UserRole } from 'shared/build/index.js'; diff --git a/backend/src/bundles/chat-messages/chat-message.entity.ts b/backend/src/bundles/chat-messages/chat-message.entity.ts index 612cbef60..a2b788bfa 100644 --- a/backend/src/bundles/chat-messages/chat-message.entity.ts +++ b/backend/src/bundles/chat-messages/chat-message.entity.ts @@ -9,6 +9,7 @@ class ChatMessageEntity implements Entity { private 'chatId': string; private 'message': string; private 'isRead': boolean; + private 'createdAt': string; private constructor({ id, @@ -17,6 +18,7 @@ class ChatMessageEntity implements Entity { chatId, message, isRead, + createdAt, }: ChatMessageProperties) { this.id = id; this.senderId = senderId; @@ -24,6 +26,7 @@ class ChatMessageEntity implements Entity { this.chatId = chatId; this.message = message; this.isRead = isRead; + this.createdAt = createdAt; } public static initialize({ @@ -33,6 +36,7 @@ class ChatMessageEntity implements Entity { chatId, message, isRead, + createdAt, }: { id: string } & Omit): ChatMessageEntity { return new ChatMessageEntity({ id, @@ -41,6 +45,7 @@ class ChatMessageEntity implements Entity { chatId, message, isRead, + createdAt, }); } @@ -50,6 +55,7 @@ class ChatMessageEntity implements Entity { chatId, message, isRead, + createdAt, }: Omit): ChatMessageEntity { return new ChatMessageEntity({ id: null, @@ -58,6 +64,7 @@ class ChatMessageEntity implements Entity { chatId, message, isRead, + createdAt, }); } @@ -69,6 +76,7 @@ class ChatMessageEntity implements Entity { chatId: this.chatId, message: this.message, isRead: this.isRead, + createdAt: this.createdAt, }; } @@ -79,6 +87,7 @@ class ChatMessageEntity implements Entity { chatId: this.chatId, message: this.message, isRead: this.isRead, + createdAt: this.createdAt, }; } } diff --git a/backend/src/bundles/chat-messages/chat-messages.controller.ts b/backend/src/bundles/chat-messages/chat-messages.controller.ts index d9e6920b8..8d2dcfd51 100644 --- a/backend/src/bundles/chat-messages/chat-messages.controller.ts +++ b/backend/src/bundles/chat-messages/chat-messages.controller.ts @@ -52,6 +52,11 @@ import { chatMessagesCreateValidationSchema } from './validation-schemas/validat * type: boolean * description: Indicates whether the message has been read (true) or not (false). * example: true + * createdAt: + * type: string + * format: date-time + * description: The creation time of message + * example: '2023-09-27T12:34:56Z' * ChatResponseDto: * type: object * properties: diff --git a/backend/src/bundles/chat-messages/chat-messages.repository.ts b/backend/src/bundles/chat-messages/chat-messages.repository.ts index dd42d1c9c..ae6a67563 100644 --- a/backend/src/bundles/chat-messages/chat-messages.repository.ts +++ b/backend/src/bundles/chat-messages/chat-messages.repository.ts @@ -36,7 +36,8 @@ class ChatMessagesRepository implements Repository { const chatMessages = await this.chatMessageModel .query() .select('*') - .where('chatId', chatId); + .where('chatId', chatId) + .orderBy('createdAt', 'asc'); return chatMessages.map((chatMessage) => ChatMessageEntity.initialize(chatMessage), diff --git a/backend/src/bundles/chat-messages/types/chat-message-properties.type.ts b/backend/src/bundles/chat-messages/types/chat-message-properties.type.ts index d2eacfc3b..bcbf7194f 100644 --- a/backend/src/bundles/chat-messages/types/chat-message-properties.type.ts +++ b/backend/src/bundles/chat-messages/types/chat-message-properties.type.ts @@ -5,6 +5,7 @@ type ChatMessageProperties = { chatId: string; message: string; isRead: boolean; + createdAt: string; }; export { type ChatMessageProperties }; diff --git a/backend/src/bundles/contacts/contacts.controller.ts b/backend/src/bundles/contacts/contacts.controller.ts new file mode 100644 index 000000000..f48638063 --- /dev/null +++ b/backend/src/bundles/contacts/contacts.controller.ts @@ -0,0 +1,170 @@ +import { ApiPath } from '~/common/enums/enums.js'; +import { HttpCode } from '~/common/http/http.js'; +import { + type ApiHandlerOptions, + type ApiHandlerResponse, +} from '~/common/packages/controller/controller.js'; +import { type Logger } from '~/common/packages/logger/logger.js'; +import { ControllerBase } from '~/common/packages/packages.js'; + +import { type ContactsService } from './contacts.service.js'; +import { ContactsApiPath } from './enums/enums.js'; +import { + type ContactsCreateRequestDto, + type ContactsFindRequestDto, +} from './types/types.js'; +import { contactsCreateValidationSchema } from './validation-schemas/validation-schemas.js'; + +/** + * @swagger + * components: + * securitySchemes: + * bearerAuth: + * type: http + * scheme: bearer + * bearerFormat: JWT + * schemas: + * Contacts: + * type: object + * properties: + * id: + * format: uuid #Example: '550e8400-e29b-41d4-a716-446655440000' + * type: string + * talentId: + * format: uuid #Example: '550e8400-e29b-41d4-a716-446655440000' + * type: string + * companyId: + * format: uuid #Example: '550e8400-e29b-41d4-a716-446655440000' + * type: string + */ +class ContactsController extends ControllerBase { + private contactsService: ContactsService; + + public constructor(logger: Logger, contactsService: ContactsService) { + super(logger, ApiPath.CONTACTS); + + this.contactsService = contactsService; + + this.addRoute({ + path: ContactsApiPath.ROOT, + method: 'POST', + validation: { + body: contactsCreateValidationSchema, + }, + handler: (options) => { + return this.create( + options as ApiHandlerOptions<{ + body: ContactsCreateRequestDto; + }>, + ); + }, + }); + + this.addRoute({ + path: ContactsApiPath.ROOT, + method: 'GET', + handler: (options) => { + return this.findContact( + options as ApiHandlerOptions<{ + query: ContactsFindRequestDto; + }>, + ); + }, + }); + } + + /** + * @swagger + * /contacts: + * post: + * tags: + * - Contacts + * description: Creates company and talent contact relation + * security: + * - bearerAuth: [] + * requestBody: + * description: Contact create object + * required: true + * content: + * application/json: + * schema: + * type: object + * $ref: '#/components/schemas/ContactsCreateRequestDto' + * examples: + * example: + * value: + * talentId: '550e8400-e29b-41d4-a716-446655440000' + * companyId: 'd36dfd26-63af-4922-a8cf-04cb939e6d97' + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * $ref: '#/components/schemas/Contacts' + * components: + * schemas: + * ContactsCreateRequestDto: + * type: object + * properties: + * talentId: + * format: uuid #Example: '550e8400-e29b-41d4-a716-446655440000' + * type: string + * required: true + * companyId: + * format: uuid #Example: 'd36dfd26-63af-4922-a8cf-04cb939e6d97' + * type: string + * required: true + * + */ + private async create( + options: ApiHandlerOptions<{ + body: ContactsCreateRequestDto; + }>, + ): Promise { + return { + status: HttpCode.OK, + payload: await this.contactsService.create(options.body), + }; + } + + /** + * @swagger + * /contacts: + * get: + * tags: [Contacts] + * description: Returns true if contact is shared + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: talentId + * schema: + * type: uuid + * - in: query + * name: companyId + * schema: + * type: uuid + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: boolean + * + */ + private async findContact( + options: ApiHandlerOptions<{ + query: ContactsFindRequestDto; + }>, + ): Promise { + return { + status: HttpCode.OK, + payload: await this.contactsService.find(options.query), + }; + } +} + +export { ContactsController }; diff --git a/backend/src/bundles/contacts/contacts.entity.ts b/backend/src/bundles/contacts/contacts.entity.ts new file mode 100644 index 000000000..3784c29ea --- /dev/null +++ b/backend/src/bundles/contacts/contacts.entity.ts @@ -0,0 +1,58 @@ +import { type Entity } from '~/common/types/types.js'; + +import { type ContactsProperties } from './types/types.js'; + +class ContactsEntity implements Entity { + private 'id': string | null; + + private 'talentId': string; + + private 'companyId': string; + + private constructor({ id, talentId, companyId }: ContactsProperties) { + this.id = id; + this.talentId = talentId; + this.companyId = companyId; + } + + public static initialize({ + id, + talentId, + companyId, + }: ContactsProperties): ContactsEntity { + return new ContactsEntity({ + id, + talentId, + companyId, + }); + } + + public static initializeNew({ + talentId, + companyId, + }: ContactsProperties): ContactsEntity { + return new ContactsEntity({ + id: null, + talentId, + companyId, + }); + } + + public toObject(): ContactsProperties { + return { + id: this.id as string, + talentId: this.talentId, + companyId: this.companyId, + }; + } + + public toNewObject(): ContactsProperties { + return { + id: null, + talentId: this.talentId, + companyId: this.companyId, + }; + } +} + +export { ContactsEntity }; diff --git a/backend/src/bundles/contacts/contacts.model.ts b/backend/src/bundles/contacts/contacts.model.ts new file mode 100644 index 000000000..a43d48869 --- /dev/null +++ b/backend/src/bundles/contacts/contacts.model.ts @@ -0,0 +1,43 @@ +import { Model } from 'objection'; + +import { + AbstractModel, + ContactsTableColumn, + DatabaseTableName, + UserDetailsTableColumn, +} from '~/common/packages/database/database.js'; + +import { UserDetailsModel } from '../user-details/user-details.model.js'; + +class ContactsModel extends AbstractModel { + public 'talentId': string; + + public 'companyId': string; + + public 'hiredTime': Date | null; + + public static override get tableName(): string { + return DatabaseTableName.CONTACTS; + } + + public static override relationMappings = { + talent: { + relation: Model.BelongsToOneRelation, + modelClass: UserDetailsModel, + join: { + from: `${DatabaseTableName.HIRING_INFO}.${ContactsTableColumn.TALENT_ID}`, + to: `${DatabaseTableName.USER_DETAILS}.${UserDetailsTableColumn.USER_ID}`, + }, + }, + company: { + relation: Model.BelongsToOneRelation, + modelClass: UserDetailsModel, + join: { + from: `${DatabaseTableName.HIRING_INFO}.${ContactsTableColumn.COMPANY_ID}`, + to: `${DatabaseTableName.USER_DETAILS}.${UserDetailsTableColumn.USER_ID}`, + }, + }, + }; +} + +export { ContactsModel }; diff --git a/backend/src/bundles/contacts/contacts.repository.ts b/backend/src/bundles/contacts/contacts.repository.ts new file mode 100644 index 000000000..e7fae5733 --- /dev/null +++ b/backend/src/bundles/contacts/contacts.repository.ts @@ -0,0 +1,70 @@ +import { ErrorMessage } from 'shared/build/index.js'; + +import { type Repository } from '~/common/types/types.js'; + +import { ContactsEntity } from './contacts.entity.js'; +import { type ContactsModel } from './contacts.model.js'; +import { + type ContactsCreateRequestDto, + type ContactsFindRequestDto, +} from './types/types.js'; + +class ContactsRepository implements Repository { + private contactsModel: typeof ContactsModel; + + public constructor(contactsModel: typeof ContactsModel) { + this.contactsModel = contactsModel; + } + + public async find( + payload: ContactsFindRequestDto, + ): Promise { + const contact = await this.contactsModel.query().findOne({ + talentId: payload.talentId, + companyId: payload.companyId, + }); + + if (!contact) { + return null; + } + + return ContactsEntity.initialize({ + id: contact.id, + talentId: contact.talentId, + companyId: contact.companyId, + }); + } + + public findAll(): Promise { + throw new Error(ErrorMessage.NOT_IMPLEMENTED); + } + + public async create( + payload: ContactsCreateRequestDto, + ): Promise { + const details = await this.contactsModel + .query() + .insert({ + talentId: payload.talentId, + companyId: payload.companyId, + }) + .returning('*') + .execute(); + + return ContactsEntity.initialize({ + id: details.id, + talentId: details.talentId, + companyId: details.companyId, + }); + } + + public update(): Promise { + throw new Error(ErrorMessage.NOT_IMPLEMENTED); + } + + public delete(): Promise { + throw new Error(ErrorMessage.NOT_IMPLEMENTED); + } +} + +export { ContactsRepository }; diff --git a/backend/src/bundles/contacts/contacts.service.ts b/backend/src/bundles/contacts/contacts.service.ts new file mode 100644 index 000000000..f12fc5cf3 --- /dev/null +++ b/backend/src/bundles/contacts/contacts.service.ts @@ -0,0 +1,57 @@ +import { ErrorMessage } from '~/common/enums/enums.js'; +import { HttpCode, HttpError } from '~/common/http/http.js'; +import { type Service } from '~/common/types/service.type.js'; + +import { type ContactsRepository } from './contacts.repository.js'; +import { + type ContactsCreateRequestDto, + type ContactsFindRequestDto, + type ContactsResponseDto, +} from './types/types.js'; + +class ContactsService implements Service { + private contactsRepository: ContactsRepository; + + public constructor(contactsRepository: ContactsRepository) { + this.contactsRepository = contactsRepository; + } + + public async find(payload: ContactsFindRequestDto): Promise { + const contact = await this.contactsRepository.find({ ...payload }); + + return !!contact; + } + + public findAll(): Promise<{ items: unknown[] }> { + throw new Error(ErrorMessage.NOT_IMPLEMENTED); + } + + public async create( + payload: ContactsCreateRequestDto, + ): Promise { + const hasSharedContact = await this.contactsRepository.find(payload); + + if (hasSharedContact) { + throw new HttpError({ + message: ErrorMessage.CONTACT_ALREADY_SHARED, + status: HttpCode.BAD_REQUEST, + }); + } + + const newContact = await this.contactsRepository.create(payload); + + return { + ...newContact.toObject(), + }; + } + + public update(): Promise { + throw new Error(ErrorMessage.NOT_IMPLEMENTED); + } + + public delete(): Promise { + throw new Error(ErrorMessage.NOT_IMPLEMENTED); + } +} + +export { ContactsService }; diff --git a/backend/src/bundles/contacts/contacts.ts b/backend/src/bundles/contacts/contacts.ts new file mode 100644 index 000000000..90d412775 --- /dev/null +++ b/backend/src/bundles/contacts/contacts.ts @@ -0,0 +1,13 @@ +import { logger } from '~/common/packages/packages.js'; + +import { ContactsController } from './contacts.controller.js'; +import { ContactsModel } from './contacts.model.js'; +import { ContactsRepository } from './contacts.repository.js'; +import { ContactsService } from './contacts.service.js'; + +const contactsRepository = new ContactsRepository(ContactsModel); +const contactsService = new ContactsService(contactsRepository); +const contactsController = new ContactsController(logger, contactsService); + +export { contactsController, contactsRepository, contactsService }; +export { ContactsModel } from './contacts.model.js'; diff --git a/backend/src/bundles/contacts/enums/enums.ts b/backend/src/bundles/contacts/enums/enums.ts new file mode 100644 index 000000000..6a8c20ffb --- /dev/null +++ b/backend/src/bundles/contacts/enums/enums.ts @@ -0,0 +1 @@ +export { ContactsApiPath } from 'shared/build/index.js'; diff --git a/backend/src/bundles/contacts/types/contacts-properties.type.ts b/backend/src/bundles/contacts/types/contacts-properties.type.ts new file mode 100644 index 000000000..a11906540 --- /dev/null +++ b/backend/src/bundles/contacts/types/contacts-properties.type.ts @@ -0,0 +1,7 @@ +type ContactsProperties = { + id: string | null; + talentId: string; + companyId: string; +}; + +export { type ContactsProperties }; diff --git a/backend/src/bundles/contacts/types/types.ts b/backend/src/bundles/contacts/types/types.ts new file mode 100644 index 000000000..ee8d54949 --- /dev/null +++ b/backend/src/bundles/contacts/types/types.ts @@ -0,0 +1,6 @@ +export { type ContactsProperties } from './contacts-properties.type.js'; +export { + type ContactsCreateRequestDto, + type ContactsFindRequestDto, + type ContactsResponseDto, +} from 'shared/build/index.js'; diff --git a/backend/src/bundles/contacts/validation-schemas/contacts-create.validation-schema.ts b/backend/src/bundles/contacts/validation-schemas/contacts-create.validation-schema.ts new file mode 100644 index 000000000..22ca7f10a --- /dev/null +++ b/backend/src/bundles/contacts/validation-schemas/contacts-create.validation-schema.ts @@ -0,0 +1,13 @@ +import joi from 'joi'; + +import { type ContactsCreateRequestDto } from '../types/types.js'; + +const contactsCreateValidationSchema = joi.object< + ContactsCreateRequestDto, + true +>({ + talentId: joi.string().trim().required(), + companyId: joi.string().trim().required(), +}); + +export { contactsCreateValidationSchema }; diff --git a/backend/src/bundles/contacts/validation-schemas/validation-schemas.ts b/backend/src/bundles/contacts/validation-schemas/validation-schemas.ts new file mode 100644 index 000000000..850c0da34 --- /dev/null +++ b/backend/src/bundles/contacts/validation-schemas/validation-schemas.ts @@ -0,0 +1 @@ +export { contactsCreateValidationSchema } from './contacts-create.validation-schema.js'; diff --git a/backend/src/bundles/files/enums/enums.ts b/backend/src/bundles/files/enums/enums.ts new file mode 100644 index 000000000..832fd5787 --- /dev/null +++ b/backend/src/bundles/files/enums/enums.ts @@ -0,0 +1 @@ +export { FileRole } from 'shared/build/index.js'; diff --git a/backend/src/bundles/files/file.controller.ts b/backend/src/bundles/files/file.controller.ts index 3bf403217..64a412151 100644 --- a/backend/src/bundles/files/file.controller.ts +++ b/backend/src/bundles/files/file.controller.ts @@ -60,6 +60,18 @@ class FileController extends ControllerBase { ); }, }); + + this.addRoute({ + path: FileApiPath.$ID, + method: 'GET', + handler: (options) => { + return this.findById( + options as ApiHandlerOptions<{ + params: { id: string }; + }>, + ); + }, + }); } /** @@ -112,6 +124,19 @@ class FileController extends ControllerBase { payload: uploadResponse, }; } + + private async findById( + options: ApiHandlerOptions<{ + params: { id: string }; + }>, + ): Promise { + const { id } = options.params; + + return { + status: HttpCode.OK, + payload: await this.fileService.findById(id), + }; + } } export { FileController }; diff --git a/backend/src/bundles/files/file.entity.ts b/backend/src/bundles/files/file.entity.ts index c355ce36a..65d95c3de 100644 --- a/backend/src/bundles/files/file.entity.ts +++ b/backend/src/bundles/files/file.entity.ts @@ -1,5 +1,7 @@ import { type Entity } from '~/common/types/types.js'; +import { type GetFileResponseDto } from './types/types.js'; + class FileEntity implements Entity { private 'id': string | null; @@ -31,12 +33,7 @@ class FileEntity implements Entity { url, fileName, etag, - }: { - id: string; - url: string; - fileName: string; - etag: string; - }): FileEntity { + }: GetFileResponseDto): FileEntity { return new FileEntity({ id, url, diff --git a/backend/src/bundles/files/file.repository.ts b/backend/src/bundles/files/file.repository.ts index d9bb0f8ee..861772607 100644 --- a/backend/src/bundles/files/file.repository.ts +++ b/backend/src/bundles/files/file.repository.ts @@ -9,8 +9,11 @@ import { type Repository } from '~/common/types/repository.type.js'; import { FileEntity } from './file.entity.js'; import { type FileModel } from './file.model.js'; -import { getFileType } from './helpers/get-file-type.helper.js'; -import { type FileUploadResponse } from './types/types.js'; +import { getFileRole } from './helpers/helpers.js'; +import { + type FileUploadResponse, + type GetFileRequestDto, +} from './types/types.js'; class FileRepository implements Repository { private fileModel: typeof FileModel; @@ -21,15 +24,20 @@ class FileRepository implements Repository { this.fileStorage = fileStorage; } - public find(): Promise { - throw new Error(ErrorMessage.NOT_IMPLEMENTED); + public async find(payload: GetFileRequestDto): Promise { + const file = await this.fileModel.query().findOne({ ...payload }); + + if (!file) { + return null; + } + return FileEntity.initialize(file); } public findAll(): Promise { throw new Error(ErrorMessage.NOT_IMPLEMENTED); } - public async create(file: S3.ManagedUpload.SendData): Promise { + public create(file: S3.ManagedUpload.SendData): Promise { return this.fileModel .query() .insert({ @@ -49,9 +57,9 @@ class FileRepository implements Repository { for (const file of response) { const data = await this.create(file); - const type = getFileType(file.Key); + const role = getFileRole(file.Key); const entity = FileEntity.initialize(data).toObject(); - uploadedFiles[type as keyof typeof uploadedFiles] = { + uploadedFiles[role as keyof typeof uploadedFiles] = { id: entity.id, url: entity.url, }; diff --git a/backend/src/bundles/files/file.service.ts b/backend/src/bundles/files/file.service.ts index bd792b9d8..b5817053d 100644 --- a/backend/src/bundles/files/file.service.ts +++ b/backend/src/bundles/files/file.service.ts @@ -3,6 +3,7 @@ import { type File as MulterFile } from 'fastify-multer/lib/interfaces.js'; import { ErrorMessage } from '~/common/enums/enums.js'; import { type Service } from '~/common/types/types.js'; +import { type FileEntity } from './file.entity.js'; import { type FileRepository } from './file.repository.js'; import { type FileUploadResponse } from './types/types.js'; @@ -17,6 +18,10 @@ class FileService implements Service { throw new Error(ErrorMessage.NOT_IMPLEMENTED); } + public findById(id: string): Promise { + return this.fileRepository.find({ id }); + } + public findAll(): Promise<{ items: unknown[] }> { throw new Error(ErrorMessage.NOT_IMPLEMENTED); } diff --git a/backend/src/bundles/files/helpers/generate-random-id.helper.ts b/backend/src/bundles/files/helpers/generate-random-id.helper.ts index 47f1e0e8b..1559bb87d 100644 --- a/backend/src/bundles/files/helpers/generate-random-id.helper.ts +++ b/backend/src/bundles/files/helpers/generate-random-id.helper.ts @@ -1,9 +1,9 @@ const RADIX = 36; const generateRandomId = (fileName: string): string => { - const [extension] = fileName.split('.').reverse(); + const [extension, name] = fileName.split('.').reverse(); - const randomId = Math.random().toString(RADIX).replace('0.', 'file_'); + const randomId = Math.random().toString(RADIX).replace('0.', `${name}_`); return `${randomId}.${extension}`; }; diff --git a/backend/src/bundles/files/helpers/get-file-role.helper.ts b/backend/src/bundles/files/helpers/get-file-role.helper.ts new file mode 100644 index 000000000..ec843240d --- /dev/null +++ b/backend/src/bundles/files/helpers/get-file-role.helper.ts @@ -0,0 +1,8 @@ +import { type FileRoleValue } from '../types/types.js'; + +const getFileRole = (fileName: string): FileRoleValue => { + const [role]: FileRoleValue[] = fileName.split('_') as FileRoleValue[]; + return role; +}; + +export { getFileRole }; diff --git a/backend/src/bundles/files/helpers/get-file-type.helper.ts b/backend/src/bundles/files/helpers/get-file-type.helper.ts deleted file mode 100644 index 9dc6e466e..000000000 --- a/backend/src/bundles/files/helpers/get-file-type.helper.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { type ValueOf } from 'shared/build/index.js'; - -import { AllowedExtensions } from '~/common/plugins/file-upload/enums/file-extension.enum.js'; -import { FileGroups } from '~/common/plugins/file-upload/enums/file-group.enum.js'; - -const allowedDocumentExtensions = new Set>([ - AllowedExtensions.DOC, - AllowedExtensions.DOCX, - AllowedExtensions.PDF, -]); - -const getFileType = (fileName: string): ValueOf => { - const [extension]: [ValueOf] = fileName - .split('.') - .reverse() as [ValueOf]; - - const isDocument = allowedDocumentExtensions.has(extension); - return isDocument ? FileGroups.DOCUMENT : FileGroups.IMAGE; -}; - -export { getFileType }; diff --git a/backend/src/bundles/files/helpers/helpers.ts b/backend/src/bundles/files/helpers/helpers.ts index 4fac79a3b..2daa56171 100644 --- a/backend/src/bundles/files/helpers/helpers.ts +++ b/backend/src/bundles/files/helpers/helpers.ts @@ -1 +1,2 @@ export { generateRandomId } from './generate-random-id.helper.js'; +export { getFileRole } from './get-file-role.helper.js'; diff --git a/backend/src/bundles/files/types/types.ts b/backend/src/bundles/files/types/types.ts index 269d90d8f..1a424b0c0 100644 --- a/backend/src/bundles/files/types/types.ts +++ b/backend/src/bundles/files/types/types.ts @@ -1,4 +1,7 @@ export { + type FileRoleValue, type FileUploadResponse, + type GetFileRequestDto, + type GetFileResponseDto, type UploadedFile, } from 'shared/build/index.js'; diff --git a/backend/src/bundles/hiring-info/hiring-info.controller.ts b/backend/src/bundles/hiring-info/hiring-info.controller.ts index 438409bfc..3016dbf0c 100644 --- a/backend/src/bundles/hiring-info/hiring-info.controller.ts +++ b/backend/src/bundles/hiring-info/hiring-info.controller.ts @@ -9,7 +9,10 @@ import { ControllerBase } from '~/common/packages/packages.js'; import { HiringInfoApiPath } from './enums/enums.js'; import { type HiringInfoService } from './hiring-info.service.js'; -import { type HiringInfoCreateRequestDto } from './types/types.js'; +import { + type HiringInfoCreateRequestDto, + type HiringInfoFindRequestDto, +} from './types/types.js'; import { hiringInfoCreateValidationSchema } from './validation-schemas/validation-schemas.js'; /** @@ -60,12 +63,27 @@ class HiringInfoController extends ControllerBase { }); this.addRoute({ - path: HiringInfoApiPath.ROOT, + path: HiringInfoApiPath.ALL, method: 'GET', handler: () => { return this.findAll(); }, }); + + this.addRoute({ + path: HiringInfoApiPath.ROOT, + method: 'GET', + validation: { + query: hiringInfoCreateValidationSchema, + }, + handler: (options) => { + return this.findHiringInfo( + options as ApiHandlerOptions<{ + query: HiringInfoFindRequestDto; + }>, + ); + }, + }); } /** @@ -126,7 +144,7 @@ class HiringInfoController extends ControllerBase { /** * @swagger - * /hiring-info: + * /hiring-info/all: * get: * tags: [Hiring Info] * description: Returns all hiring info records @@ -185,6 +203,43 @@ class HiringInfoController extends ControllerBase { payload: await this.hiringInfoService.findAll(), }; } + + /** + * @swagger + * /hiring-info: + * get: + * tags: [Hiring Info] + * description: Returns true if talent is hired + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: talentId + * schema: + * type: uuid + * - in: query + * name: companyId + * schema: + * type: uuid + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: boolean + * + */ + private async findHiringInfo( + options: ApiHandlerOptions<{ + query: HiringInfoFindRequestDto; + }>, + ): Promise { + return { + status: HttpCode.OK, + payload: await this.hiringInfoService.find(options.query), + }; + } } export { HiringInfoController }; diff --git a/backend/src/bundles/hiring-info/hiring-info.repository.ts b/backend/src/bundles/hiring-info/hiring-info.repository.ts index d121cb739..3d09fe1fc 100644 --- a/backend/src/bundles/hiring-info/hiring-info.repository.ts +++ b/backend/src/bundles/hiring-info/hiring-info.repository.ts @@ -4,7 +4,10 @@ import { type Repository } from '~/common/types/types.js'; import { HiringInfoEntity } from './hiring-info.entity.js'; import { type HiringInfoModel } from './hiring-info.model.js'; -import { type HiringInfoCreateRequestDto } from './types/types.js'; +import { + type HiringInfoCreateRequestDto, + type HiringInfoFindRequestDto, +} from './types/types.js'; class HiringInfoRepository implements Repository { private hiringInfoModel: typeof HiringInfoModel; @@ -13,8 +16,24 @@ class HiringInfoRepository implements Repository { this.hiringInfoModel = hiringInfoModel; } - public find(): Promise { - throw new Error(ErrorMessage.NOT_IMPLEMENTED); + public async find( + payload: HiringInfoFindRequestDto, + ): Promise { + const hiringInfo = await this.hiringInfoModel.query().findOne({ + talentId: payload.talentId, + companyId: payload.companyId, + }); + + if (!hiringInfo) { + return null; + } + + return HiringInfoEntity.initialize({ + id: hiringInfo.id, + talentId: hiringInfo.talentId, + companyId: hiringInfo.companyId, + hiredTime: hiringInfo.hiredTime, + }); } public async findAll(): Promise { @@ -49,7 +68,8 @@ class HiringInfoRepository implements Repository { const details = await this.hiringInfoModel .query() .insert({ - ...payload, + talentId: payload.talentId, + companyId: payload.companyId, }) .returning('*') .execute(); diff --git a/backend/src/bundles/hiring-info/hiring-info.service.ts b/backend/src/bundles/hiring-info/hiring-info.service.ts index 87c60b5ef..045580686 100644 --- a/backend/src/bundles/hiring-info/hiring-info.service.ts +++ b/backend/src/bundles/hiring-info/hiring-info.service.ts @@ -1,10 +1,10 @@ import { ErrorMessage } from '~/common/enums/enums.js'; import { type Service } from '~/common/types/service.type.js'; -import { type HiringInfoEntity } from './hiring-info.entity.js'; import { type HiringInfoRepository } from './hiring-info.repository.js'; import { type HiringInfoCreateRequestDto, + type HiringInfoFindRequestDto, type HiringInfoResponseDto, } from './types/types.js'; @@ -15,8 +15,10 @@ class HiringInfoService implements Service { this.hiringInfoRepository = hiringInfoRepository; } - public find(): Promise { - throw new Error(ErrorMessage.NOT_IMPLEMENTED); + public async find(payload: HiringInfoFindRequestDto): Promise { + const hiringInfo = await this.hiringInfoRepository.find({ ...payload }); + + return !!hiringInfo; } public async findAll(): Promise<{ items: HiringInfoResponseDto[] }> { diff --git a/backend/src/bundles/hiring-info/types/types.ts b/backend/src/bundles/hiring-info/types/types.ts index d63828330..1db402deb 100644 --- a/backend/src/bundles/hiring-info/types/types.ts +++ b/backend/src/bundles/hiring-info/types/types.ts @@ -1,5 +1,7 @@ export { type HiringInfoProperties } from './hiring-info-properties.type.js'; export { type HiringInfoCreateRequestDto, + type HiringInfoFindRequestDto, + type HiringInfoFindResponseDto, type HiringInfoResponseDto, } from 'shared/build/index.js'; diff --git a/backend/src/bundles/lms-data/lms-data.service.ts b/backend/src/bundles/lms-data/lms-data.service.ts index 9532056cd..177d12a95 100644 --- a/backend/src/bundles/lms-data/lms-data.service.ts +++ b/backend/src/bundles/lms-data/lms-data.service.ts @@ -40,27 +40,11 @@ class LMSDataService implements Service { const userEmail = user?.toObject().email; if (userEmail) { - const dataFromLMS = await this.findByUserEmailOnLMSServer( + const dataFromLMS = await this.getUserDataFromLMSServerbyEmail( userEmail, ); if (dataFromLMS) { - const parsedLMSData = parseLMSServerData(userId, dataFromLMS); - - const newDBRecord = await this.lmsDataRepository.create( - LMSDataEntity.initialize({ - ...parsedLMSData, - userId: user.toObject().id, - lectureDetails: JSON.stringify( - parsedLMSData.lectureDetails, - ), - projectCoachesFeedback: JSON.stringify( - parsedLMSData.projectCoachesFeedback, - ), - hrFeedback: JSON.stringify(parsedLMSData.hrFeedback), - project: JSON.stringify(parsedLMSData.project), - }), - ); - return newDBRecord.toObject(); + await this.addUserLMSDataToDB(user.toObject().id, dataFromLMS); } throw new HttpError({ @@ -75,7 +59,32 @@ class LMSDataService implements Service { }); } - private async findByUserEmailOnLMSServer( + public async addUserLMSDataToDB( + userId: string, + dataFromLMS: LMSDataServerResponseDto | null, + ): Promise { + if (!dataFromLMS) { + return null; + } + + const parsedLMSData = parseLMSServerData(userId, dataFromLMS); + + const newDBRecord = await this.lmsDataRepository.create( + LMSDataEntity.initialize({ + ...parsedLMSData, + userId, + lectureDetails: JSON.stringify(parsedLMSData.lectureDetails), + projectCoachesFeedback: JSON.stringify( + parsedLMSData.projectCoachesFeedback, + ), + hrFeedback: JSON.stringify(parsedLMSData.hrFeedback), + project: JSON.stringify(parsedLMSData.project), + }), + ); + return newDBRecord.toObject(); + } + + public async getUserDataFromLMSServerbyEmail( email: string, ): Promise { const url = new URL(config.ENV.LMS_DATA_SERVER.LMS_SERVER); diff --git a/backend/src/bundles/user-details/types/types.ts b/backend/src/bundles/user-details/types/types.ts index 2be996ea1..988e8196c 100644 --- a/backend/src/bundles/user-details/types/types.ts +++ b/backend/src/bundles/user-details/types/types.ts @@ -1,4 +1,5 @@ export { type UserDetailsProperties } from './user-details-properties.type.js'; +export { type UserDetailsWithFiles } from './user-details-with-files.type.js'; export { type TalentHardSkill, type UserDetailsCreateDto, diff --git a/backend/src/bundles/user-details/types/user-details-properties.type.ts b/backend/src/bundles/user-details/types/user-details-properties.type.ts index f1091076f..bc40f2eff 100644 --- a/backend/src/bundles/user-details/types/user-details-properties.type.ts +++ b/backend/src/bundles/user-details/types/user-details-properties.type.ts @@ -40,6 +40,7 @@ type UserDetailsProperties = { completedStep: ValueOf | null; createdAt: string | null; email?: string | null; + publishedAt: string | null; }; export { type UserDetailsProperties }; diff --git a/backend/src/bundles/user-details/types/user-details-with-files.type.ts b/backend/src/bundles/user-details/types/user-details-with-files.type.ts new file mode 100644 index 000000000..5db62ac4d --- /dev/null +++ b/backend/src/bundles/user-details/types/user-details-with-files.type.ts @@ -0,0 +1,11 @@ +import { type FileModel } from '~/bundles/files/file.model.js'; + +import { type UserDetailsModel } from '../user-details.model.js'; + +type UserDetailsWithFiles = UserDetailsModel & { + cv?: FileModel | null; + photo?: FileModel | null; + companyLogo?: FileModel | null; +}; + +export { type UserDetailsWithFiles }; diff --git a/backend/src/bundles/user-details/user-details.entity.ts b/backend/src/bundles/user-details/user-details.entity.ts index 4cb50bdb7..4705aa234 100644 --- a/backend/src/bundles/user-details/user-details.entity.ts +++ b/backend/src/bundles/user-details/user-details.entity.ts @@ -70,6 +70,8 @@ class UserDetailsEntity implements Entity { private 'email'?: string | null; + private 'publishedAt': string | null; + private constructor({ id, userId, @@ -100,6 +102,7 @@ class UserDetailsEntity implements Entity { completedStep, email, createdAt, + publishedAt, }: UserDetailsProperties) { this.id = id; this.userId = userId; @@ -130,6 +133,7 @@ class UserDetailsEntity implements Entity { this.completedStep = completedStep; this.email = email; this.createdAt = createdAt; + this.publishedAt = publishedAt; } public static initialize({ @@ -162,6 +166,7 @@ class UserDetailsEntity implements Entity { completedStep, email, createdAt, + publishedAt, }: UserDetailsProperties): UserDetailsEntity { return new UserDetailsEntity({ id, @@ -193,6 +198,7 @@ class UserDetailsEntity implements Entity { completedStep, email, createdAt, + publishedAt, }); } @@ -225,6 +231,7 @@ class UserDetailsEntity implements Entity { completedStep, email, createdAt, + publishedAt, }: UserDetailsProperties): UserDetailsEntity { return new UserDetailsEntity({ id: null, @@ -256,6 +263,7 @@ class UserDetailsEntity implements Entity { completedStep, email, createdAt, + publishedAt, }); } @@ -290,6 +298,7 @@ class UserDetailsEntity implements Entity { completedStep: this.completedStep, email: this.email, createdAt: this.createdAt, + publishedAt: this.publishedAt, }; } @@ -324,6 +333,7 @@ class UserDetailsEntity implements Entity { completedStep: this.completedStep, email: this.email, createdAt: this.createdAt, + publishedAt: this.publishedAt, }; } } diff --git a/backend/src/bundles/user-details/user-details.model.ts b/backend/src/bundles/user-details/user-details.model.ts index 611fc61af..e84c46f22 100644 --- a/backend/src/bundles/user-details/user-details.model.ts +++ b/backend/src/bundles/user-details/user-details.model.ts @@ -83,7 +83,7 @@ class UserDetailsModel extends AbstractModel { public 'completedStep': ValueOf; - public 'publishedAt': Date; + public 'publishedAt': string; public 'user'?: UserModel; diff --git a/backend/src/bundles/user-details/user-details.repository.ts b/backend/src/bundles/user-details/user-details.repository.ts index 78c83aef8..06ca7bc27 100644 --- a/backend/src/bundles/user-details/user-details.repository.ts +++ b/backend/src/bundles/user-details/user-details.repository.ts @@ -11,7 +11,9 @@ import { searchUserByRelativeTable } from './helpers/search-user-by-relative-tab import { type UserDetailsCreateDto, type UserDetailsFindRequestDto, + type UserDetailsResponseDto, type UserDetailsUpdateDto, + type UserDetailsWithFiles, } from './types/types.js'; import { UserDetailsEntity } from './user-details.entity.js'; import { type UserDetailsModel } from './user-details.model.js'; @@ -63,6 +65,7 @@ class UserDetailsRepository implements Repository { cvId: details.cvId, completedStep: details.completedStep, createdAt: details.createdAt, + publishedAt: details.publishedAt, }); } @@ -109,6 +112,7 @@ class UserDetailsRepository implements Repository { employerPosition: details.employerPosition ?? '', cvId: details.cvId, completedStep: details.completedStep, + publishedAt: details.publishedAt, }); } @@ -231,7 +235,7 @@ class UserDetailsRepository implements Repository { public async create( payload: UserDetailsCreateDto, - ): Promise { + ): Promise { const details = await this.userDetailsModel .query() .insert({ @@ -240,7 +244,13 @@ class UserDetailsRepository implements Repository { .returning('*') .execute(); - return UserDetailsEntity.initialize({ + const files = (await this.userDetailsModel + .query() + .findById(details.id) + .withGraphFetched('[cv, photo, companyLogo]') + .execute()) as UserDetailsWithFiles; + + const detailsWithFiles = UserDetailsEntity.initialize({ id: details.id, userId: details.userId, isApproved: details.isApproved, @@ -269,19 +279,33 @@ class UserDetailsRepository implements Repository { cvId: details.cvId, completedStep: details.completedStep, createdAt: details.createdAt, - }); + publishedAt: details.publishedAt, + }).toObject(); + + return { + ...detailsWithFiles, + cvUrl: files.cv?.url ?? null, + photoUrl: files.photo?.url ?? null, + companyLogoUrl: files.companyLogo?.url ?? null, + }; } public async update( payload: UserDetailsUpdateDto, - ): Promise { + ): Promise { const { id, ...rest } = payload; const details = await this.userDetailsModel .query() .patchAndFetchById(id as string, rest); - return UserDetailsEntity.initialize({ + const files = (await this.userDetailsModel + .query() + .findById(details.id) + .withGraphFetched('[cv, photo, companyLogo]') + .execute()) as UserDetailsWithFiles; + + const detailsWithFiles = UserDetailsEntity.initialize({ id: details.id, userId: details.userId, isApproved: details.isApproved, @@ -310,16 +334,29 @@ class UserDetailsRepository implements Repository { cvId: details.cvId, completedStep: details.completedStep, createdAt: details.createdAt, - }); + publishedAt: details.publishedAt, + }).toObject(); + + return { + ...detailsWithFiles, + cvUrl: files.cv?.url ?? null, + photoUrl: files.photo?.url ?? null, + companyLogoUrl: files.companyLogo?.url ?? null, + }; } - public async publish(payload: UserDetailsUpdateDto): Promise { + public async publish( + payload: UserDetailsUpdateDto, + ): Promise { const { id } = payload; const details = await this.userDetailsModel .query() - .patchAndFetchById(id as string, { publishedAt: new Date() }); - return details.publishedAt.toLocaleString(); + .patchAndFetchById(id as string, { + publishedAt: new Date().toISOString(), + }); + + return UserDetailsEntity.initialize(details); } public delete(): Promise { diff --git a/backend/src/bundles/user-details/user-details.service.ts b/backend/src/bundles/user-details/user-details.service.ts index c49710687..a84292c42 100644 --- a/backend/src/bundles/user-details/user-details.service.ts +++ b/backend/src/bundles/user-details/user-details.service.ts @@ -127,7 +127,7 @@ class UserDetailsService implements Service { userDetails, ); - const userDetailsId = newUserDetails.toObject().id as string; + const userDetailsId = newUserDetails.id as string; let badgesResult: TalentBadge[] = [], hardSkillsResult: TalentHardSkill[] = []; @@ -156,7 +156,7 @@ class UserDetailsService implements Service { } return { - ...newUserDetails.toObject(), + ...newUserDetails, talentBadges: badgesResult, talentHardSkills: hardSkillsResult, }; @@ -206,7 +206,7 @@ class UserDetailsService implements Service { }); return { - ...updatedUserDetails.toObject(), + ...updatedUserDetails, talentBadges: badgesResult, talentHardSkills: hardSkillsResult, }; @@ -257,7 +257,9 @@ class UserDetailsService implements Service { return true; } - public async publish(payload: { userId: string }): Promise { + public async publish(payload: { + userId: string; + }): Promise { const { userId } = payload; const userDetails = await this.userDetailsRepository.find({ userId }); diff --git a/backend/src/bundles/users/users.ts b/backend/src/bundles/users/users.ts index 14ab4f005..199e1d63a 100644 --- a/backend/src/bundles/users/users.ts +++ b/backend/src/bundles/users/users.ts @@ -1,6 +1,6 @@ +import { lmsDataService } from '~/bundles/lms-data/lms-data.js'; import { encrypt, logger } from '~/common/packages/packages.js'; -import { lmsDataService } from '../lms-data/lms-data.js'; import { UserController } from './user.controller.js'; import { UserModel } from './user.model.js'; import { UserRepository } from './user.repository.js'; @@ -10,6 +10,7 @@ const userRepository = new UserRepository(UserModel); const userService = new UserService(userRepository, encrypt); const userController = new UserController(logger, userService, lmsDataService); +export { lmsDataService } from '~/bundles/lms-data/lms-data.js'; export { userController, userRepository, userService }; export { type UserForgotPasswordRequestDto, diff --git a/backend/src/common/packages/database/database.ts b/backend/src/common/packages/database/database.ts index 5a8769942..27b0787f6 100644 --- a/backend/src/common/packages/database/database.ts +++ b/backend/src/common/packages/database/database.ts @@ -9,6 +9,7 @@ export { Abstract as AbstractModel } from './abstract.model.js'; export { BSABadgesTableColumn, ChatMessagesTableColumn, + ContactsTableColumn, DatabaseTableName, FilesTableColumn, HardSkillsTableColumn, diff --git a/backend/src/common/packages/database/enums/database-table-name.enum.ts b/backend/src/common/packages/database/enums/database-table-name.enum.ts index b436e3380..b04630b77 100644 --- a/backend/src/common/packages/database/enums/database-table-name.enum.ts +++ b/backend/src/common/packages/database/enums/database-table-name.enum.ts @@ -10,6 +10,7 @@ const DatabaseTableName = { HARD_SKILLS: 'hard_skills', HIRING_INFO: 'hiring_info', CHAT_MESSAGES: 'chat_messages', + CONTACTS: 'contacts', } as const; export { DatabaseTableName }; diff --git a/backend/src/common/packages/database/enums/enums.ts b/backend/src/common/packages/database/enums/enums.ts index f50b992ac..7794d1f6e 100644 --- a/backend/src/common/packages/database/enums/enums.ts +++ b/backend/src/common/packages/database/enums/enums.ts @@ -2,6 +2,7 @@ export { DatabaseTableName } from './database-table-name.enum.js'; export { BSABadgesTableColumn, ChatMessagesTableColumn, + ContactsTableColumn, FilesTableColumn, HardSkillsTableColumn, HiringInfoTableColumn, diff --git a/backend/src/common/packages/database/enums/table-columns/contacts-table-column.enum.ts b/backend/src/common/packages/database/enums/table-columns/contacts-table-column.enum.ts new file mode 100644 index 000000000..3e78e0c2d --- /dev/null +++ b/backend/src/common/packages/database/enums/table-columns/contacts-table-column.enum.ts @@ -0,0 +1,9 @@ +const ContactsTableColumn = { + ID: 'id', + TALENT_ID: 'talent_id', + COMPANY_ID: 'company_id', + CREATED_AT: 'created_at', + UPDATED_AT: 'updated_at', +} as const; + +export { ContactsTableColumn }; diff --git a/backend/src/common/packages/database/enums/table-columns/enums.ts b/backend/src/common/packages/database/enums/table-columns/enums.ts index 81e036eb4..cbab99be0 100644 --- a/backend/src/common/packages/database/enums/table-columns/enums.ts +++ b/backend/src/common/packages/database/enums/table-columns/enums.ts @@ -1,5 +1,6 @@ export { BSABadgesTableColumn } from './bsa-badges-table-column.enum.js'; export { ChatMessagesTableColumn } from './chat-messages-table-column.enum.js'; +export { ContactsTableColumn } from './contacts-table-column.enum.js'; export { FilesTableColumn } from './files-table-column.enum.js'; export { HardSkillsTableColumn } from './hard-skills-table-column.enum.js'; export { HiringInfoTableColumn } from './hiring-info-table-column.enum.js'; diff --git a/backend/src/common/packages/database/enums/table-columns/hiring-info-table-column.enum.ts b/backend/src/common/packages/database/enums/table-columns/hiring-info-table-column.enum.ts index 63724ce88..71e7ed8de 100644 --- a/backend/src/common/packages/database/enums/table-columns/hiring-info-table-column.enum.ts +++ b/backend/src/common/packages/database/enums/table-columns/hiring-info-table-column.enum.ts @@ -2,16 +2,7 @@ const HiringInfoTableColumn = { ID: 'id', TALENT_ID: 'talent_id', COMPANY_ID: 'company_id', - FIRST_CONTACT_TIME: 'first_contact_time', - HAS_SHARED_INFO: 'has_shared_info', - SHARED_INFO_TIME: 'shared_info_time', - IS_HIRED: 'is_hired', HIRED_TIME: 'hired_time', - HIRED_SALARY: 'hired_salary', - HIRED_POSITION: 'hired_position', - STATUS: 'status', - FEE: 'fee', - IS_APPROVED: 'is_approved', CREATED_AT: 'created_at', UPDATED_AT: 'updated_at', } as const; diff --git a/backend/src/common/server-application/server-application.ts b/backend/src/common/server-application/server-application.ts index f84149cf2..c5626baa7 100644 --- a/backend/src/common/server-application/server-application.ts +++ b/backend/src/common/server-application/server-application.ts @@ -1,6 +1,7 @@ import { authController } from '~/bundles/auth/auth.js'; import { bsaBadgesController } from '~/bundles/bsa-badges/bsa-badges.js'; import { chatMessagesController } from '~/bundles/chat-messages/chat-messages.js'; +import { contactsController } from '~/bundles/contacts/contacts.js'; import { fileController } from '~/bundles/files/files.js'; import { hardSkillsController } from '~/bundles/hard-skills/hard-skills.js'; import { hiringInfoController } from '~/bundles/hiring-info/hiring-info.js'; @@ -16,6 +17,7 @@ const apiV1 = new ServerAppApiBase( config, ...authController.routes, ...userController.routes, + ...contactsController.routes, ...userDetailsController.routes, ...fileController.routes, ...hardSkillsController.routes, diff --git a/backend/src/migrations/20230926173256_add_contacts_table.ts b/backend/src/migrations/20230926173256_add_contacts_table.ts new file mode 100644 index 000000000..75976ff1a --- /dev/null +++ b/backend/src/migrations/20230926173256_add_contacts_table.ts @@ -0,0 +1,64 @@ +import { type Knex } from 'knex'; + +const UUID = 'uuid_generate_v4()'; +const CONTRAINT_NAME = 'contacts_pkey'; + +const TableName = { + CONTACTS: 'contacts', + USER_DETAILS: 'user_details', +} as const; + +const ColumnName = { + ID: 'id', + USER_ID: 'user_id', + TALENT_ID: 'talent_id', + COMPANY_ID: 'company_id', + CREATED_AT: 'created_at', + UPDATED_AT: 'updated_at', +} as const; + +const RelationRule = { + CASCADE: 'CASCADE', + SET_NULL: 'SET NULL', +} as const; + +async function up(knex: Knex): Promise { + await knex.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'); + + return knex.schema.createTable(TableName.CONTACTS, (table) => { + table + .uuid(ColumnName.ID) + .unique() + .notNullable() + .defaultTo(knex.raw(UUID)) + .primary({ constraintName: CONTRAINT_NAME }); + table + .uuid(ColumnName.TALENT_ID) + .notNullable() + .references(ColumnName.USER_ID) + .inTable(TableName.USER_DETAILS) + .onUpdate(RelationRule.CASCADE) + .onDelete(RelationRule.CASCADE); + table + .uuid(ColumnName.COMPANY_ID) + .notNullable() + .references(ColumnName.USER_ID) + .inTable(TableName.USER_DETAILS) + .onUpdate(RelationRule.CASCADE) + .onDelete(RelationRule.CASCADE); + table + .dateTime(ColumnName.CREATED_AT) + .notNullable() + .defaultTo(knex.fn.now()); + table + .dateTime(ColumnName.UPDATED_AT) + .notNullable() + .defaultTo(knex.fn.now()); + }); +} + +async function down(knex: Knex): Promise { + return knex.schema.dropTableIfExists(TableName.CONTACTS); +} + +export { down, up }; diff --git a/backend/src/migrations/20230927022842_update_talent_hard_skills_table.ts b/backend/src/migrations/20230927022842_update_talent_hard_skills_table.ts new file mode 100644 index 000000000..06aeba9e6 --- /dev/null +++ b/backend/src/migrations/20230927022842_update_talent_hard_skills_table.ts @@ -0,0 +1,33 @@ +import { type Knex } from 'knex'; + +const TableName = { + TALENT_HARD_SKILLS: 'talent_hard_skills', +} as const; + +const ColumnName = { + HARD_SKILL_ID: 'hard_skill_id', +} as const; + +async function up(knex: Knex): Promise { + return knex.schema.alterTable(TableName.TALENT_HARD_SKILLS, (table) => { + table.dropUnique([ColumnName.HARD_SKILL_ID]); + }); +} + +async function down(knex: Knex): Promise { + await knex.raw(` + DELETE FROM ${TableName.TALENT_HARD_SKILLS} + WHERE ${ColumnName.HARD_SKILL_ID} IN ( + SELECT ${ColumnName.HARD_SKILL_ID} + FROM ${TableName.TALENT_HARD_SKILLS} + GROUP BY ${ColumnName.HARD_SKILL_ID} + HAVING COUNT(*) > 1 + ) +`); + + return knex.schema.alterTable(TableName.TALENT_HARD_SKILLS, (table) => { + table.unique([ColumnName.HARD_SKILL_ID]); + }); +} + +export { down, up }; diff --git a/frontend/src/bundles/admin-panel/pages/connections/connections-panel.tsx b/frontend/src/bundles/admin-panel/pages/connections/connections-panel.tsx index ef1ff67a6..5e3918ff4 100644 --- a/frontend/src/bundles/admin-panel/pages/connections/connections-panel.tsx +++ b/frontend/src/bundles/admin-panel/pages/connections/connections-panel.tsx @@ -1,6 +1,5 @@ import { ManageSearch } from '@mui/icons-material'; -import { actions as adminActions } from '~/bundles/admin-panel/store/admin.js'; import { type BodyRow } from '~/bundles/common/components/components.js'; import { Button, @@ -19,6 +18,7 @@ import { useState, useTheme, } from '~/bundles/common/hooks/hooks.js'; +import { actions as adminActions } from '~/bundles/hiring-info/store/hiring-info.js'; import { ChatList } from '../../components/chat-list/chat-list.js'; import { CVAndContacts } from '../../components/components.js'; @@ -33,10 +33,11 @@ import { type TabValues } from '../../types/types.js'; import styles from './styles.module.scss'; const tabs = [ - { - label: 'Chats', - labelItemCount: 1, - }, + //TODO: add here hiring info chats talent-company + // { + // label: 'Chats', + // labelItemCount: 1, + // }, { label: 'Hirings', labelItemCount: 1, @@ -44,7 +45,7 @@ const tabs = [ ] as AdminTab[]; const AdminConnectionsPanel: React.FC = () => { - const [activeTab, setActiveTab] = useState('Chats'); + const [activeTab, setActiveTab] = useState('Hirings'); const theme = useTheme(); const dispatch = useAppDispatch(); @@ -182,7 +183,8 @@ const AdminConnectionsPanel: React.FC = () => { )} - + {/* TODO: hire approve from admin page + + )} + + {icon} +

{name}

+ + {isNotificationVisible && ( + + )} + + ); +}; + +export { SidebarItem }; diff --git a/frontend/src/bundles/common/components/sidebar/sidebar-notification/sidebar-notification.tsx b/frontend/src/bundles/common/components/sidebar/sidebar-notification/sidebar-notification.tsx new file mode 100644 index 000000000..64ff492c7 --- /dev/null +++ b/frontend/src/bundles/common/components/sidebar/sidebar-notification/sidebar-notification.tsx @@ -0,0 +1,25 @@ +import { Close } from '@mui/icons-material'; + +import { Typography } from '../../components.js'; +import styles from './styles.module.scss'; + +type Properties = { + isVisible: boolean; + handleClose: () => void; +}; + +const SidebarNotification: React.FC = ({ + isVisible, + handleClose, +}) => { + return ( +
+ + You can't visit this page until your account is approved + + +
+ ); +}; + +export { SidebarNotification }; diff --git a/frontend/src/bundles/common/components/sidebar/sidebar-notification/styles.module.scss b/frontend/src/bundles/common/components/sidebar/sidebar-notification/styles.module.scss new file mode 100644 index 000000000..cc8d2fe2d --- /dev/null +++ b/frontend/src/bundles/common/components/sidebar/sidebar-notification/styles.module.scss @@ -0,0 +1,65 @@ +.notification { + position: absolute; + top: 0; + left: calc(var(--sidebar-width) + 10px); + z-index: 5; + width: 200px; + height: 45px; + padding: 5px 10px; + background-color: var(--sunglow); + border-radius: 5px; + transform: translateY(-40%); + opacity: 0; + transition: + width 0.5s, + opacity 0.5s; + + &::before { + content: ""; + position: absolute; + bottom: 50%; + left: 0; + border-width: 7px 0 13px 25px; + border-style: solid; + border-color: transparent transparent var(--sunglow) transparent; + transform: translate(-100%, 50%); + } +} + +.notification.shown { + animation: fade-in-out 0.4s ease-in-out forwards; +} + +@keyframes fade-in-out { + 0% { + transform: translate(-50%, -40%); + opacity: 0; + } + + 100% { + transform: translate(0, -40%); + opacity: 1; + } +} + +.notification.hidden { + width: 0; + opacity: 0; +} + +.notification .message { + width: 95%; + height: 32px; + margin: 0; + overflow: hidden; + color: var(--midnight-black); + font-size: var(--font-size-caption); +} + +.icon { + position: absolute; + top: 1px; + right: 5px; + width: 15px; + cursor: pointer; +} diff --git a/frontend/src/bundles/common/components/sidebar/sidebar.tsx b/frontend/src/bundles/common/components/sidebar/sidebar.tsx index 4e3a7d4c1..3c52bef5f 100644 --- a/frontend/src/bundles/common/components/sidebar/sidebar.tsx +++ b/frontend/src/bundles/common/components/sidebar/sidebar.tsx @@ -5,17 +5,18 @@ import { PeopleRounded, } from '@mui/icons-material'; -import { Grid, Link, Logo } from '~/bundles/common/components/components.js'; +import { Grid, Logo } from '~/bundles/common/components/components.js'; import { AppRoute } from '~/bundles/common/enums/enums.js'; import { UserRole } from '~/bundles/users/users.js'; import { type RootReducer } from '~/framework/store/store.package.js'; import { getValidClassNames } from '../../helpers/helpers.js'; import { useAppSelector, useCallback, useState } from '../../hooks/hooks.js'; +import { SidebarItem } from './sidebar-item/sidebar-item.js'; import styles from './styles.module.scss'; import { type SideBarMenu } from './types/sidebar-menu.type.js'; -const GENERAL_MENU: SideBarMenu = [ +const GENERAL_MENU_ITEMS: SideBarMenu = [ { link: AppRoute.CANDIDATES, name: 'Candidates', @@ -28,7 +29,7 @@ const GENERAL_MENU: SideBarMenu = [ }, ]; -const ADMIN_MENU: SideBarMenu = [ +const ADMIN_MENU_ITEMS: SideBarMenu = [ { link: AppRoute.ADMIN_VERIFICATIONS_PANEL, name: 'Home', @@ -46,17 +47,13 @@ const Sidebar: React.FC = () => { const currentUser = useAppSelector( (state: RootReducer) => state.auth.currentUser, ); - const { isApproved } = useAppSelector((state: RootReducer) => - currentUser?.role == UserRole.TALENT - ? state.talentOnBoarding - : state.employerOnBoarding, - ); - const isAdmin = currentUser?.role === 'admin'; + + const isAdmin = currentUser?.role === UserRole.ADMIN; const handleToggleSidebar = useCallback(() => { setSidebarVisible(!isSidebarVisible); }, [isSidebarVisible]); - const menuItems = isAdmin ? ADMIN_MENU : GENERAL_MENU; + const menuItems = isAdmin ? ADMIN_MENU_ITEMS : GENERAL_MENU_ITEMS; return ( <> @@ -69,23 +66,12 @@ const Sidebar: React.FC = () => {
    {menuItems.map((item) => ( -
  • - - {item.icon} -

    {item.name}

    - -
  • + icon={item.icon} + link={item.link} + name={item.name} + /> ))}
diff --git a/frontend/src/bundles/common/components/sidebar/styles.module.scss b/frontend/src/bundles/common/components/sidebar/styles.module.scss index 9160c196e..fc3e63791 100644 --- a/frontend/src/bundles/common/components/sidebar/styles.module.scss +++ b/frontend/src/bundles/common/components/sidebar/styles.module.scss @@ -30,6 +30,15 @@ list-style: none; } +.listButton { + position: absolute; + width: 100%; + height: 100%; + background-color: transparent; + border: none; + outline: none; +} + a { display: flex; flex-direction: column; @@ -50,6 +59,9 @@ a { } .listItem { + position: relative; + width: 100%; + & a[aria-current="page"] { color: var(--slate-gray); cursor: default; diff --git a/frontend/src/bundles/common/enums/enums.ts b/frontend/src/bundles/common/enums/enums.ts index ed76ce0b3..49dbba9c5 100644 --- a/frontend/src/bundles/common/enums/enums.ts +++ b/frontend/src/bundles/common/enums/enums.ts @@ -14,4 +14,5 @@ export { HttpCode, ServerErrorType, UserDetailsApiPath, + UserRole, } from 'shared/build/index.js'; diff --git a/frontend/src/bundles/common/pages/home.tsx b/frontend/src/bundles/common/pages/home.tsx index 64cf59c95..7b74da87b 100644 --- a/frontend/src/bundles/common/pages/home.tsx +++ b/frontend/src/bundles/common/pages/home.tsx @@ -4,7 +4,7 @@ import { type RootReducer } from '~/framework/store/store.package.js'; import { Navigate } from '../components/components.js'; import { AppRoute } from '../enums/app-route.enum.js'; import { configureString } from '../helpers/helpers.js'; -import { useAppSelector } from '../hooks/hooks.js'; +import { useAppSelector, useEffect, useNavigate } from '../hooks/hooks.js'; import { UserRole } from '../types/types.js'; const Home: React.FC = () => { @@ -16,14 +16,18 @@ const Home: React.FC = () => { ? state.talentOnBoarding : state.employerOnBoarding, ); + const navigate = useNavigate(); + useEffect(() => { + if (isApproved) { + navigate(AppRoute.CHATS); + } + }, [isApproved, navigate]); + switch (role) { case UserRole.ADMIN: { return ; } case UserRole.TALENT: { - if (isApproved) { - return ; - } return ( { ); } case UserRole.EMPLOYER: { - if (isApproved) { - return ; - } - return ; + return ; } default: { - return ; + return ; } } }; diff --git a/frontend/src/bundles/employer-onboarding/components/onboarding-form/components/employer-file-upload.tsx b/frontend/src/bundles/employer-onboarding/components/onboarding-form/components/employer-file-upload.tsx index 6ba676582..b18ada68f 100644 --- a/frontend/src/bundles/employer-onboarding/components/onboarding-form/components/employer-file-upload.tsx +++ b/frontend/src/bundles/employer-onboarding/components/onboarding-form/components/employer-file-upload.tsx @@ -60,7 +60,6 @@ const EmployerFileUpload: React.FC = ({ setError, clearErrors, }); - field.onChange(file); return; } catch { diff --git a/frontend/src/bundles/employer-onboarding/components/onboarding-form/onboarding-form.tsx b/frontend/src/bundles/employer-onboarding/components/onboarding-form/onboarding-form.tsx index a945b04aa..ae8596704 100644 --- a/frontend/src/bundles/employer-onboarding/components/onboarding-form/onboarding-form.tsx +++ b/frontend/src/bundles/employer-onboarding/components/onboarding-form/onboarding-form.tsx @@ -40,6 +40,16 @@ const getEmployerOnBoardingState = ( state: RootReducer, ): UserDetailsGeneralCustom => state.employerOnBoarding; +const getImageSource = ( + file?: File | null, + url?: string | null, +): string | null => { + if (file) { + return URL.createObjectURL(file); + } + return url ?? null; +}; + const OnboardingForm: React.FC = () => { const { setSubmitForm } = useFormSubmit(); const { @@ -52,6 +62,8 @@ const OnboardingForm: React.FC = () => { description, companyLogo, linkedinLink, + photoUrl, + companyLogoUrl, } = useAppSelector((rootState) => getEmployerOnBoardingState(rootState)); const hasChangesInDetails = useAppSelector( @@ -78,9 +90,12 @@ const OnboardingForm: React.FC = () => { description, companyLogo, linkedinLink, + photoUrl, + companyLogoUrl, }), [ companyLogo, + companyLogoUrl, companyName, companyWebsite, description, @@ -89,6 +104,7 @@ const OnboardingForm: React.FC = () => { linkedinLink, location, photo, + photoUrl, ], ), validationSchema: EmployerOnboardingValidationSchema, @@ -96,13 +112,15 @@ const OnboardingForm: React.FC = () => { useEffect(() => { reset({ photo, + companyLogo, fullName, employerPosition, companyName, companyWebsite, location, description, - companyLogo, + photoUrl, + companyLogoUrl, linkedinLink, }); }, [ @@ -116,11 +134,14 @@ const OnboardingForm: React.FC = () => { description, companyLogo, reset, + companyLogoUrl, + photoUrl, ]); const dispatch = useAppDispatch(); const watchedValues = watch([ 'photo', + 'photoUrl', 'fullName', 'employerPosition', 'companyName', @@ -128,12 +149,14 @@ const OnboardingForm: React.FC = () => { 'location', 'description', 'companyLogo', + 'companyLogoUrl', 'linkedinLink', ]); useEffect(() => { const newValues = getValues([ 'photo', + 'photoUrl', 'fullName', 'employerPosition', 'companyName', @@ -141,17 +164,18 @@ const OnboardingForm: React.FC = () => { 'location', 'description', 'companyLogo', + 'companyLogoUrl', 'linkedinLink', ]); const initialValues = { - photo, + photoUrl, fullName, employerPosition, companyName, companyWebsite, location, description, - companyLogo, + companyLogoUrl, linkedinLink, }; const hasChanges = @@ -174,30 +198,17 @@ const OnboardingForm: React.FC = () => { hasChangesInDetails, photo, companyLogo, + photoUrl, + companyLogoUrl, ]); const { currentUser } = useAppSelector((state: RootReducer) => state.auth); const handleFormSubmit = useCallback( (data: EmployerOnboardingDto): boolean => { - const { - fullName, - employerPosition, - companyName, - companyWebsite, - location, - description, - linkedinLink, - } = data; void dispatch( employerActions.saveEmployerDetails({ - fullName, - employerPosition, - companyName, - companyWebsite, - location, - description, - linkedinLink, + ...data, userId: currentUser?.id, }), ); @@ -221,6 +232,20 @@ const OnboardingForm: React.FC = () => { }; }, [handleSubmit, handleFormSubmit, setSubmitForm]); + const ImageDisplay = ({ + file, + url, + alt, + }: { + file?: File | null; + url?: string | null; + alt: string; + }): JSX.Element | null => { + const source = getImageSource(file, url); + return source ? ( + {alt} + ) : null; + }; return ( @@ -335,17 +360,12 @@ const OnboardingForm: React.FC = () => { - {errors.photo ?? !watch('photo') ? null : ( - Profile - )} + - { - {errors.companyLogo ?? - !watch('companyLogo') ? null : ( - Company logo - )} + - (); } diff --git a/frontend/src/bundles/employer-onboarding/enums/onboarding-form/onboarding-form.validation-rule.ts b/frontend/src/bundles/employer-onboarding/enums/onboarding-form/onboarding-form.validation-rule.ts index db418a180..f7d342f07 100644 --- a/frontend/src/bundles/employer-onboarding/enums/onboarding-form/onboarding-form.validation-rule.ts +++ b/frontend/src/bundles/employer-onboarding/enums/onboarding-form/onboarding-form.validation-rule.ts @@ -6,7 +6,7 @@ const EmployerOnboardingValidationRule = { MIN_DESCRIPTION_LENGTH: 100, MAX_DESCRIPTION_LENGTH: 2500, MIN_LENGTH_COMPANY_WEBSITE: 5, - MAX_LENGTH_COMPANY_WEBSITE: 50, + MAX_LENGTH_COMPANY_WEBSITE: 250, MIN_POSITION_LENGTH: 2, MAX_POSITION_LENGTH: 50, MIN_COMPANY_NAME_LENGTH: 2, diff --git a/frontend/src/bundles/employer-onboarding/helpers/helpers.ts b/frontend/src/bundles/employer-onboarding/helpers/helpers.ts index fb43f85e8..831083413 100644 --- a/frontend/src/bundles/employer-onboarding/helpers/helpers.ts +++ b/frontend/src/bundles/employer-onboarding/helpers/helpers.ts @@ -1 +1,2 @@ export { fileSizeValidator } from './file-size-validator.js'; +export { mapFilesToPayload } from './map-files-to-payload.js'; diff --git a/frontend/src/bundles/employer-onboarding/helpers/map-files-to-payload.ts b/frontend/src/bundles/employer-onboarding/helpers/map-files-to-payload.ts new file mode 100644 index 000000000..37579582d --- /dev/null +++ b/frontend/src/bundles/employer-onboarding/helpers/map-files-to-payload.ts @@ -0,0 +1,31 @@ +import { type FileUploadResponse } from 'shared/build/index.js'; + +import { type UserDetailsGeneralCustom } from '../types/types.js'; + +const mapFilesToPayload = ({ + payload, + files, +}: { + payload: UserDetailsGeneralCustom; + files: FileUploadResponse; +}): UserDetailsGeneralCustom => { + if (files.companyLogo) { + payload.companyLogoId = files.companyLogo.id; + } + + if (files.employerPhoto) { + payload.photoId = files.employerPhoto.id; + } + + if (files.cv) { + payload.cvId = files.cv.id; + } + + if (files.talentPhoto) { + payload.photoId = files.talentPhoto.id; + } + + return payload; +}; + +export { mapFilesToPayload }; diff --git a/frontend/src/bundles/employer-onboarding/pages/onboarding/onboarding.tsx b/frontend/src/bundles/employer-onboarding/pages/onboarding/onboarding.tsx index 8226fb9cc..8fcc809fe 100644 --- a/frontend/src/bundles/employer-onboarding/pages/onboarding/onboarding.tsx +++ b/frontend/src/bundles/employer-onboarding/pages/onboarding/onboarding.tsx @@ -7,15 +7,7 @@ import { import { useFormSubmit } from '~/bundles/common/context/context.js'; import { AppRoute } from '~/bundles/common/enums/app-route.enum.js'; import { getValidClassNames } from '~/bundles/common/helpers/helpers.js'; -import { - useAppDispatch, - useAppSelector, - useCallback, - useEffect, - useNavigate, -} from '~/bundles/common/hooks/hooks.js'; -import { actions } from '~/bundles/employer-onboarding/store/employer-onboarding.js'; -import { type RootReducer } from '~/framework/store/store.js'; +import { useCallback, useNavigate } from '~/bundles/common/hooks/hooks.js'; import { OnboardingForm } from '../../components/onboarding-form/onboarding-form.js'; import styles from './styles.module.scss'; @@ -23,17 +15,7 @@ import styles from './styles.module.scss'; const Onboarding: React.FC = () => { const { submitForm } = useFormSubmit(); - const dispatch = useAppDispatch(); const navigate = useNavigate(); - const { currentUser } = useAppSelector((state: RootReducer) => state.auth); - - useEffect(() => { - void dispatch( - actions.getEmployerDetails({ - userId: currentUser?.id, - }), - ); - }, [currentUser?.id, dispatch]); const handleFormSubmit = useCallback((): void => { if (submitForm) { diff --git a/frontend/src/bundles/employer-onboarding/store/actions.ts b/frontend/src/bundles/employer-onboarding/store/actions.ts index cb9868eb0..c468bd011 100644 --- a/frontend/src/bundles/employer-onboarding/store/actions.ts +++ b/frontend/src/bundles/employer-onboarding/store/actions.ts @@ -1,7 +1,10 @@ import { createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit'; import { type AsyncThunkConfig } from '~/bundles/common/types/types.js'; +import { type FileDto } from '~/bundles/file-upload/types/file-dto.type.js'; +import { EMPTY_FILE_COUNT } from '~/bundles/talent-onboarding/constants/constants.js'; +import { mapFilesToPayload } from '../helpers/map-files-to-payload.js'; import { type UserDetailsGeneralCustom } from '../types/types.js'; import { name as sliceName } from './slice.js'; @@ -50,17 +53,50 @@ const saveEmployerDetails = createAsyncThunk< AsyncThunkConfig >( `${sliceName}/save-employer-details`, - async (registerPayload, { dispatch, rejectWithValue }) => { + async (registerPayload, { dispatch, rejectWithValue, extra }) => { + const { fileUploadApi } = extra; + try { + const { photo, companyLogo, ...restPayload } = registerPayload; + delete restPayload.photoUrl; + delete restPayload.companyLogoUrl; + const files: FileDto[] = []; + + if (photo) { + const [extension] = photo.name.split('.').reverse(); + files.push({ + role: 'employerPhoto', + extension, + file: photo, + }); + } + + if (companyLogo) { + const [extension] = companyLogo.name.split('.').reverse(); + files.push({ + role: 'companyLogo', + extension, + file: companyLogo, + }); + } + + if (files.length > EMPTY_FILE_COUNT) { + const response = await fileUploadApi.upload({ files }); + mapFilesToPayload({ + payload: restPayload, + files: response, + }); + } + const userDetails = (await dispatch( - getEmployerDetails(registerPayload), + getEmployerDetails(restPayload), )) as unknown as PayloadAction; const result = userDetails.payload ? ((await dispatch( - updateEmployerDetails(registerPayload), + updateEmployerDetails(restPayload), )) as PayloadAction) : ((await dispatch( - createEmployerDetails(registerPayload), + createEmployerDetails(restPayload), )) as PayloadAction); return result.payload; @@ -80,15 +116,24 @@ const getEmployerDetails = createAsyncThunk< >( `${sliceName}/get-employer-details`, async (findPayload, { extra, rejectWithValue }) => { - const { employerOnBoardingApi } = extra; + const { employerOnBoardingApi, fileUploadApi } = extra; try { const userDetails = await employerOnBoardingApi.getUserDetailsByUserId({ userId: findPayload.userId, }); - - return userDetails ?? null; + const photo = await fileUploadApi.getFileById({ + id: userDetails?.photoId ?? '', + }); + const companyLogo = await fileUploadApi.getFileById({ + id: userDetails?.companyLogoId ?? '', + }); + return { + ...userDetails, + photoUrl: photo?.url, + companyLogoUrl: companyLogo?.url, + }; } catch (error) { rejectWithValue({ _type: 'rejected', diff --git a/frontend/src/bundles/employer-onboarding/store/slice.ts b/frontend/src/bundles/employer-onboarding/store/slice.ts index eec922c29..eb15b8ca0 100644 --- a/frontend/src/bundles/employer-onboarding/store/slice.ts +++ b/frontend/src/bundles/employer-onboarding/store/slice.ts @@ -14,6 +14,9 @@ import { const initialState: UserDetailsGeneralCustom = { ...DEFAULT_EMPLOYER_REGISTRATION_FORM_PAYLOAD, dataStatus: DataStatus.IDLE, + cvUrl: null, + photoUrl: null, + companyLogoUrl: null, }; const { reducer, actions, name } = createSlice({ diff --git a/frontend/src/bundles/employer-onboarding/types/general-update-user-details-request-dto/general-update-user-details-request-dto.ts b/frontend/src/bundles/employer-onboarding/types/general-update-user-details-request-dto/general-update-user-details-request-dto.ts index 05872cb34..d3de69441 100644 --- a/frontend/src/bundles/employer-onboarding/types/general-update-user-details-request-dto/general-update-user-details-request-dto.ts +++ b/frontend/src/bundles/employer-onboarding/types/general-update-user-details-request-dto/general-update-user-details-request-dto.ts @@ -10,6 +10,10 @@ type UserDetailsGeneralCustom = UserDetailsUpdateRequestDto & { companyLogo?: File | null; dataStatus?: ValueOf; createdAt?: string; + publishedAt?: string; + cvUrl?: string | null; + companyLogoUrl?: string | null; + photoUrl?: string | null; }; export { type UserDetailsGeneralCustom }; diff --git a/frontend/src/bundles/employer-onboarding/types/onboarding-form/onboarding-dto.ts b/frontend/src/bundles/employer-onboarding/types/onboarding-form/onboarding-dto.ts index 6e79fc63c..540ae3348 100644 --- a/frontend/src/bundles/employer-onboarding/types/onboarding-form/onboarding-dto.ts +++ b/frontend/src/bundles/employer-onboarding/types/onboarding-form/onboarding-dto.ts @@ -1,6 +1,8 @@ type EmployerOnboardingDto = { photo: File | null; companyLogo: File | null; + photoUrl?: string | null; + companyLogoUrl?: string | null; fullName: string; employerPosition: string; companyName: string; diff --git a/frontend/src/bundles/employer-onboarding/validation-schemas/onboarding-form/onboarding-form.validation-schema.ts b/frontend/src/bundles/employer-onboarding/validation-schemas/onboarding-form/onboarding-form.validation-schema.ts index 929660308..c51a9b1f7 100644 --- a/frontend/src/bundles/employer-onboarding/validation-schemas/onboarding-form/onboarding-form.validation-schema.ts +++ b/frontend/src/bundles/employer-onboarding/validation-schemas/onboarding-form/onboarding-form.validation-schema.ts @@ -24,6 +24,15 @@ const EmployerOnboardingValidationSchema = joi.object< 'any.invalid': EmployerOnboardingValidationMessage.PHOTO_MAX_SIZE, }), + companyLogoUrl: joi.string().allow(null).messages({ + 'any.invalid': + EmployerOnboardingValidationMessage.COMPANY_LOGO_MAX_SIZE, + }), + + photoUrl: joi.string().allow(null).messages({ + 'any.invalid': EmployerOnboardingValidationMessage.PHOTO_MAX_SIZE, + }), + fullName: joi .string() .trim() diff --git a/frontend/src/bundles/file-upload/file-upload-api.ts b/frontend/src/bundles/file-upload/file-upload-api.ts index 7d8fc8471..b2b110013 100644 --- a/frontend/src/bundles/file-upload/file-upload-api.ts +++ b/frontend/src/bundles/file-upload/file-upload-api.ts @@ -4,7 +4,12 @@ import { type Http } from '~/framework/http/http.js'; import { type Storage } from '~/framework/storage/storage.js'; import { FileApiPath } from './enums/enums.js'; -import { type FileUploadResponse } from './types/types.js'; +import { + type FileDto, + type FileUploadResponse, + type GetFileRequestDto, + type GetFileResponseDto, +} from './types/types.js'; type Constructor = { baseUrl: string; @@ -18,11 +23,13 @@ class FileUploadApi extends HttpApiBase { } public async upload(payload: { - files: File[]; + files: FileDto[]; }): Promise { const formData = new FormData(); - for (const file of payload.files) { - formData.append('files', file); + for (const fileData of payload.files) { + const { role, extension, file } = fileData; + const newFileName = `${role}.${extension}`; + formData.append('files', file, newFileName); } const response = await this.load( @@ -37,6 +44,22 @@ class FileUploadApi extends HttpApiBase { return response.json(); } + + public async getFileById( + payload: GetFileRequestDto, + ): Promise { + const { id } = payload; + + const response = await this.load( + this.getFullEndpoint('/', ':id', { id }), + { + method: 'GET', + contentType: ContentType.JSON, + hasAuth: true, + }, + ); + return response.json(); + } } export { FileUploadApi }; diff --git a/frontend/src/bundles/file-upload/types/file-dto.type.ts b/frontend/src/bundles/file-upload/types/file-dto.type.ts new file mode 100644 index 000000000..64e1ec3a2 --- /dev/null +++ b/frontend/src/bundles/file-upload/types/file-dto.type.ts @@ -0,0 +1,7 @@ +type FileDto = { + role: string; + extension: string; + file: File; +}; + +export { type FileDto }; diff --git a/frontend/src/bundles/file-upload/types/types.ts b/frontend/src/bundles/file-upload/types/types.ts index 4021ea0a8..b255b9541 100644 --- a/frontend/src/bundles/file-upload/types/types.ts +++ b/frontend/src/bundles/file-upload/types/types.ts @@ -1 +1,6 @@ -export { type FileUploadResponse } from 'shared/build/index.js'; +export { type FileDto } from './file-dto.type.js'; +export { + type FileUploadResponse, + type GetFileRequestDto, + type GetFileResponseDto, +} from 'shared/build/index.js'; diff --git a/frontend/src/bundles/admin-panel/admin-api.ts b/frontend/src/bundles/hiring-info/hiring-info-api.ts similarity index 66% rename from frontend/src/bundles/admin-panel/admin-api.ts rename to frontend/src/bundles/hiring-info/hiring-info-api.ts index 50c631729..6fb1600a4 100644 --- a/frontend/src/bundles/admin-panel/admin-api.ts +++ b/frontend/src/bundles/hiring-info/hiring-info-api.ts @@ -10,6 +10,7 @@ import { type Storage } from '~/framework/storage/storage.js'; import { type HiringInfoCreateRequestDto, type HiringInfoFindAllRequestDto, + type HiringInfoFindRequestDto, type HiringInfoResponseDto, } from './types/types.js'; @@ -19,14 +20,14 @@ type Constructor = { storage: Storage; }; -class AdminApi extends HttpApiBase { +class HiringInfoApi extends HttpApiBase { public constructor({ baseUrl, http, storage }: Constructor) { super({ path: ApiPath.HIRING_INFO, baseUrl, http, storage }); } public async getAllHiringInfo(): Promise { const response = await this.load( - this.getFullEndpoint(HiringInfoApiPath.ROOT, {}), + this.getFullEndpoint(HiringInfoApiPath.ALL, {}), { method: 'GET', contentType: ContentType.JSON, @@ -36,6 +37,26 @@ class AdminApi extends HttpApiBase { return response.json(); } + public async getHiringInfo( + payload: HiringInfoFindRequestDto, + ): Promise { + const queryParameters = Object.keys(payload).map((key) => `?${key}`); + + const response = await this.load( + this.getFullEndpoint( + HiringInfoApiPath.ROOT, + ...queryParameters, + payload, + ), + { + method: 'GET', + contentType: ContentType.JSON, + hasAuth: true, + }, + ); + return response.json(); + } + public async createHiringInfo( payload: HiringInfoCreateRequestDto, ): Promise { @@ -52,4 +73,4 @@ class AdminApi extends HttpApiBase { } } -export { AdminApi }; +export { HiringInfoApi }; diff --git a/frontend/src/bundles/hiring-info/hiring-info.ts b/frontend/src/bundles/hiring-info/hiring-info.ts new file mode 100644 index 000000000..13b11d60f --- /dev/null +++ b/frontend/src/bundles/hiring-info/hiring-info.ts @@ -0,0 +1,13 @@ +import { config } from '~/framework/config/config.js'; +import { http } from '~/framework/http/http.js'; +import { storage } from '~/framework/storage/storage.js'; + +import { HiringInfoApi } from './hiring-info-api.js'; + +const hiringInfoApi = new HiringInfoApi({ + baseUrl: config.ENV.API.ORIGIN_URL, + storage, + http, +}); + +export { hiringInfoApi }; diff --git a/frontend/src/bundles/hiring-info/store/actions.ts b/frontend/src/bundles/hiring-info/store/actions.ts new file mode 100644 index 000000000..3df7f8e0c --- /dev/null +++ b/frontend/src/bundles/hiring-info/store/actions.ts @@ -0,0 +1,44 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +import { type AsyncThunkConfig } from '~/bundles/common/types/types.js'; + +import { + type HiringInfoCreateRequestDto, + type HiringInfoFindRequestDto, + type HiringInfoFindResponseDto, + type HiringInfoResponseDto, +} from '../types/types.js'; +import { name as sliceName } from './slice.js'; + +const getAllHiringInfo = createAsyncThunk< + HiringInfoFindResponseDto[], + undefined, + AsyncThunkConfig +>(`${sliceName}/get-all-hiring-info`, async (_, { extra }) => { + const { hiringInfoApi } = extra; + + const { items } = await hiringInfoApi.getAllHiringInfo(); + + return items; +}); + +const getHiringInfo = createAsyncThunk< + boolean, + HiringInfoFindRequestDto, + AsyncThunkConfig +>(`${sliceName}/get-hiring-info`, async (findPayload, { extra }) => { + const { hiringInfoApi } = extra; + + return await hiringInfoApi.getHiringInfo(findPayload); +}); + +const submitHiringInfo = createAsyncThunk< + HiringInfoResponseDto, + HiringInfoCreateRequestDto, + AsyncThunkConfig +>(`${sliceName}/create-hiring-info`, async (createPayload, { extra }) => { + const { hiringInfoApi } = extra; + return await hiringInfoApi.createHiringInfo(createPayload); +}); + +export { getAllHiringInfo, getHiringInfo, submitHiringInfo }; diff --git a/frontend/src/bundles/admin-panel/store/admin.ts b/frontend/src/bundles/hiring-info/store/hiring-info.ts similarity index 56% rename from frontend/src/bundles/admin-panel/store/admin.ts rename to frontend/src/bundles/hiring-info/store/hiring-info.ts index 5d8ad2384..97b6a5f1e 100644 --- a/frontend/src/bundles/admin-panel/store/admin.ts +++ b/frontend/src/bundles/hiring-info/store/hiring-info.ts @@ -1,10 +1,15 @@ -import { approveHiringInfo, getAllHiringInfo } from './actions.js'; +import { + getAllHiringInfo, + getHiringInfo, + submitHiringInfo, +} from './actions.js'; import { actions } from './slice.js'; const allActions = { ...actions, - approveHiringInfo, + submitHiringInfo, getAllHiringInfo, + getHiringInfo, }; export { allActions as actions }; diff --git a/frontend/src/bundles/admin-panel/store/slice.ts b/frontend/src/bundles/hiring-info/store/slice.ts similarity index 81% rename from frontend/src/bundles/admin-panel/store/slice.ts rename to frontend/src/bundles/hiring-info/store/slice.ts index cd534ef5e..1bd1efb77 100644 --- a/frontend/src/bundles/admin-panel/store/slice.ts +++ b/frontend/src/bundles/hiring-info/store/slice.ts @@ -1,10 +1,10 @@ import { createSlice } from '@reduxjs/toolkit'; -import { type HiringInfoFindRequestDto } from '../types/types.js'; +import { type HiringInfoFindResponseDto } from '../types/types.js'; import { getAllHiringInfo } from './actions.js'; type State = { - hiringInfo: HiringInfoFindRequestDto[]; + hiringInfo: HiringInfoFindResponseDto[]; }; const initialState: State = { diff --git a/frontend/src/bundles/hiring-info/types/types.ts b/frontend/src/bundles/hiring-info/types/types.ts new file mode 100644 index 000000000..3e79b6e7c --- /dev/null +++ b/frontend/src/bundles/hiring-info/types/types.ts @@ -0,0 +1,7 @@ +export { + type HiringInfoCreateRequestDto, + type HiringInfoFindAllRequestDto, + type HiringInfoFindRequestDto, + type HiringInfoFindResponseDto, + type HiringInfoResponseDto, +} from 'shared/build/index.js'; diff --git a/frontend/src/bundles/profile-cabinet/pages/profile-cabinet.tsx b/frontend/src/bundles/profile-cabinet/pages/profile-cabinet.tsx index 41ccb7d83..5a9afff6e 100644 --- a/frontend/src/bundles/profile-cabinet/pages/profile-cabinet.tsx +++ b/frontend/src/bundles/profile-cabinet/pages/profile-cabinet.tsx @@ -19,9 +19,10 @@ import { useAppSelector, useCallback, useEffect, + useNavigate, + useState, } from '~/bundles/common/hooks/hooks.js'; import { OnboardingForm } from '~/bundles/employer-onboarding/components/onboarding-form/onboarding-form.js'; -import { actions as employerActions } from '~/bundles/employer-onboarding/store/employer-onboarding.js'; import { StepsRoute } from '~/bundles/talent-onboarding/enums/enums.js'; import { actions as talentActions } from '~/bundles/talent-onboarding/store/talent-onboarding.js'; import { type RootReducer } from '~/framework/store/store.js'; @@ -66,37 +67,50 @@ const ProfileCabinet: React.FC = () => { break; } } + const [isWaitingForApproval, setIsWaitingForApproval] = + useState(false); + + const navigate = useNavigate(); + const { submitForm } = useFormSubmit(); + const dispatch = useAppDispatch(); + const { hasChanges } = useAppSelector((state: RootReducer) => ({ hasChanges: state.cabinet.hasChangesInDetails, })); + const currentUser = useAppSelector( (rootState) => getAuthState(rootState).currentUser, ); + + const { talentOnBoarding, employerOnBoarding } = useAppSelector( + (state: RootReducer) => state, + ); + useEffect(() => { - switch (role) { - case UserRole.TALENT: { - void dispatch( - talentActions.getTalentDetails({ - userId: currentUser?.id, - }), - ); - break; - } - case UserRole.EMPLOYER: { - void dispatch( - employerActions.getEmployerDetails({ - userId: currentUser?.id, - }), - ); - break; - } - default: { - break; - } + if ( + (talentOnBoarding.publishedAt && !talentOnBoarding.isApproved) ?? + (employerOnBoarding.publishedAt && !employerOnBoarding.isApproved) + ) { + setIsWaitingForApproval(true); } - }, [currentUser?.id, dispatch, role]); + if (talentOnBoarding.isApproved ?? employerOnBoarding.isApproved) { + setIsWaitingForApproval(false); + void dispatch( + storeActions.notify({ + type: NotificationType.SUCCESS, + message: 'Profile was approved', + }), + ); + } + }, [ + dispatch, + employerOnBoarding.isApproved, + employerOnBoarding.publishedAt, + talentOnBoarding.isApproved, + talentOnBoarding.publishedAt, + ]); const handleSaveClick = useCallback(() => { void (async (): Promise => { @@ -114,8 +128,26 @@ const ProfileCabinet: React.FC = () => { })(); }, [dispatch, submitForm]); + const handlePublishNowClick = useCallback(() => { + if (currentUser) { + void dispatch( + talentActions.updateTalentPublishedDate({ + userId: currentUser.id, + }), + ); + } + + if (role === UserRole.TALENT) { + navigate(`/${role}/onboarding/step/${StepsRoute.STEP_05}`); + } + }, [currentUser, dispatch, navigate, role]); + return ( - + Your Profile @@ -137,13 +169,35 @@ const ProfileCabinet: React.FC = () => { ) : ( )} - )} - + {icon}

{name}

diff --git a/frontend/src/bundles/common/components/sidebar/styles.module.scss b/frontend/src/bundles/common/components/sidebar/styles.module.scss index fc3e63791..71e312521 100644 --- a/frontend/src/bundles/common/components/sidebar/styles.module.scss +++ b/frontend/src/bundles/common/components/sidebar/styles.module.scss @@ -39,7 +39,7 @@ outline: none; } -a { +.link { display: flex; flex-direction: column; align-items: center; @@ -61,11 +61,6 @@ a { .listItem { position: relative; width: 100%; - - & a[aria-current="page"] { - color: var(--slate-gray); - cursor: default; - } } // BURGER From bd41db3eeb12b02a53f8901e98468c3657c9e5da Mon Sep 17 00:00:00 2001 From: mchornomaz Date: Fri, 29 Sep 2023 11:11:57 +0300 Subject: [PATCH 24/75] bt-695: * fixed employer profile page layout --- .../components/onboarding-form/styles.module.scss | 6 ++++++ .../bundles/profile-cabinet/pages/styles.module.scss | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/frontend/src/bundles/employer-onboarding/components/onboarding-form/styles.module.scss b/frontend/src/bundles/employer-onboarding/components/onboarding-form/styles.module.scss index 7baf1c077..45afced02 100644 --- a/frontend/src/bundles/employer-onboarding/components/onboarding-form/styles.module.scss +++ b/frontend/src/bundles/employer-onboarding/components/onboarding-form/styles.module.scss @@ -220,3 +220,9 @@ } } } + +@media screen and (width <=760px) { + .formWrapper { + width: 100%; + } +} diff --git a/frontend/src/bundles/profile-cabinet/pages/styles.module.scss b/frontend/src/bundles/profile-cabinet/pages/styles.module.scss index 24308e980..59315cb5c 100644 --- a/frontend/src/bundles/profile-cabinet/pages/styles.module.scss +++ b/frontend/src/bundles/profile-cabinet/pages/styles.module.scss @@ -95,4 +95,15 @@ flex-direction: column; align-content: center; align-items: center; + width: 65vw; +} + +@media screen and (width <=760px) { + .stepContainer { + display: flex; + flex-direction: column; + align-content: center; + align-items: center; + width: 100%; + } } From a5a858189e4d3234c09d4a2af06091693a409391 Mon Sep 17 00:00:00 2001 From: dobroillya Date: Fri, 29 Sep 2023 11:12:06 +0300 Subject: [PATCH 25/75] bt-716: * types --- mobile/src/bundles/common/types/types.ts | 2 +- .../user-details-general-dto.type.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mobile/src/bundles/common/types/types.ts b/mobile/src/bundles/common/types/types.ts index e79914d44..ac2bf7ef8 100644 --- a/mobile/src/bundles/common/types/types.ts +++ b/mobile/src/bundles/common/types/types.ts @@ -1,5 +1,4 @@ export { type AsyncThunkConfig } from './app/app'; -export { type FileUploadResponse } from './general-update-user-details-request-dto/'; export { type UserDetailsGeneralCustom } from './general-update-user-details-request-dto/general-update-user-details-request-dto.js'; export { type AuthNavigationParameterList, @@ -18,6 +17,7 @@ export { type TalentOnboardingNavigationParameterList, type TalentOnboardingRouteProperties, } from './navigation/navigation'; +export { type FileUploadResponse } from './user-details-general/user-details-general-dto.type'; export { type UserDetailsGeneralCreateRequestDto, type UserDetailsGeneralRequestDto, diff --git a/mobile/src/bundles/common/types/user-details-general/user-details-general-dto.type.ts b/mobile/src/bundles/common/types/user-details-general/user-details-general-dto.type.ts index cd6d8c295..3ce049507 100644 --- a/mobile/src/bundles/common/types/user-details-general/user-details-general-dto.type.ts +++ b/mobile/src/bundles/common/types/user-details-general/user-details-general-dto.type.ts @@ -53,6 +53,12 @@ type UserDetailsGeneralResponseDto = UserDetailsResponseDto & Partial & Partial; +type FileUploadResponse = { + [FileRole.COMPANY_LOGO]?: UploadedFile; + [FileRole.CV]?: UploadedFile; + [FileRole.PHOTO]?: UploadedFile; +}; + const FileRole = { COMPANY_LOGO: 'companyLogo', PHOTO: 'rn', @@ -60,12 +66,6 @@ const FileRole = { } as const; export { FileRole }; - -type FileUploadResponse = { - [FileRole.COMPANY_LOGO]?: UploadedFile; - [FileRole.CV]?: UploadedFile; - [FileRole.PHOTO]?: UploadedFile; -}; export { type CVDto, type FileUploadResponse, From d76846704f41e1a41c8afb6367e3f5fe6d4aac7f Mon Sep 17 00:00:00 2001 From: dobroillya Date: Fri, 29 Sep 2023 11:37:14 +0300 Subject: [PATCH 26/75] bt-716: * types move --- mobile/src/bundles/common/enums/enums.ts | 1 + mobile/src/bundles/common/enums/file-role/file-role.ts | 7 +++++++ .../user-details-general/user-details-general-dto.type.ts | 8 +------- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 mobile/src/bundles/common/enums/file-role/file-role.ts diff --git a/mobile/src/bundles/common/enums/enums.ts b/mobile/src/bundles/common/enums/enums.ts index 6b1eeb308..aa889dd71 100644 --- a/mobile/src/bundles/common/enums/enums.ts +++ b/mobile/src/bundles/common/enums/enums.ts @@ -1,4 +1,5 @@ export { DataStatus, TalentOnboardingStepState } from './app/app'; +export { FileRole } from './file-role/file-role'; export { AuthScreenName, CompletedTalentOnboardingStep, diff --git a/mobile/src/bundles/common/enums/file-role/file-role.ts b/mobile/src/bundles/common/enums/file-role/file-role.ts new file mode 100644 index 000000000..4243994fe --- /dev/null +++ b/mobile/src/bundles/common/enums/file-role/file-role.ts @@ -0,0 +1,7 @@ +const FileRole = { + COMPANY_LOGO: 'companyLogo', + PHOTO: 'rn', + CV: 'cv', +} as const; + +export { FileRole }; diff --git a/mobile/src/bundles/common/types/user-details-general/user-details-general-dto.type.ts b/mobile/src/bundles/common/types/user-details-general/user-details-general-dto.type.ts index 3ce049507..a3747202d 100644 --- a/mobile/src/bundles/common/types/user-details-general/user-details-general-dto.type.ts +++ b/mobile/src/bundles/common/types/user-details-general/user-details-general-dto.type.ts @@ -1,5 +1,6 @@ import { type UploadedFile } from 'shared/build/index'; +import { type FileRole } from '~/bundles/common/enums/enums'; import { type UserDetailsCreateRequestDto, type UserDetailsResponseDto, @@ -59,13 +60,6 @@ type FileUploadResponse = { [FileRole.PHOTO]?: UploadedFile; }; -const FileRole = { - COMPANY_LOGO: 'companyLogo', - PHOTO: 'rn', - CV: 'cv', -} as const; - -export { FileRole }; export { type CVDto, type FileUploadResponse, From f344bef0ebd02f1f9d039c88ad55e9e72dfabc3b Mon Sep 17 00:00:00 2001 From: dobroillya Date: Fri, 29 Sep 2023 11:40:45 +0300 Subject: [PATCH 27/75] bt-716: * fix ts errors --- mobile/src/bundles/common/store/actions.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mobile/src/bundles/common/store/actions.ts b/mobile/src/bundles/common/store/actions.ts index 15d209fc0..6af39d50f 100644 --- a/mobile/src/bundles/common/store/actions.ts +++ b/mobile/src/bundles/common/store/actions.ts @@ -166,10 +166,12 @@ const getUserDetails = createAsyncThunk< companyLogoUrl: companyLogo?.url, }; } else { - return { - ...userDetails, - photoUrl: photo?.url, - }; + return userDetails + ? { + ...userDetails, + photoUrl: photo?.url, + } + : null; } } catch (error) { const errorMessage = getErrorMessage(error); From 6ecfad7ed13e107ad4d5ae6945d8540445a4fa0b Mon Sep 17 00:00:00 2001 From: dobroillya Date: Fri, 29 Sep 2023 12:10:15 +0300 Subject: [PATCH 28/75] bt-716: * fix display photo talent --- mobile/src/bundles/common/store/actions.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mobile/src/bundles/common/store/actions.ts b/mobile/src/bundles/common/store/actions.ts index 6af39d50f..342032a17 100644 --- a/mobile/src/bundles/common/store/actions.ts +++ b/mobile/src/bundles/common/store/actions.ts @@ -165,13 +165,17 @@ const getUserDetails = createAsyncThunk< photoUrl: photo?.url, companyLogoUrl: companyLogo?.url, }; + } else if (userDetails?.cvId) { + const cv = await fileUploadApi.getFileById({ + id: userDetails.cvId, + }); + return { + ...userDetails, + cvUrl: cv?.url, + photoUrl: photo?.url, + }; } else { - return userDetails - ? { - ...userDetails, - photoUrl: photo?.url, - } - : null; + return null; } } catch (error) { const errorMessage = getErrorMessage(error); From 9b3beadf2b3c28a705e63368c64cc181e6c2b8f6 Mon Sep 17 00:00:00 2001 From: mchornomaz Date: Fri, 29 Sep 2023 12:22:45 +0300 Subject: [PATCH 29/75] bt-695: * fixed talant page layout --- .../profile-cabinet/pages/styles.module.scss | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/frontend/src/bundles/profile-cabinet/pages/styles.module.scss b/frontend/src/bundles/profile-cabinet/pages/styles.module.scss index 59315cb5c..55ebbfb68 100644 --- a/frontend/src/bundles/profile-cabinet/pages/styles.module.scss +++ b/frontend/src/bundles/profile-cabinet/pages/styles.module.scss @@ -37,7 +37,7 @@ display: flex; flex-direction: column; gap: 26px; - width: 100%; + min-width: max-content; max-width: var(--talent-profile-form-width); padding: 40px; background-color: var(--white); @@ -91,19 +91,11 @@ } .stepContainer { - display: flex; - flex-direction: column; - align-content: center; - align-items: center; - width: 65vw; + margin: 0 auto; } @media screen and (width <=760px) { - .stepContainer { - display: flex; - flex-direction: column; - align-content: center; - align-items: center; - width: 100%; + .form { + min-width: 100%; } } From 6b45718d37ecb39727951e58159d8dd440dd575d Mon Sep 17 00:00:00 2001 From: dobroillya Date: Fri, 29 Sep 2023 14:26:40 +0300 Subject: [PATCH 30/75] bt-716: * fix issue with error in action --- mobile/src/bundles/common/store/actions.ts | 27 ++++++++++++---------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/mobile/src/bundles/common/store/actions.ts b/mobile/src/bundles/common/store/actions.ts index 342032a17..30999c479 100644 --- a/mobile/src/bundles/common/store/actions.ts +++ b/mobile/src/bundles/common/store/actions.ts @@ -152,26 +152,29 @@ const getUserDetails = createAsyncThunk< userId: payload.userId, }); - const photo = await fileUploadApi.getFileById({ - id: userDetails?.photoId ?? '', - }); - - if (userDetails?.companyLogoId) { - const companyLogo = await fileUploadApi.getFileById({ - id: userDetails.companyLogoId, + if (userDetails?.cvId && userDetails.photoId) { + const photo = await fileUploadApi.getFileById({ + id: userDetails.photoId, }); + const cv = await fileUploadApi.getFileById({ + id: userDetails.cvId, + }); + return { ...userDetails, + cvUrl: cv?.url, photoUrl: photo?.url, - companyLogoUrl: companyLogo?.url, }; - } else if (userDetails?.cvId) { - const cv = await fileUploadApi.getFileById({ - id: userDetails.cvId, + } else if (userDetails?.companyLogoId && userDetails.photoId) { + const companyLogo = await fileUploadApi.getFileById({ + id: userDetails.companyLogoId, + }); + const photo = await fileUploadApi.getFileById({ + id: userDetails.photoId, }); return { ...userDetails, - cvUrl: cv?.url, + companyLogoUrl: companyLogo?.url, photoUrl: photo?.url, }; } else { From c45178fc3b7e9efcd863be50ebd5bacc7b9227f3 Mon Sep 17 00:00:00 2001 From: JoshDagat Date: Fri, 29 Sep 2023 19:36:01 +0800 Subject: [PATCH 31/75] bt-820: + Add LMS feedback data to admin verification --- .../characteristics/characteristics.tsx | 29 +++++++++++++++++-- .../admin-panel/constants/constants.ts | 3 +- .../verifications/verifications-panel.tsx | 20 +++++++++++-- .../components/multi-read/multi-read.tsx | 7 ++++- frontend/src/bundles/lms/store/actions.ts | 4 +-- frontend/src/bundles/lms/store/slice.ts | 5 ++-- frontend/src/bundles/lms/types/types.ts | 5 +++- frontend/src/bundles/users/users-api.ts | 8 ++--- 8 files changed, 64 insertions(+), 17 deletions(-) diff --git a/frontend/src/bundles/admin-panel/components/characteristics/characteristics.tsx b/frontend/src/bundles/admin-panel/components/characteristics/characteristics.tsx index b01faa7d7..fc514c08c 100644 --- a/frontend/src/bundles/admin-panel/components/characteristics/characteristics.tsx +++ b/frontend/src/bundles/admin-panel/components/characteristics/characteristics.tsx @@ -1,8 +1,10 @@ +import { type LMSDataServerResponseDto } from 'shared/build/index.js'; + import { - mockFeedback, mockPersonalityTypeOptions, mockSoftSkills, } from '~/assets/mock-data/mock-data.js'; +import { ONE } from '~/bundles/admin-panel/constants/constants.js'; import { Autocomplete, FormControl, @@ -16,7 +18,11 @@ import { useAppForm } from '~/bundles/common/hooks/hooks.js'; import styles from './styles.module.scss'; -const Characteristics: React.FC = () => { +type Properties = { + lmsData: LMSDataServerResponseDto; +}; + +const Characteristics: React.FC = ({ lmsData }) => { const { control, errors } = useAppForm<{ personalityType: string; softSkills: string[]; @@ -24,6 +30,23 @@ const Characteristics: React.FC = () => { defaultValues: { personalityType: '', softSkills: [] }, }); + const { projectCoachesFeedback } = lmsData; + const feedbackData = projectCoachesFeedback.filter( + (it) => it.feedback !== null, + ); + + const mappedFeedbBack: { + title: string; + text: string; + }[] = []; + + for (const [index, feedback] of feedbackData.entries()) { + mappedFeedbBack.push({ + title: `Coach feedback from week ${index + ONE}`, + text: feedback.feedback as string, + }); + } + return ( @@ -35,7 +58,7 @@ const Characteristics: React.FC = () => { item className={getValidClassNames(styles.body, styles.feedback)} > - + diff --git a/frontend/src/bundles/admin-panel/constants/constants.ts b/frontend/src/bundles/admin-panel/constants/constants.ts index a7cd539d9..e0d34eaa0 100644 --- a/frontend/src/bundles/admin-panel/constants/constants.ts +++ b/frontend/src/bundles/admin-panel/constants/constants.ts @@ -1,4 +1,5 @@ const FIRST_INDEX = 0; +const ONE = 1; export { PreviewTab } from './preview-tab.constant.js'; -export { FIRST_INDEX }; +export { FIRST_INDEX, ONE }; diff --git a/frontend/src/bundles/admin-panel/pages/verifications/verifications-panel.tsx b/frontend/src/bundles/admin-panel/pages/verifications/verifications-panel.tsx index 70cc8d6c7..a6058a682 100644 --- a/frontend/src/bundles/admin-panel/pages/verifications/verifications-panel.tsx +++ b/frontend/src/bundles/admin-panel/pages/verifications/verifications-panel.tsx @@ -1,4 +1,5 @@ import { ManageSearch } from '@mui/icons-material'; +import { type LMSDataServerResponseDto } from 'shared/build/index.js'; import { Avatar, @@ -19,6 +20,7 @@ import { useTheme, } from '~/bundles/common/hooks/hooks.js'; import { actions as adminActions } from '~/bundles/hiring-info/store/hiring-info.js'; +import { actions as lmsActions } from '~/bundles/lms/store/lms.js'; import { Characteristics, @@ -39,8 +41,12 @@ const AdminVerificationsPanel: React.FC = () => { const dispatch = useAppDispatch(); const theme = useTheme(); - const { shortDetails, fullDetails } = useAppSelector( - (state) => state.admin, + const { shortDetails, fullDetails, lmsData } = useAppSelector( + ({ admin, lms }) => ({ + shortDetails: admin.shortDetails, + fullDetails: admin.fullDetails, + lmsData: lms.lmsData, + }), ); const [filter, setFilter] = useState('talent'); @@ -86,7 +92,9 @@ const AdminVerificationsPanel: React.FC = () => { userDetails={fullDetails as UserDetailsFullResponseDto} /> ), - [PreviewTab.CHARACTERISTICS]: , + [PreviewTab.CHARACTERISTICS]: ( + + ), }; useEffect(() => { @@ -106,6 +114,12 @@ const AdminVerificationsPanel: React.FC = () => { ); } }, [selectedId, dispatch]); + + useEffect(() => { + if (selectedId) { + void dispatch(lmsActions.getTalentLmsData({ userId: selectedId })); + } + }, [dispatch, selectedId]); const isScreenMoreMD = useMediaQuery(theme.breakpoints.up('md')); const isTogglePreviewAllowed = !isScreenMoreMD && isFilterOpen; diff --git a/frontend/src/bundles/common/components/multi-read/multi-read.tsx b/frontend/src/bundles/common/components/multi-read/multi-read.tsx index 813744c23..4241a4308 100644 --- a/frontend/src/bundles/common/components/multi-read/multi-read.tsx +++ b/frontend/src/bundles/common/components/multi-read/multi-read.tsx @@ -20,6 +20,10 @@ const MultiRead: React.FC = ({ rows = [] }) => { [setSelectedId], ); + const html = { + __html: rows[selectedId].text, + }; + return ( @@ -40,7 +44,8 @@ const MultiRead: React.FC = ({ rows = [] }) => { - {rows[selectedId].text} +
+ {/* {rows[selectedId].text} */}
diff --git a/frontend/src/bundles/lms/store/actions.ts b/frontend/src/bundles/lms/store/actions.ts index 2974639ca..33fb2dc40 100644 --- a/frontend/src/bundles/lms/store/actions.ts +++ b/frontend/src/bundles/lms/store/actions.ts @@ -2,10 +2,10 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { type AsyncThunkConfig } from '~/bundles/common/types/types.js'; -import { type UserLMSDataDto } from '../types/types.js'; +import { type LMSDataServerResponseDto } from './../types/types.js'; const getTalentLmsData = createAsyncThunk< - UserLMSDataDto | null, + LMSDataServerResponseDto | null, { userId: string }, AsyncThunkConfig >('lms/lms-data', async (payload, { extra, rejectWithValue }) => { diff --git a/frontend/src/bundles/lms/store/slice.ts b/frontend/src/bundles/lms/store/slice.ts index fcb39ef59..f6207c045 100644 --- a/frontend/src/bundles/lms/store/slice.ts +++ b/frontend/src/bundles/lms/store/slice.ts @@ -1,14 +1,15 @@ import { createSlice } from '@reduxjs/toolkit'; +import { type LMSDataServerResponseDto } from 'shared/build/index.js'; import { mockBadges } from '~/assets/mock-data/mock-data.js'; import { DataStatus } from '~/bundles/common/enums/enums.js'; import { type ValueOf } from '~/framework/socket/types/types.js'; -import { type BSABadge, type UserLMSDataDto } from '../types/types.js'; +import { type BSABadge } from '../types/types.js'; import { getTalentLmsData } from './actions.js'; type State = { - lmsData: UserLMSDataDto | null; + lmsData: LMSDataServerResponseDto | null; bsaBadges: BSABadge[]; dataStatus: ValueOf; }; diff --git a/frontend/src/bundles/lms/types/types.ts b/frontend/src/bundles/lms/types/types.ts index 325684a54..a0dc3ccb5 100644 --- a/frontend/src/bundles/lms/types/types.ts +++ b/frontend/src/bundles/lms/types/types.ts @@ -14,4 +14,7 @@ type BSABadge = { }; export { type BSABadge }; -export { type UserLMSDataDto } from 'shared/build/index.js'; +export { + type LMSDataServerResponseDto, + type UserLMSDataDto, +} from 'shared/build/index.js'; diff --git a/frontend/src/bundles/users/users-api.ts b/frontend/src/bundles/users/users-api.ts index e7305fd83..011b9b109 100644 --- a/frontend/src/bundles/users/users-api.ts +++ b/frontend/src/bundles/users/users-api.ts @@ -1,10 +1,10 @@ +import { type LMSDataServerResponseDto } from 'shared/build/index.js'; + import { ApiPath, ContentType } from '~/bundles/common/enums/enums.js'; import { HttpApiBase } from '~/framework/api/api.js'; import { type Http } from '~/framework/http/http.js'; import { type Storage } from '~/framework/storage/storage.js'; -import { type UserLMSDataDto } from '../lms/types/types.js'; - type Constructor = { baseUrl: string; http: Http; @@ -18,7 +18,7 @@ class UsersApi extends HttpApiBase { public async getTalentLmsDataById( payload: string, - ): Promise { + ): Promise { const path = '/:userId/lms-data'.replace(':userId', payload); const response = await this.load(this.getFullEndpoint(path, {}), { method: 'GET', @@ -26,7 +26,7 @@ class UsersApi extends HttpApiBase { hasAuth: true, }); - return await response.json(); + return await response.json(); } } From 7923b950f37cf0c5fe352072f6c4e5e7817d033d Mon Sep 17 00:00:00 2001 From: likeri29 Date: Fri, 29 Sep 2023 13:48:35 +0200 Subject: [PATCH 32/75] bt-716: * fix error when getting user by id --- mobile/src/bundles/common/store/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/src/bundles/common/store/actions.ts b/mobile/src/bundles/common/store/actions.ts index 30999c479..d5856d47b 100644 --- a/mobile/src/bundles/common/store/actions.ts +++ b/mobile/src/bundles/common/store/actions.ts @@ -178,7 +178,7 @@ const getUserDetails = createAsyncThunk< photoUrl: photo?.url, }; } else { - return null; + return userDetails; } } catch (error) { const errorMessage = getErrorMessage(error); From 7d3eb95d3972e5bba030171abff382b4c75ae9d0 Mon Sep 17 00:00:00 2001 From: dobroillya Date: Fri, 29 Sep 2023 15:18:29 +0300 Subject: [PATCH 33/75] bt-832: * change styles --- .../components/candidate-card/candidate-card.tsx | 12 +++++------- .../employer/components/candidate-card/styles.ts | 1 + 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/mobile/src/bundles/employer/components/candidate-card/candidate-card.tsx b/mobile/src/bundles/employer/components/candidate-card/candidate-card.tsx index 64ec7787e..ed945e3f5 100644 --- a/mobile/src/bundles/employer/components/candidate-card/candidate-card.tsx +++ b/mobile/src/bundles/employer/components/candidate-card/candidate-card.tsx @@ -129,21 +129,19 @@ const CandidateCard: React.FC = (candidateInfo) => { ), )} + + Skills + - - Skills - {hardSkills.slice(0, MaxValue.SKILLS).map(({ name, id }) => ( ))} diff --git a/mobile/src/bundles/employer/components/candidate-card/styles.ts b/mobile/src/bundles/employer/components/candidate-card/styles.ts index b48d24a2b..7337fdef4 100644 --- a/mobile/src/bundles/employer/components/candidate-card/styles.ts +++ b/mobile/src/bundles/employer/components/candidate-card/styles.ts @@ -22,6 +22,7 @@ const styles = StyleSheet.create({ }, badgeContainer: { gap: 10, + flexWrap: 'wrap', }, skills: { gap: 15, From 98cae028f188a7ad587a154075b0377ff0a403ae Mon Sep 17 00:00:00 2001 From: likeri29 Date: Fri, 29 Sep 2023 14:22:18 +0200 Subject: [PATCH 34/75] bt-716: * fix dispalying talent photo --- mobile/src/bundles/common/store/actions.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mobile/src/bundles/common/store/actions.ts b/mobile/src/bundles/common/store/actions.ts index d5856d47b..aae58882b 100644 --- a/mobile/src/bundles/common/store/actions.ts +++ b/mobile/src/bundles/common/store/actions.ts @@ -152,17 +152,13 @@ const getUserDetails = createAsyncThunk< userId: payload.userId, }); - if (userDetails?.cvId && userDetails.photoId) { + if (userDetails?.photoId) { const photo = await fileUploadApi.getFileById({ id: userDetails.photoId, }); - const cv = await fileUploadApi.getFileById({ - id: userDetails.cvId, - }); return { ...userDetails, - cvUrl: cv?.url, photoUrl: photo?.url, }; } else if (userDetails?.companyLogoId && userDetails.photoId) { From 35f31d8422d9b2fb3c9db742b8fef284bb42f2bb Mon Sep 17 00:00:00 2001 From: savinaDaria Date: Fri, 29 Sep 2023 15:42:50 +0300 Subject: [PATCH 35/75] bt-819: * fix modal send vacancy and hire action --- .../candidate-modal.tsx | 9 +- .../candidate-profile/candidate-profile.tsx | 22 +++- .../profile-second-section.tsx | 3 + .../components/company-info/company-info.tsx | 106 ++++++++++++++---- .../company-info/styles.module.scss | 11 +- frontend/src/bundles/chat/store/slice.ts | 13 +++ .../pages/candidate-page/candidate-page.tsx | 20 +++- .../src/helpers/parse-messages.helper.tsx | 33 ++++-- .../contact-modal.validation-schema.ts | 4 +- 9 files changed, 183 insertions(+), 38 deletions(-) diff --git a/frontend/src/bundles/candidate-details/components/candidate-contact-modal/candidate-modal.tsx b/frontend/src/bundles/candidate-details/components/candidate-contact-modal/candidate-modal.tsx index 6166b1899..796ee7769 100644 --- a/frontend/src/bundles/candidate-details/components/candidate-contact-modal/candidate-modal.tsx +++ b/frontend/src/bundles/candidate-details/components/candidate-contact-modal/candidate-modal.tsx @@ -103,9 +103,16 @@ const CandidateModal: React.FC = ({ isOpen = true, onClose }) => { chatActions.createMessage({ senderId: employerId, receiverId: candidateId, - message: data.message, + message: + data.message + + ' \n\n ' + + data.links + .map((item) => ` Vacancy_&_${item.value} `) + .join(' '), }), ); + + void dispatch(chatActions.getAllChatsByUserId(employerId)); onClose(); } }, diff --git a/frontend/src/bundles/candidate-details/components/candidate-profile/candidate-profile.tsx b/frontend/src/bundles/candidate-details/components/candidate-profile/candidate-profile.tsx index 03b6bd1ef..c5e30fb65 100644 --- a/frontend/src/bundles/candidate-details/components/candidate-profile/candidate-profile.tsx +++ b/frontend/src/bundles/candidate-details/components/candidate-profile/candidate-profile.tsx @@ -4,6 +4,7 @@ import { UserRole } from 'shared/build/index.js'; import { type State } from '~/bundles/auth/store/auth.js'; import { CandidateModal } from '~/bundles/candidate-details/components/components.js'; import { actions as candidateActions } from '~/bundles/candidate-details/store/candidate.js'; +import { actions as chatActions } from '~/bundles/chat/store/chat.js'; import { Button, Grid } from '~/bundles/common/components/components.js'; import { useCommonData } from '~/bundles/common/data/hooks/use-common-data.hook.js'; import { getValidClassNames } from '~/bundles/common/helpers/helpers.js'; @@ -38,6 +39,7 @@ type Properties = { candidateData?: SeacrhCandidateResponse & { email?: string; }; + hasSentAlreadyFirstMessage?: boolean; }; const getAuthState = (state: RootReducer): State => state.auth; @@ -47,6 +49,7 @@ const CandidateProfile: React.FC = ({ isFifthStep, isProfileCard, candidateData, + hasSentAlreadyFirstMessage = false, }) => { const [isContactModalOpen, setIsContactModalOpen] = useState(false); @@ -68,6 +71,7 @@ const CandidateProfile: React.FC = ({ email: state.auth.currentUser?.email, lmsProject: state.lms.lmsData?.project, })); + const { publishedAt, isApproved } = useAppSelector( (state: RootReducer) => state.talentOnBoarding, ); @@ -79,7 +83,6 @@ const CandidateProfile: React.FC = ({ void dispatch(talentActions.getTalentDetails({ userId })); void dispatch(lmsActions.getTalentLmsData({ userId })); - if (currentUser?.role == UserRole.EMPLOYER) { void dispatch( hiringInfoActions.getHiringInfo({ @@ -93,9 +96,24 @@ const CandidateProfile: React.FC = ({ companyId: userId, }), ); + void dispatch(chatActions.getAllChatsByUserId(currentUser.id)); } }, [currentUser, data.userId, dispatch]); + const { chats } = useAppSelector(({ chat }) => ({ + chats: chat.chats, + })); + const [hasAlreadySentFirstMessage, setHasSentAlreadyFirstMessage] = + useState(hasSentAlreadyFirstMessage); + useEffect(() => { + const chatWithCandidate = chats.find( + (chat) => chat.participants.receiver.id == data.userId, + ); + if (chatWithCandidate) { + setHasSentAlreadyFirstMessage(true); + } + }, [chats, data.userId, hasSentAlreadyFirstMessage]); + const hardskillsLabels = hardSkillsOptions .filter( (item) => @@ -172,6 +190,7 @@ const CandidateProfile: React.FC = ({ = ({ label="Contact candidate" className={styles.contactButton} onClick={handleOpenContactModal} + isDisabled={hasAlreadySentFirstMessage} />
)} diff --git a/frontend/src/bundles/candidate-details/components/candidate-profile/profile-second-section/profile-second-section.tsx b/frontend/src/bundles/candidate-details/components/candidate-profile/profile-second-section/profile-second-section.tsx index 47667ed79..72ffae8cf 100644 --- a/frontend/src/bundles/candidate-details/components/candidate-profile/profile-second-section/profile-second-section.tsx +++ b/frontend/src/bundles/candidate-details/components/candidate-profile/profile-second-section/profile-second-section.tsx @@ -26,6 +26,7 @@ type Properties = { candidateParameters: SecondSectionDetails; isProfileOpen?: boolean; isFifthStep?: boolean; + hasSentAlreadyFirstMessage?: boolean; isContactModalOpen: boolean; onContactModalClose: () => void; onContactModalOpen: () => void; @@ -35,6 +36,7 @@ const ProfileSecondSection: React.FC = ({ candidateParameters, isProfileOpen, isFifthStep, + hasSentAlreadyFirstMessage, isContactModalOpen, onContactModalClose, onContactModalOpen, @@ -189,6 +191,7 @@ const ProfileSecondSection: React.FC = ({ label="Contact candidate" className={styles.contactButton} onClick={onContactModalOpen} + isDisabled={hasSentAlreadyFirstMessage} />
)} diff --git a/frontend/src/bundles/chat/components/company-info/company-info.tsx b/frontend/src/bundles/chat/components/company-info/company-info.tsx index 8b1d07ba7..2098c33fc 100644 --- a/frontend/src/bundles/chat/components/company-info/company-info.tsx +++ b/frontend/src/bundles/chat/components/company-info/company-info.tsx @@ -3,15 +3,20 @@ import { actions as chatActions } from '~/bundles/chat/store/chat.js'; import { Avatar, Button, + FormControl, Grid, Logo, + RadioGroup, Typography, } from '~/bundles/common/components/components.js'; import { useAppDispatch, + useAppForm, useAppSelector, useCallback, + useEffect, } from '~/bundles/common/hooks/hooks.js'; +import { actions as hiringInfoActions } from '~/bundles/hiring-info/store/hiring-info.js'; import { userDetailsApi } from '~/bundles/user-details/user-details.js'; import styles from './styles.module.scss'; @@ -20,15 +25,32 @@ type Properties = { className?: string; }; +const options = [ + { + value: 'Yes', + label: 'Yes', + }, + { + value: 'No', + label: 'No', + }, +]; const CompanyInfo: React.FC = ({ className }) => { - const { company, hasSharedContacts, talentId, employerId, currentChatId } = - useAppSelector(({ chat }) => ({ - company: chat.current.employerDetails, - hasSharedContacts: chat.current.talentHasSharedContacts, - talentId: chat.current.talentId, - employerId: chat.current.employerDetails.employerId, - currentChatId: chat.current.chatId, - })); + const { + company, + hasSharedContacts, + talentId, + talentIsHired, + companyId, + currentChatId, + } = useAppSelector(({ chat }) => ({ + company: chat.current.employerDetails, + hasSharedContacts: chat.current.talentHasSharedContacts, + talentId: chat.current.talentId, + companyId: chat.current.employerDetails.employerId, + currentChatId: chat.current.chatId, + talentIsHired: chat.current.talentIsHired, + })); const dispatch = useAppDispatch(); const { @@ -56,7 +78,7 @@ const CompanyInfo: React.FC = ({ className }) => { `CV_&_${cvUrl} ` + `Profile_&_${baseUrl}/candidates/${talentId} `, senderId: talentId as string, - receiverId: employerId as string, + receiverId: companyId as string, chatId: currentChatId as string, }), ); @@ -65,11 +87,33 @@ const CompanyInfo: React.FC = ({ className }) => { }; void createNotificationMessage(); - }, [dispatch, currentChatId, employerId, talentId]); + }, [dispatch, currentChatId, companyId, talentId]); + + const { control, watch } = useAppForm<{ hire: 'Yes' | 'No' }>({ + defaultValues: { hire: 'Yes' }, + }); - const handleAlreadyHiredButtonClick = useCallback(() => { - //TODO: Implement button click handler - }, []); + useEffect(() => { + if (talentId && companyId) { + void dispatch( + hiringInfoActions.getHiringInfo({ + talentId, + companyId, + }), + ); + } + }, [dispatch, companyId, talentId]); + + const handleHireSubmit = useCallback((): void => { + if (watch('hire') === 'Yes') { + void dispatch( + hiringInfoActions.submitHiringInfo({ + talentId: talentId ?? '', + companyId: companyId ?? '', + }), + ); + } + }, [dispatch, companyId, talentId, watch]); const aboutInfo = about ?? 'No information provided'; return currentChatId ? ( @@ -137,13 +181,35 @@ const CompanyInfo: React.FC = ({ className }) => { onClick={handleShareCVButtonClick} isDisabled={hasSharedContacts} /> - )} - + {icon}

{name}

diff --git a/frontend/src/bundles/common/components/sidebar/sidebar.tsx b/frontend/src/bundles/common/components/sidebar/sidebar.tsx index 6dc2631c7..4aeac9093 100644 --- a/frontend/src/bundles/common/components/sidebar/sidebar.tsx +++ b/frontend/src/bundles/common/components/sidebar/sidebar.tsx @@ -83,6 +83,7 @@ const Sidebar: React.FC = () => { className={getValidClassNames( isSidebarVisible ? styles.visible : styles.hidden, styles.wrapper, + currentUser?.role === UserRole.ADMIN && styles.adminSidebar, )} > @@ -93,6 +94,7 @@ const Sidebar: React.FC = () => { icon={item.icon} link={item.link} name={item.name} + currentUser={currentUser?.role} /> ))} diff --git a/frontend/src/bundles/common/components/sidebar/styles.module.scss b/frontend/src/bundles/common/components/sidebar/styles.module.scss index fc3e63791..712864a36 100644 --- a/frontend/src/bundles/common/components/sidebar/styles.module.scss +++ b/frontend/src/bundles/common/components/sidebar/styles.module.scss @@ -9,6 +9,10 @@ transition: all 0.2s ease-in; } +.adminSidebar { + background-color: var(--slate-gray); +} + .visible { left: 0; } @@ -58,6 +62,26 @@ a { } } +.adminSidebarIcons, +.adminSidebarIcons[aria-current="page"] { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: 100%; + color: var(--white); + text-decoration: none; + + .title { + margin: 0 auto; + font-weight: var(--font-weight-semi-bold); + } + + &[aria-current="page"] { + color: var(--sunglow); + } +} + .listItem { position: relative; width: 100%; diff --git a/frontend/src/bundles/search-candidates/components/filters/employee-filters.tsx b/frontend/src/bundles/search-candidates/components/filters/employee-filters.tsx index 159bc370d..e44f5fce7 100644 --- a/frontend/src/bundles/search-candidates/components/filters/employee-filters.tsx +++ b/frontend/src/bundles/search-candidates/components/filters/employee-filters.tsx @@ -20,8 +20,8 @@ import { type ValueOf } from '~/bundles/common/types/types.js'; import { DEFAULT_EMPLOYEES_FILTERS_PAYLOAD } from '../../constants/constants.js'; import { - BsaCharacteristics, - BsaProject, + // BsaCharacteristics, + // BsaProject, CheckboxesFields, Country, EmploymentType, @@ -45,17 +45,17 @@ const yearsOfExperience = Object.values(YearsOfExperience).map( }), ); -const bsaCharacteristics = Object.values(BsaCharacteristics).map( - (characteristic) => ({ - value: characteristic, - label: characteristic, - }), -); +// const bsaCharacteristics = Object.values(BsaCharacteristics).map( +// (characteristic) => ({ +// value: characteristic, +// label: characteristic, +// }), +// ); -const bsaProjects = Object.values(BsaProject).map((project) => ({ - value: project, - label: project, -})); +// const bsaProjects = Object.values(BsaProject).map((project) => ({ +// value: project, +// label: project, +// })); const locationOptions = Object.values(Country).map((country) => ({ value: country, @@ -235,7 +235,7 @@ const EmployeeFilters: React.FC = ({ control, reset }) => { />
- + {/* BSA Characteristics