diff --git a/cypress/e2e/Auth.cy.ts b/cypress/e2e/Auth.cy.ts index 1c7fec13..a3f8be9f 100644 --- a/cypress/e2e/Auth.cy.ts +++ b/cypress/e2e/Auth.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { getTestSelectorByModule } from 'support/helpers'; import { interceptFetchClientFailedRequest, diff --git a/cypress/e2e/Branch/BranchCatalog.cy.ts b/cypress/e2e/Branch/BranchCatalog.cy.ts index 771d58e3..6e8f4ca3 100644 --- a/cypress/e2e/Branch/BranchCatalog.cy.ts +++ b/cypress/e2e/Branch/BranchCatalog.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { clearInputField, clickActionButton, diff --git a/cypress/e2e/Branch/BranchManagement.cy.ts b/cypress/e2e/Branch/BranchManagement.cy.ts index e8242edf..52de5435 100644 --- a/cypress/e2e/Branch/BranchManagement.cy.ts +++ b/cypress/e2e/Branch/BranchManagement.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { clearBranchDetailsForm, fillBranchDetailsForm, diff --git a/cypress/e2e/Branch/BranchView.cy.ts b/cypress/e2e/Branch/BranchView.cy.ts index 9ecd8ddb..a00b743a 100644 --- a/cypress/e2e/Branch/BranchView.cy.ts +++ b/cypress/e2e/Branch/BranchView.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { getLinearLoader, getTestSelectorByModule, selectAction, verifyInputFields, verifyTextFields } from 'support/helpers'; import { interceptEditBranchRequest, diff --git a/cypress/e2e/Company/CompanyManagement.cy.ts b/cypress/e2e/Company/CompanyManagement.cy.ts index 162979a8..028d86b4 100644 --- a/cypress/e2e/Company/CompanyManagement.cy.ts +++ b/cypress/e2e/Company/CompanyManagement.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { clearInputField, clickActionButton, diff --git a/cypress/e2e/Company/CompanySettings.cy.ts b/cypress/e2e/Company/CompanySettings.cy.ts index 4960d5df..3bcb43c0 100644 --- a/cypress/e2e/Company/CompanySettings.cy.ts +++ b/cypress/e2e/Company/CompanySettings.cy.ts @@ -1,5 +1,6 @@ -import { COMPANY_SETTINGS_KEY, ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { COMPANY_SETTINGS_KEY } from 'shared/constants/configs'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { clickActionButton, clickFlowsIcon, diff --git a/cypress/e2e/Department/DepartmentCatalog.cy.ts b/cypress/e2e/Department/DepartmentCatalog.cy.ts index 42841c70..796df94c 100644 --- a/cypress/e2e/Department/DepartmentCatalog.cy.ts +++ b/cypress/e2e/Department/DepartmentCatalog.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { clearInputField, clickActionButton, diff --git a/cypress/e2e/Department/DepartmentManagement.cy.ts b/cypress/e2e/Department/DepartmentManagement.cy.ts index fd4ef4fc..21e890fc 100644 --- a/cypress/e2e/Department/DepartmentManagement.cy.ts +++ b/cypress/e2e/Department/DepartmentManagement.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { getLinearLoader, getLoadingButtonLoadingIcon, diff --git a/cypress/e2e/Department/DepartmentView.cy.ts b/cypress/e2e/Department/DepartmentView.cy.ts index 254a98a6..06c7c6d9 100644 --- a/cypress/e2e/Department/DepartmentView.cy.ts +++ b/cypress/e2e/Department/DepartmentView.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { getLinearLoader, getTestSelectorByModule, selectAction, verifyInputFields, verifyTextFields } from 'support/helpers'; import { interceptFetchBranchesRequest, diff --git a/cypress/e2e/Employee/EmployeeCatalog.cy.ts b/cypress/e2e/Employee/EmployeeCatalog.cy.ts index 1dc741e3..da8e9ced 100644 --- a/cypress/e2e/Employee/EmployeeCatalog.cy.ts +++ b/cypress/e2e/Employee/EmployeeCatalog.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { clearInputField, getLinearLoader, diff --git a/cypress/e2e/Employee/EmployeeManagement.cy.ts b/cypress/e2e/Employee/EmployeeManagement.cy.ts index 60a6d63d..d4756b86 100644 --- a/cypress/e2e/Employee/EmployeeManagement.cy.ts +++ b/cypress/e2e/Employee/EmployeeManagement.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { clickActionButton, getLinearLoader, diff --git a/cypress/e2e/Employee/EmployeeOrgChart.cy.ts b/cypress/e2e/Employee/EmployeeOrgChart.cy.ts index bbdde83e..51b074b7 100644 --- a/cypress/e2e/Employee/EmployeeOrgChart.cy.ts +++ b/cypress/e2e/Employee/EmployeeOrgChart.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { checkEmployeeReportsTo, clickActionButton, diff --git a/cypress/e2e/Employee/Profile.cy.ts b/cypress/e2e/Employee/Profile.cy.ts index 235aa08f..00eef20d 100644 --- a/cypress/e2e/Employee/Profile.cy.ts +++ b/cypress/e2e/Employee/Profile.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { clearInputField, clickActionButton, diff --git a/cypress/e2e/Flows.cy.ts b/cypress/e2e/Flows.cy.ts index 4aa95a18..829570ea 100644 --- a/cypress/e2e/Flows.cy.ts +++ b/cypress/e2e/Flows.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { checkIsSubFlowDisabled, checkIsSubFlowHasDisabledAttribute, diff --git a/cypress/e2e/License.cy.ts b/cypress/e2e/License.cy.ts index 28152f31..3d134f5b 100644 --- a/cypress/e2e/License.cy.ts +++ b/cypress/e2e/License.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { getTestSelectorByModule } from 'support/helpers'; import { interceptFetchBranchesRequest, diff --git a/cypress/e2e/Offboarding.cy.ts b/cypress/e2e/Offboarding.cy.ts index 39c06968..49885327 100644 --- a/cypress/e2e/Offboarding.cy.ts +++ b/cypress/e2e/Offboarding.cy.ts @@ -1,5 +1,6 @@ -import { COMPANY_SETTINGS_KEY, ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { COMPANY_SETTINGS_KEY } from 'shared/constants/configs'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { clickActionButton, getTestSelectorByModule, diff --git a/cypress/e2e/Onboarding.cy.ts b/cypress/e2e/Onboarding.cy.ts index a116e205..74298eb3 100644 --- a/cypress/e2e/Onboarding.cy.ts +++ b/cypress/e2e/Onboarding.cy.ts @@ -1,5 +1,5 @@ -import { ROUTES } from 'shared/constants'; -import { Module, SubModule } from 'shared/types'; +import { ROUTES } from 'shared/constants/routes'; +import { Module, SubModule } from 'shared/types/module'; import { clickActionButton, clickField, diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index fbdbc4c2..44c92f3e 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,7 +1,7 @@ /// import { SinonStub } from 'cypress/types/sinon'; -import { ThemeMode } from 'shared/types'; +import { ThemeMode } from 'shared/types/theme'; // *********************************************** // This example commands.ts shows you how to diff --git a/cypress/support/helpers.ts b/cypress/support/helpers.ts index 2276bcba..17152c78 100644 --- a/cypress/support/helpers.ts +++ b/cypress/support/helpers.ts @@ -1,4 +1,8 @@ -import { Branch, ColorSchemeId, Department, EntityInput, Module, SubModule, ThemeMode } from 'shared/types'; +import { Branch } from 'shared/types/branch'; +import { EntityInput } from 'shared/types/common'; +import { Department } from 'shared/types/department'; +import { Module, SubModule } from 'shared/types/module'; +import { ColorSchemeId, ThemeMode } from 'shared/types/theme'; const colorSchemeValues: ColorSchemeId[] = [ 'midnight', diff --git a/cypress/support/interceptors.ts b/cypress/support/interceptors.ts index d294dc27..a2b50d08 100644 --- a/cypress/support/interceptors.ts +++ b/cypress/support/interceptors.ts @@ -1,4 +1,10 @@ -import { Branch, Department, Employee, PaginatedResponse } from '../../src/shared/types'; +import { Branch } from '../../src/shared/types/branch'; +import { Department } from '../../src/shared/types/department'; +import { Employee } from '../../src/shared/types/employee'; + +type PaginatedResponse = { + items: T[]; +}; const baseUrl = Cypress.config('baseUrl'); const serverBaseUrl = Cypress.env('serverBaseUrl'); @@ -12,18 +18,12 @@ export const interceptFetchSystemLicenseRequest = () => { export const interceptFetchClientRequest = () => { cy.fixture('client').then((client) => { - cy.intercept('GET', `${serverBaseUrl}/Identity/Client?origin=${Cypress.config('baseUrl')}`, { body: client }).as('fetchClientRequest'); + cy.intercept('GET', `${serverBaseUrl}/Identity/Client*`, { body: client }).as('fetchClientRequest'); }); }; export const interceptFetchClientFailedRequest = () => { - cy.interceptWithAuth( - 'GET', - `${serverBaseUrl}/Identity/Client?origin=${Cypress.config('baseUrl')}`, - null, - 'fetchClientFailedRequest', - 404 - ); + cy.interceptWithAuth('GET', `${serverBaseUrl}/Identity/Client*`, null, 'fetchClientFailedRequest', 404); }; export const interceptLoginRequest = () => { diff --git a/jest.config.ts b/jest.config.ts index 08792375..3461fbfc 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -171,12 +171,11 @@ const config: Config = { // A map from regular expressions to paths to transformers transform: { - '^.+\\.tsx?$': 'ts-jest', - '^.+\\.ts?$': 'ts-jest', + '^.+\\.[tj]sx?$': 'ts-jest', }, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - transformIgnorePatterns: ['node_modules/(?!(\\oidc-client-ts)/)'], + transformIgnorePatterns: ['node_modules/(?!(@fossa-app|@fable-org|oidc-client-ts)/)'], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, diff --git a/package-lock.json b/package-lock.json index b872c749..463b93a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.13.0", + "@fossa-app/bridge": "^0.1.34", "@mui/icons-material": "^6.1.6", "@mui/material": "^6.4.8", "@reduxjs/toolkit": "^2.11.2", @@ -1758,6 +1759,21 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fable-org/fable-library-ts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fable-org/fable-library-ts/-/fable-library-ts-2.0.0.tgz", + "integrity": "sha512-jT0//WjcvRqC8x+9t8nrsKUmuCJdKsl/dXDfMCtk4g8qh9CAX/g9xzdbhTTtUV4Dxp3lkHkylKV/5gel4P05Ew==", + "license": "MIT" + }, + "node_modules/@fossa-app/bridge": { + "version": "0.1.34", + "resolved": "https://registry.npmjs.org/@fossa-app/bridge/-/bridge-0.1.34.tgz", + "integrity": "sha512-zRVgAseXtagw29dD2d30smsUl6i9odWjl0SOHQVf5izLrJ9K2CXRvoogyiIXSCZD9kiHset9YahmQomjnBYF+g==", + "license": "MIT", + "dependencies": { + "@fable-org/fable-library-ts": "^2.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", diff --git a/package.json b/package.json index e81cad6d..12be4e01 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,10 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.13.0", + "@fossa-app/bridge": "^0.1.34", "@mui/icons-material": "^6.1.6", "@mui/material": "^6.4.8", "@reduxjs/toolkit": "^2.11.2", - "axios": "^1.16.1", "leaflet": "^1.9.4", "oidc-client-ts": "^3.5.0", "react": "^19.2.0", diff --git a/src/AxiosInterceptor.tsx b/src/AxiosInterceptor.tsx deleted file mode 100644 index d2782771..00000000 --- a/src/AxiosInterceptor.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useAppDispatch, useAppSelector } from 'store'; -import { removeUser, selectAuthSettings, setError } from 'store/features'; -import axios, { AxiosError, AxiosRequestConfig } from 'shared/configs/axios'; -import { getUserFromLocalStorage, getUserManager, parseResponse } from 'shared/helpers'; -import { MESSAGES, ROUTES } from 'shared/constants'; -import { ErrorResponseDTO } from 'shared/types'; - -// TODO: debug, a weird issue takes place when authenticated, the token is not being set and receiving 401, fixes after refresh -const AxiosInterceptor: React.FC = ({ children }) => { - const navigate = useNavigate(); - const userManager = getUserManager(); - const dispatch = useAppDispatch(); - const { item: authSettings } = useAppSelector(selectAuthSettings); - const [shouldNavigate, setShouldNavigate] = React.useState(false); - - const refreshToken = React.useCallback( - async (errorConfig: AxiosRequestConfig): Promise => { - try { - const user = await userManager.signinSilent(); - - if (user?.access_token && errorConfig.headers) { - errorConfig.headers.Authorization = `${user.token_type} ${user.access_token}`; - - return axios(errorConfig); - } - } catch { - await userManager.removeUser(); - - setShouldNavigate(true); - dispatch(removeUser()); - - dispatch( - setError({ - title: MESSAGES.error.general.unAuthorized, - }) - ); - - return Promise.reject({ - title: MESSAGES.error.general.unAuthorized, - status: 401, - }); - } - - return null; - }, - [dispatch, userManager] - ); - - React.useEffect(() => { - if (shouldNavigate) { - navigate(ROUTES.login.path); - } - }, [shouldNavigate, navigate]); - - React.useEffect(() => { - const requestInterceptor = axios.interceptors.request.use((config) => { - const { access_token, token_type = 'Bearer' } = getUserFromLocalStorage(authSettings.client_id) || {}; - - if (access_token) { - config.headers.Authorization = `${token_type} ${access_token}`; - } - - return config; - }); - - const responseInterceptor = axios.interceptors.response.use( - (response) => { - return parseResponse(response); - }, - async (error: AxiosError) => { - if (!error.isAxiosError) { - return Promise.reject(error); - } - - if (error.response) { - error.response = parseResponse(error.response); - } - - if (error.code === 'ERR_NETWORK') { - dispatch( - setError({ - title: MESSAGES.error.general.network, - }) - ); - - return Promise.reject({ - status: 599, - title: MESSAGES.error.general.network, - }); - } - - if (error.config && error.response?.status === 401) { - return refreshToken(error.config); - } - - if (error.response && error.response.status >= 500) { - dispatch( - setError({ - title: MESSAGES.error.general.common, - }) - ); - - return Promise.reject({ - ...(error.response.data as ErrorResponseDTO), - title: MESSAGES.error.general.common, - }); - } - - return Promise.reject(error.response?.data); - } - ); - - return () => { - axios.interceptors.request.eject(requestInterceptor); - axios.interceptors.response.eject(responseInterceptor); - }; - }, [authSettings, dispatch, refreshToken]); - - return <>{children}; -}; - -export default AxiosInterceptor; diff --git a/src/components/Catalog.tsx b/src/components/Catalog.tsx index 3f94bf00..afbb631f 100644 --- a/src/components/Catalog.tsx +++ b/src/components/Catalog.tsx @@ -3,7 +3,7 @@ import { generatePath, useNavigate } from 'react-router-dom'; import { AsyncThunkAction } from '@reduxjs/toolkit'; import { AppDispatch, PaginatedStateEntity, RootState, Status, useAppDispatch, useAppSelector } from 'store'; import { selectUserRoles } from 'store/features'; -import { Module, SubModule, Entity, PaginationParams, UserRole, ErrorResponseDTO, PaginatedResponse } from 'shared/types'; +import { Module, SubModule, Entity, PaginationParams, UserRole, ProblemDetailsModel, PaginatedResponse } from 'shared/types'; import { APP_CONFIG } from 'shared/constants'; import { getTestSelectorByModule } from 'shared/helpers'; import { useUnmount } from 'shared/hooks'; @@ -15,7 +15,7 @@ import { renderPrimaryLinkText } from 'components/UI/helpers/renderPrimaryLinkTe interface StateAction { state: RootState; - rejectValue: ErrorResponseDTO; + rejectValue: ProblemDetailsModel; dispatch?: AppDispatch; } diff --git a/src/components/Entity/EntityManager.tsx b/src/components/Entity/EntityManager.tsx index be00e561..3af6f95c 100644 --- a/src/components/Entity/EntityManager.tsx +++ b/src/components/Entity/EntityManager.tsx @@ -4,12 +4,12 @@ import { DefaultValues, FieldErrors, FieldValues } from 'react-hook-form'; import { AsyncThunkAction } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector, RootState, AppDispatch, StateEntity } from 'store'; import { useOnFormSubmitEffect, useSafeNavigateBack, useUnmount } from 'shared/hooks'; -import { BaseEntity, EntityInput, ErrorResponse, ErrorResponseDTO, Module, SubModule } from 'shared/types'; +import { BaseEntity, EntityInput, ErrorResponse, ProblemDetailsModel, Module, SubModule } from 'shared/types'; import { areEqualBigIds } from 'shared/helpers'; import PageLayout from 'components/layouts/PageLayout'; import Form, { FormActionName, FormFieldProps, FormProps } from 'components/UI/Form'; -type EntityManagerProps = { +type EntityManagerProps = { module: Module; subModule: SubModule; pageTitle: { create: string; edit: string }; @@ -24,15 +24,17 @@ type EntityManagerProps = { resetEntity: () => ReturnType; resetErrors: () => ReturnType; resetCatalogFetchStatus: () => ReturnType; - mapDTO: (value: T) => EntityInput; - createEntityAction: (args: EntityInput) => AsyncThunkAction, { rejectValue: ErrorResponse }>; + mapInput: (value: T) => EntityInput; + createEntityAction: ( + args: EntityInput + ) => AsyncThunkAction, { rejectValue: ErrorResponse }>; editEntityAction: ( - args: [string, EntityInput] - ) => AsyncThunkAction], { rejectValue: ErrorResponse }>; - fetchEntityAction: (id: string) => AsyncThunkAction; + args: [string, EntityInput] + ) => AsyncThunkAction], { rejectValue: ErrorResponse }>; + fetchEntityAction: (id: string) => AsyncThunkAction; }; -const EntityManager = ({ +const EntityManager = ({ module, subModule, pageTitle, @@ -50,8 +52,8 @@ const EntityManager = ({ fetchEntityAction, createEntityAction, editEntityAction, - mapDTO, -}: EntityManagerProps) => { + mapInput, +}: EntityManagerProps) => { const dispatch = useAppDispatch(); const { id } = useParams<{ id: string }>(); const { item: values, fetchStatus, updateStatus = 'idle' } = useAppSelector(selectEntity); @@ -71,12 +73,12 @@ const EntityManager = ({ }); const handleSubmit = (formValue: T) => { - const dto = mapDTO(formValue); + const input = mapInput(formValue); if (id) { - dispatch(editEntityAction([id, dto])); + dispatch(editEntityAction([id, input])); } else { - dispatch(createEntityAction(dto)); + dispatch(createEntityAction(input)); } setFormSubmitted(true); diff --git a/src/components/Entity/EntityViewer.tsx b/src/components/Entity/EntityViewer.tsx index c3c3e2a6..375c2fe4 100644 --- a/src/components/Entity/EntityViewer.tsx +++ b/src/components/Entity/EntityViewer.tsx @@ -4,7 +4,7 @@ import { AsyncThunkAction } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector, RootState, AppDispatch, StateEntity } from 'store'; import { selectUserRoles } from 'store/features'; import { areEqualBigIds, hasAllowedRole } from 'shared/helpers'; -import { Module, SubModule, ErrorResponseDTO, BaseEntity } from 'shared/types'; +import { Module, SubModule, ProblemDetailsModel, BaseEntity } from 'shared/types'; import PageLayout from 'components/layouts/PageLayout'; import ViewDetails, { ViewDetailActionName, ViewDetailProps } from 'components/UI/ViewDetails'; @@ -17,7 +17,7 @@ type EntityViewerProps = { editRoute?: string; selectEntity: (state: RootState) => StateEntity; resetEntity: () => ReturnType; - fetchEntityAction: (id: string) => AsyncThunkAction; + fetchEntityAction: (id: string) => AsyncThunkAction; }; const EntityViewer = ({ diff --git a/src/layout/MessageLayout.tsx b/src/layout/MessageLayout.tsx index 053d800e..802a2cae 100644 --- a/src/layout/MessageLayout.tsx +++ b/src/layout/MessageLayout.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useAppDispatch, useAppSelector } from 'store'; import { clearMessages, selectMessage } from 'store/features'; import { Module, SubModule } from 'shared/types'; -import { getTestSelectorByModule } from 'shared/helpers'; +import { getProblemTitle, getTestSelectorByModule } from 'shared/helpers'; import Snackbar from 'components/UI/Snackbar'; const MessageLayout: React.FC = () => { @@ -24,7 +24,7 @@ const MessageLayout: React.FC = () => { data-cy={testSelector} type={type} open={!!error || !!success} - message={error ? error.title : success} + message={error ? getProblemTitle(error) : success} onClose={handleSnackbarClose} /> ); diff --git a/src/pages/Manage/Branch/pages/BranchManagement.tsx b/src/pages/Manage/Branch/pages/BranchManagement.tsx index e7372708..9391725f 100644 --- a/src/pages/Manage/Branch/pages/BranchManagement.tsx +++ b/src/pages/Manage/Branch/pages/BranchManagement.tsx @@ -19,13 +19,14 @@ import { ROUTES, USER_PERMISSION_GENERAL_ERROR, } from 'shared/constants'; -import { Branch, BranchDTO, TimeZone } from 'shared/types'; +import { Branch, TimeZone } from 'shared/types'; import { getBranchManagementDetailsByAddressFormSchema, - mapBranchDTO, + mapBranchInput, mapDisabledFields, mapBranchFieldOptionsToFieldOptions, deepCopyObject, + getProblemErrors, } from 'shared/helpers'; import { FormFieldProps } from 'components/UI/Form'; import EntityManager from 'components/Entity/EntityManager'; @@ -44,7 +45,7 @@ const BranchManagementPage: React.FC = () => { const [noPhysicalAddress, setNoPhysicalAddress] = React.useState(undefined); const [fields, setFields] = React.useState[]>([]); const formLoading = fetchStatus === 'loading' || (!branch && !!id) || fields.length === 0; - const errors = isUserAdmin ? deepCopyObject(updateError?.errors) : USER_PERMISSION_GENERAL_ERROR; + const errors = isUserAdmin ? deepCopyObject(getProblemErrors(updateError)) : USER_PERMISSION_GENERAL_ERROR; React.useEffect(() => { if (!id || (id && branch && noPhysicalAddress !== undefined)) { @@ -70,7 +71,7 @@ const BranchManagementPage: React.FC = () => { }; return ( - + module={testModule} subModule={testSubModule} pageTitle={{ create: 'Create Branch', edit: 'Edit Branch' }} @@ -88,7 +89,7 @@ const BranchManagementPage: React.FC = () => { fetchEntityAction={(id) => fetchBranchById({ id, skipState: false })} createEntityAction={createBranch} editEntityAction={editBranch} - mapDTO={mapBranchDTO} + mapInput={mapBranchInput} /> ); }; diff --git a/src/pages/Manage/Company/pages/CompanyEdit.tsx b/src/pages/Manage/Company/pages/CompanyEdit.tsx index 785ca475..a7e8a497 100644 --- a/src/pages/Manage/Company/pages/CompanyEdit.tsx +++ b/src/pages/Manage/Company/pages/CompanyEdit.tsx @@ -16,8 +16,8 @@ import { ROUTES, USER_PERMISSION_GENERAL_ERROR, } from 'shared/constants'; -import { Company, CompanyDTO, EntityInput } from 'shared/types'; -import { deepCopyObject, mapCountriesToFieldOptions, mapDisabledFields } from 'shared/helpers'; +import { Company, EntityInput } from 'shared/types'; +import { deepCopyObject, getProblemErrors, mapCountriesToFieldOptions, mapDisabledFields } from 'shared/helpers'; import { useOnFormSubmitEffect } from 'shared/hooks'; import PageLayout from 'components/layouts/PageLayout'; import Form, { FormActionName } from 'components/UI/Form'; @@ -34,10 +34,10 @@ const CompanyEditPage: React.FC = () => { const { item: company, updateError: error, fetchStatus, updateStatus = 'idle' } = useAppSelector(selectCompany); const [formSubmitted, setFormSubmitted] = React.useState(false); const fields = mapCountriesToFieldOptions(mapDisabledFields(COMPANY_MANAGEMENT_DETAILS_FORM_SCHEMA.fields, userRoles), countries); - const errors = isUserAdmin ? deepCopyObject(error?.errors) : USER_PERMISSION_GENERAL_ERROR; + const errors = isUserAdmin ? deepCopyObject(getProblemErrors(error)) : USER_PERMISSION_GENERAL_ERROR; const navigateToViewCompany = () => navigate(ROUTES.viewCompany.path); - const handleSubmit = (data: EntityInput) => { + const handleSubmit = (data: EntityInput) => { dispatch(editCompany(data)); setFormSubmitted(true); }; diff --git a/src/pages/Manage/Company/pages/CompanySettings.tsx b/src/pages/Manage/Company/pages/CompanySettings.tsx index 73d686ca..930869fe 100644 --- a/src/pages/Manage/Company/pages/CompanySettings.tsx +++ b/src/pages/Manage/Company/pages/CompanySettings.tsx @@ -10,7 +10,7 @@ import { } from 'store/features'; import { editCompanySettings } from 'store/thunks'; import { COMPANY_SETTINGS_FIELDS, COMPANY_SETTINGS_MANAGEMENT_DETAILS_FORM_SCHEMA, ROUTES } from 'shared/constants'; -import { CompanySettings, CompanySettingsDTO, EntityInput, ThemeMode } from 'shared/types'; +import { CompanySettings, EntityInput, ThemeMode } from 'shared/types'; import { mapDisabledFields } from 'shared/helpers'; import { COLOR_SCHEMES } from 'shared/themes'; import { useUnmount } from 'shared/hooks'; @@ -29,11 +29,11 @@ const CompanySettingsPage: React.FC = () => { const { isDarkTheme } = useAppSelector(selectAppConfig); const mode: ThemeMode = isDarkTheme ? 'dark' : 'light'; - const handleSubmit = (data: EntityInput) => { + const handleSubmit = (data: EntityInput) => { dispatch(editCompanySettings({ ...companySettings, ...data })); }; - const handleChange = (data: CompanySettingsDTO) => { + const handleChange = (data: CompanySettings) => { if (data.colorSchemeId) { dispatch(setPreviewCompanyColorSchemeSettings(data.colorSchemeId)); } diff --git a/src/pages/Manage/Department/pages/DepartmentManagement.tsx b/src/pages/Manage/Department/pages/DepartmentManagement.tsx index 4b12137b..35065a37 100644 --- a/src/pages/Manage/Department/pages/DepartmentManagement.tsx +++ b/src/pages/Manage/Department/pages/DepartmentManagement.tsx @@ -23,8 +23,14 @@ import { ROUTES, USER_PERMISSION_GENERAL_ERROR, } from 'shared/constants'; -import { Department, DepartmentDTO, EmployeeDTO } from 'shared/types'; -import { mapDisabledFields, deepCopyObject, mapDepartmentDTO, mapDepartmentFieldOptionsToFieldOptions } from 'shared/helpers'; +import { Department } from 'shared/types'; +import { + mapDisabledFields, + deepCopyObject, + mapDepartmentInput, + mapDepartmentFieldOptionsToFieldOptions, + getProblemErrors, +} from 'shared/helpers'; import EntityManager from 'components/Entity/EntityManager'; const testModule = DEPARTMENT_MANAGEMENT_DETAILS_FORM_SCHEMA.module; @@ -47,7 +53,7 @@ const DepartmentManagementPage: React.FC = () => { } = useAppSelector(selectManagers); const parentDepartmentsLoading = parentDepartmentsFetchStatus === 'loading'; const managersLoading = managersFetchStatus === 'loading'; - const errors = isUserAdmin ? deepCopyObject(updateError?.errors) : USER_PERMISSION_GENERAL_ERROR; + const errors = isUserAdmin ? deepCopyObject(getProblemErrors(updateError)) : USER_PERMISSION_GENERAL_ERROR; const handleParentDepartmentsScrollEnd = () => { if (parentDepartmentsPage.pageNumber! < parentDepartmentsPage.totalPages!) { @@ -68,12 +74,12 @@ const DepartmentManagementPage: React.FC = () => { ); const parentDepartmentItems = department?.parentDepartmentId && !isParentDepartmentOptionAvailable - ? [{ id: department.parentDepartmentId, name: department.parentDepartmentName } as DepartmentDTO, ...parentDepartments] + ? [{ id: department.parentDepartmentId, name: department.parentDepartmentName ?? '' }, ...parentDepartments] : parentDepartments; const isManagerOptionAvailable = managers.some((managertItem) => String(managertItem.id) === String(department?.managerId)); const managerItems = department?.managerId && !isManagerOptionAvailable - ? [{ id: department.managerId, name: department.managerName } as unknown as EmployeeDTO, ...managers] + ? [{ id: department.managerId, fullName: department.managerName ?? '' }, ...managers] : managers; const disabledFields = mapDisabledFields(DEPARTMENT_MANAGEMENT_DETAILS_FORM_SCHEMA.fields, userRoles); const mappedFields = mapDepartmentFieldOptionsToFieldOptions(disabledFields, parentDepartmentItems, managerItems); @@ -110,7 +116,7 @@ const DepartmentManagementPage: React.FC = () => { }, [parentDepartmentsFetchStatus, parentDepartmentsPage, dispatch]); return ( - + module={testModule} subModule={testSubModule} pageTitle={{ create: 'Create Department', edit: 'Edit Department' }} @@ -127,7 +133,7 @@ const DepartmentManagementPage: React.FC = () => { fetchEntityAction={(id) => fetchDepartmentById({ id, skipState: false })} createEntityAction={createDepartment} editEntityAction={editDepartment} - mapDTO={mapDepartmentDTO} + mapInput={mapDepartmentInput} /> ); }; diff --git a/src/pages/Manage/Employee/pages/EmployeeEdit.tsx b/src/pages/Manage/Employee/pages/EmployeeEdit.tsx index 4bcdf784..cda1b5b4 100644 --- a/src/pages/Manage/Employee/pages/EmployeeEdit.tsx +++ b/src/pages/Manage/Employee/pages/EmployeeEdit.tsx @@ -20,14 +20,15 @@ import { } from 'store/features'; import { fetchAssignedBranches, fetchAssignedDepartments, editEmployee, fetchEmployeeById, fetchManagers } from 'store/thunks'; import { APP_CONFIG, EMPLOYEE_DETAILS_FORM_DEFAULT_VALUES, EMPLOYEE_DETAILS_FORM_SCHEMA, EMPLOYEE_FIELDS, ROUTES } from 'shared/constants'; -import { Branch, Department, Employee, EmployeeDTO, EntityInput } from 'shared/types'; +import { Employee, EntityInput } from 'shared/types'; import { areEqualBigIds, deepCopyObject, mapBranchToFieldOption, mapDepartmentToFieldOption, - mapEmployeeDTO, + mapEmployeeInput, mapEmployeeToFieldOption, + getProblemErrors, } from 'shared/helpers'; import { useOnFormSubmitEffect, useSafeNavigateBack } from 'shared/hooks'; import PageLayout from 'components/layouts/PageLayout'; @@ -59,7 +60,7 @@ const EmployeeEditPage: React.FC = () => { const safeNavigateBack = useSafeNavigateBack(ROUTES.employees.path); const [formSubmitted, setFormSubmitted] = React.useState(false); const defaultValues: EntityInput = employee || EMPLOYEE_DETAILS_FORM_DEFAULT_VALUES; - const errors = deepCopyObject(updateError?.errors); + const errors = deepCopyObject(getProblemErrors(updateError)); const handleAssignedBranchesScrollEnd = () => { if (assignedBranchesPage.pageNumber! < assignedBranchesPage.totalPages!) { @@ -93,7 +94,7 @@ const EmployeeEditPage: React.FC = () => { }; const handleSubmit = (formValue: Employee) => { - const submitData = mapEmployeeDTO(formValue); + const submitData = mapEmployeeInput(formValue); dispatch(editEmployee([id!, submitData])); setFormSubmitted(true); @@ -102,19 +103,19 @@ const EmployeeEditPage: React.FC = () => { const isBranchOptionAvailable = assignedBranches.some((branchItem) => String(branchItem.id) === String(employee?.assignedBranchId)); const branchItems = employee?.assignedBranchId && !isBranchOptionAvailable - ? [{ id: employee.assignedBranchId, name: employee.assignedBranchName } as Branch, ...assignedBranches] + ? [{ id: employee.assignedBranchId, name: employee.assignedBranchName ?? '' }, ...assignedBranches] : assignedBranches; const isDepartmentOptionAvailable = assignedDepartments.some( (departmentItem) => String(departmentItem.id) === String(employee?.assignedDepartmentId) ); const departmentItems = employee?.assignedDepartmentId && !isDepartmentOptionAvailable - ? [{ id: employee.assignedDepartmentId, name: employee.assignedDepartmentName } as Department, ...assignedDepartments] + ? [{ id: employee.assignedDepartmentId, name: employee.assignedDepartmentName ?? '' }, ...assignedDepartments] : assignedDepartments; const isManagerOptionAvailable = managers.some((managerItem) => String(managerItem.id) === String(employee?.reportsToId)); const managerItems = employee?.reportsToId && !isManagerOptionAvailable - ? [{ id: employee.reportsToId, name: employee.reportsToName } as unknown as EmployeeDTO, ...managers] + ? [{ id: employee.reportsToId, fullName: employee.reportsToName ?? '' }, ...managers] : managers; const fields = EMPLOYEE_DETAILS_FORM_SCHEMA.fields.map((field) => { diff --git a/src/pages/Manage/Offboarding/pages/Company/CompanyDelete.tsx b/src/pages/Manage/Offboarding/pages/Company/CompanyDelete.tsx index fb7e25f7..e0a642df 100644 --- a/src/pages/Manage/Offboarding/pages/Company/CompanyDelete.tsx +++ b/src/pages/Manage/Offboarding/pages/Company/CompanyDelete.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useAppDispatch, useAppSelector } from 'store'; import { selectCompany, selectIsUserAdmin, selectUserRoles } from 'store/features'; import { deleteCompany } from 'store/thunks'; -import { deepCopyObject, hasAllowedRole } from 'shared/helpers'; +import { deepCopyObject, getProblemErrors, hasAllowedRole } from 'shared/helpers'; import { DELETE_COMPANY_DETAILS_FORM_SCHEMA, USER_PERMISSION_GENERAL_ERROR } from 'shared/constants'; import { Company } from 'shared/types'; import Form, { FormActionName } from 'components/UI/Form'; @@ -16,7 +16,7 @@ const CompanyDeletePage: React.FC = () => { const isUserAdmin = useAppSelector(selectIsUserAdmin); const { deleteError: error, deleteStatus } = useAppSelector(selectCompany); const fields = DELETE_COMPANY_DETAILS_FORM_SCHEMA.fields; - const errors = isUserAdmin ? deepCopyObject(error?.errors) : USER_PERMISSION_GENERAL_ERROR; + const errors = isUserAdmin ? deepCopyObject(getProblemErrors(error)) : USER_PERMISSION_GENERAL_ERROR; const actions = DELETE_COMPANY_DETAILS_FORM_SCHEMA.actions.map((action) => { switch (action.name) { diff --git a/src/pages/Manage/Offboarding/pages/Company/CompanySettingsDelete.tsx b/src/pages/Manage/Offboarding/pages/Company/CompanySettingsDelete.tsx index f28f46ef..a0449fc5 100644 --- a/src/pages/Manage/Offboarding/pages/Company/CompanySettingsDelete.tsx +++ b/src/pages/Manage/Offboarding/pages/Company/CompanySettingsDelete.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { FieldErrors, FieldValues } from 'react-hook-form'; import { useAppDispatch, useAppSelector } from 'store'; import { selectCompanySettings, selectIsUserAdmin, selectUserRoles } from 'store/features'; import { deleteCompanySettings } from 'store/thunks'; -import { deepCopyObject, hasAllowedRole } from 'shared/helpers'; +import { createProblemDetails, deepCopyObject, hasAllowedRole, mapError } from 'shared/helpers'; import { DELETE_COMPANY_SETTINGS_DETAILS_FORM_SCHEMA, USER_PERMISSION_GENERAL_ERROR } from 'shared/constants'; import Form, { FormActionName } from 'components/UI/Form'; @@ -16,7 +15,9 @@ const CompanySettingsDeletePage: React.FC = () => { const isUserAdmin = useAppSelector(selectIsUserAdmin); const { fetchError: error, deleteStatus } = useAppSelector(selectCompanySettings); const fields = DELETE_COMPANY_SETTINGS_DETAILS_FORM_SCHEMA.fields; - const errors = isUserAdmin ? deepCopyObject(error?.errors as FieldErrors) : USER_PERMISSION_GENERAL_ERROR; + const errors = isUserAdmin + ? deepCopyObject(error ? mapError(createProblemDetails(error)).errors : undefined) + : USER_PERMISSION_GENERAL_ERROR; const actions = DELETE_COMPANY_SETTINGS_DETAILS_FORM_SCHEMA.actions.map((action) => { switch (action.name) { diff --git a/src/pages/Manage/Onboarding/pages/Company/BranchCreate.tsx b/src/pages/Manage/Onboarding/pages/Company/BranchCreate.tsx index 865b4e37..73a38f5d 100644 --- a/src/pages/Manage/Onboarding/pages/Company/BranchCreate.tsx +++ b/src/pages/Manage/Onboarding/pages/Company/BranchCreate.tsx @@ -12,10 +12,11 @@ import { createOnboardingBranch } from 'store/thunks'; import { Branch } from 'shared/types'; import { getBranchManagementDetailsByAddressFormSchema, - mapBranchDTO, + mapBranchInput, mapDisabledFields, mapBranchFieldOptionsToFieldOptions, deepCopyObject, + getProblemErrors, hasAllowedRole, } from 'shared/helpers'; import { CREATE_BRANCH_DETAILS_FORM_SCHEMA, BRANCH_DETAILS_FORM_DEFAULT_VALUES, USER_PERMISSION_GENERAL_ERROR } from 'shared/constants'; @@ -37,7 +38,7 @@ const BranchCreatePage: React.FC = () => { const schema = getBranchManagementDetailsByAddressFormSchema(CREATE_BRANCH_DETAILS_FORM_SCHEMA.fields, !!noPhysicalAddress); const disabledFields = mapDisabledFields(schema, userRoles); const fields = mapBranchFieldOptionsToFieldOptions(disabledFields, companyTimeZones, availableCountries); - const errors = isUserAdmin ? deepCopyObject(error?.errors) : USER_PERMISSION_GENERAL_ERROR; + const errors = isUserAdmin ? deepCopyObject(getProblemErrors(error)) : USER_PERMISSION_GENERAL_ERROR; const actions = CREATE_BRANCH_DETAILS_FORM_SCHEMA.actions.map((action) => action.name === FormActionName.submit @@ -46,7 +47,7 @@ const BranchCreatePage: React.FC = () => { ); const handleSubmit = (formValue: Branch) => { - const submitData = mapBranchDTO(formValue); + const submitData = mapBranchInput(formValue); dispatch(createOnboardingBranch(submitData)); }; diff --git a/src/pages/Manage/Onboarding/pages/Company/CompanyCreate.tsx b/src/pages/Manage/Onboarding/pages/Company/CompanyCreate.tsx index 1b77138c..585f79e4 100644 --- a/src/pages/Manage/Onboarding/pages/Company/CompanyCreate.tsx +++ b/src/pages/Manage/Onboarding/pages/Company/CompanyCreate.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { useAppDispatch, useAppSelector } from 'store'; import { selectCompany, selectIsUserAdmin, selectUserRoles, selectSystemCountries } from 'store/features'; import { createCompany } from 'store/thunks'; -import { Company, CompanyDTO } from 'shared/types'; -import { deepCopyObject, hasAllowedRole, mapCountriesToFieldOptions, mapDisabledFields } from 'shared/helpers'; +import { Company } from 'shared/types'; +import { deepCopyObject, getProblemErrors, hasAllowedRole, mapCountriesToFieldOptions, mapDisabledFields } from 'shared/helpers'; import { COMPANY_DETAILS_FORM_DEFAULT_VALUES, CREATE_COMPANY_DETAILS_FORM_SCHEMA, USER_PERMISSION_GENERAL_ERROR } from 'shared/constants'; import Form, { FormActionName } from 'components/UI/Form'; @@ -22,9 +22,9 @@ const CompanyCreatePage: React.FC = () => { ? { ...action, disabled: !hasAllowedRole(action.roles, userRoles), loading: updateStatus === 'loading' } : action ); - const errors = isUserAdmin ? deepCopyObject(error?.errors) : USER_PERMISSION_GENERAL_ERROR; + const errors = isUserAdmin ? deepCopyObject(getProblemErrors(error)) : USER_PERMISSION_GENERAL_ERROR; - const handleSubmit = (data: CompanyDTO) => { + const handleSubmit = (data: Company) => { dispatch(createCompany(data)); }; diff --git a/src/pages/Manage/Onboarding/pages/Company/CompanyLicenseUpload.tsx b/src/pages/Manage/Onboarding/pages/Company/CompanyLicenseUpload.tsx index ec6857ff..a1ae56a6 100644 --- a/src/pages/Manage/Onboarding/pages/Company/CompanyLicenseUpload.tsx +++ b/src/pages/Manage/Onboarding/pages/Company/CompanyLicenseUpload.tsx @@ -11,7 +11,7 @@ import { setCompanyLicenseSkipped, resetCompanyLicenseSkipped, } from 'store/features'; -import { mapDisabledFields, hasAllowedRole, deepCopyObject } from 'shared/helpers'; +import { mapDisabledFields, hasAllowedRole, deepCopyObject, getProblemErrors } from 'shared/helpers'; import { ROUTES, UPLOAD_COMPANY_LICENSE_DETAILS_FORM_SCHEMA, USER_PERMISSION_GENERAL_ERROR } from 'shared/constants'; import Form, { FormActionName } from 'components/UI/Form'; import { renderCopyableField } from 'components/UI/helpers/renderCopyableField'; @@ -27,7 +27,7 @@ const CompanyLicenseUploadPage: React.FC = () => { const { updateStatus, updateError: error } = useAppSelector(selectCompanyLicense); const { item: company } = useAppSelector(selectCompany); const skipRef = React.useRef(false); - const errors = isUserAdmin ? deepCopyObject(error?.errors) : USER_PERMISSION_GENERAL_ERROR; + const errors = isUserAdmin ? deepCopyObject(getProblemErrors(error)) : USER_PERMISSION_GENERAL_ERROR; React.useEffect(() => { return () => { diff --git a/src/pages/Manage/Onboarding/pages/Company/CompanySettingsCreate.tsx b/src/pages/Manage/Onboarding/pages/Company/CompanySettingsCreate.tsx index 3670cf30..a057abb0 100644 --- a/src/pages/Manage/Onboarding/pages/Company/CompanySettingsCreate.tsx +++ b/src/pages/Manage/Onboarding/pages/Company/CompanySettingsCreate.tsx @@ -15,8 +15,8 @@ import { CREATE_COMPANY_SETTINGS_DETAILS_FORM_SCHEMA, USER_PERMISSION_GENERAL_ERROR, } from 'shared/constants'; -import { CompanySettings, CompanySettingsDTO, EntityInput, ThemeMode } from 'shared/types'; -import { deepCopyObject, hasAllowedRole, mapDisabledFields } from 'shared/helpers'; +import { CompanySettings, EntityInput, ThemeMode } from 'shared/types'; +import { deepCopyObject, getProblemErrors, hasAllowedRole, mapDisabledFields } from 'shared/helpers'; import { COLOR_SCHEMES } from 'shared/themes'; import { useUnmount } from 'shared/hooks'; import PageLayout from 'components/layouts/PageLayout'; @@ -33,7 +33,7 @@ const CompanySettingsCreatePage: React.FC = () => { const isUserAdmin = useAppSelector(selectIsUserAdmin); const mode: ThemeMode = isDarkTheme ? 'dark' : 'light'; const filteredSchemes = Object.fromEntries(Object.entries(COLOR_SCHEMES).filter(([, scheme]) => scheme[mode])); - const errors = isUserAdmin ? deepCopyObject(error?.errors) : USER_PERMISSION_GENERAL_ERROR; + const errors = isUserAdmin ? deepCopyObject(getProblemErrors(error)) : USER_PERMISSION_GENERAL_ERROR; const fields = mapDisabledFields(CREATE_COMPANY_SETTINGS_DETAILS_FORM_SCHEMA.fields, userRoles).map((field) => { if (field.name === COMPANY_SETTINGS_FIELDS.colorSchemeId!.field) { @@ -56,11 +56,11 @@ const CompanySettingsCreatePage: React.FC = () => { dispatch(resetPreviewCompanyColorSchemeSettings()); }); - const handleSubmit = (data: EntityInput) => { + const handleSubmit = (data: EntityInput) => { dispatch(createCompanySettings(data)); }; - const handleChange = (data: EntityInput) => { + const handleChange = (data: EntityInput) => { const { colorSchemeId } = data; if (colorSchemeId) { dispatch(setPreviewCompanyColorSchemeSettings(colorSchemeId)); diff --git a/src/pages/Manage/Onboarding/pages/Employee/EmployeeCreate.tsx b/src/pages/Manage/Onboarding/pages/Employee/EmployeeCreate.tsx index 701644d3..5cf8277a 100644 --- a/src/pages/Manage/Onboarding/pages/Employee/EmployeeCreate.tsx +++ b/src/pages/Manage/Onboarding/pages/Employee/EmployeeCreate.tsx @@ -4,7 +4,7 @@ import { selectProfile } from 'store/features'; import { createProfile } from 'store/thunks'; import { Employee } from 'shared/types'; import { EMPLOYEE_DETAILS_FORM_DEFAULT_VALUES, CREATE_EMPLOYEE_DETAILS_FORM_SCHEMA } from 'shared/constants'; -import { deepCopyObject, mapProfileDTO } from 'shared/helpers'; +import { deepCopyObject, getProblemErrors, mapProfileInput } from 'shared/helpers'; import Form, { FormActionName } from 'components/UI/Form'; const testModule = CREATE_EMPLOYEE_DETAILS_FORM_SCHEMA.module; @@ -13,14 +13,14 @@ const testSubModule = CREATE_EMPLOYEE_DETAILS_FORM_SCHEMA.subModule; const EmployeeCreatePage: React.FC = () => { const dispatch = useAppDispatch(); const { item: profile, updateStatus, updateError: error } = useAppSelector(selectProfile); - const errors = deepCopyObject(error?.errors); + const errors = deepCopyObject(getProblemErrors(error)); const actions = CREATE_EMPLOYEE_DETAILS_FORM_SCHEMA.actions.map((action) => action.name === FormActionName.submit ? { ...action, loading: updateStatus === 'loading' } : action ); const handleSubmit = (formValue: Employee) => { - const submitData = mapProfileDTO(formValue); + const submitData = mapProfileInput(formValue); dispatch(createProfile(submitData)); }; diff --git a/src/pages/Manage/Profile/pages/EditProfile.tsx b/src/pages/Manage/Profile/pages/EditProfile.tsx index 56e437bb..aa482f17 100644 --- a/src/pages/Manage/Profile/pages/EditProfile.tsx +++ b/src/pages/Manage/Profile/pages/EditProfile.tsx @@ -5,7 +5,7 @@ import { resetProfileFetchStatus, selectProfile } from 'store/features'; import { editProfile } from 'store/thunks'; import { EMPLOYEE_DETAILS_FORM_DEFAULT_VALUES, PROFILE_DETAILS_FORM_SCHEMA, ROUTES } from 'shared/constants'; import { Employee, EntityInput } from 'shared/types'; -import { deepCopyObject, mapProfileDTO } from 'shared/helpers'; +import { deepCopyObject, getProblemErrors, mapProfileInput } from 'shared/helpers'; import { useOnFormSubmitEffect } from 'shared/hooks'; import PageLayout from 'components/layouts/PageLayout'; import Form, { FormActionName } from 'components/UI/Form'; @@ -19,7 +19,7 @@ const EditProfilePage: React.FC = () => { const { item: profile, updateError: error, updateStatus = 'idle' } = useAppSelector(selectProfile); const [formSubmitted, setFormSubmitted] = React.useState(false); const defaultValues: EntityInput = profile || EMPLOYEE_DETAILS_FORM_DEFAULT_VALUES; - const errors = deepCopyObject(error?.errors); + const errors = deepCopyObject(getProblemErrors(error)); const navigateToViewProfile = () => { navigate(ROUTES.viewProfile.path); @@ -47,7 +47,7 @@ const EditProfilePage: React.FC = () => { }); const handleSubmit = (formValue: EntityInput) => { - const submitData = mapProfileDTO(formValue); + const submitData = mapProfileInput(formValue); dispatch(editProfile(submitData)); setFormSubmitted(true); diff --git a/src/pages/Root.tsx b/src/pages/Root.tsx index 10c599a3..2a6b494e 100644 --- a/src/pages/Root.tsx +++ b/src/pages/Root.tsx @@ -9,7 +9,6 @@ import { useAppTheme } from 'shared/hooks'; import { darkScrollbar, getTestSelectorByModule, lightScrollbar } from 'shared/helpers'; import { Module, SubModule } from 'shared/types'; import MessageLayout from 'layout/MessageLayout'; -import AxiosInterceptor from '../AxiosInterceptor'; import ClientLoader from '../ClientLoader'; const RootPage: React.FC = () => { @@ -23,20 +22,18 @@ const RootPage: React.FC = () => { }); return ( - - - - - - - - - + + + + + + + ); }; diff --git a/src/shared/configs/BridgeClients.ts b/src/shared/configs/BridgeClients.ts new file mode 100644 index 00000000..68e482b0 --- /dev/null +++ b/src/shared/configs/BridgeClients.ts @@ -0,0 +1,25 @@ +import { HttpTransport_$ctor_3A0BE0FB } from '@fossa-app/bridge/Services/HttpTransport'; +import { JsonSerializer_$ctor } from '@fossa-app/bridge/Services/JsonSerializer'; +import { Clients_$ctor_Z7C557C0 } from '@fossa-app/bridge/Services/Clients'; +import { IClients } from '@fossa-app/bridge/Services/IClients'; + +import { AppAccessTokenProvider, FetchHttpRequestSender } from './BridgeTransport'; + +// Initialize the shared Fable HttpTransport +const sender = new FetchHttpRequestSender(); +const serializer = JsonSerializer_$ctor(); +const tokenProvider = new AppAccessTokenProvider(); + +export const httpTransport = HttpTransport_$ctor_3A0BE0FB(sender, serializer, tokenProvider); + +export const clients: IClients = Clients_$ctor_Z7C557C0(httpTransport); + +// Export strongly typed Fable clients +export const branchClient = clients.BranchClient; +export const companyClient = clients.CompanyClient; +export const companySettingsClient = clients.CompanySettingsClient; +export const departmentClient = clients.DepartmentClient; +export const employeeClient = clients.EmployeeClient; +export const identityClient = clients.IdentityClient; +export const systemLicenseClient = clients.SystemLicenseClient; +export const companyLicenseClient = clients.CompanyLicenseClient; diff --git a/src/shared/configs/BridgeResponses.ts b/src/shared/configs/BridgeResponses.ts new file mode 100644 index 00000000..e924aa96 --- /dev/null +++ b/src/shared/configs/BridgeResponses.ts @@ -0,0 +1,79 @@ +import type { ClientResult$1_$union } from '@fossa-app/bridge/Models/ApiModels/ClientResults'; +import type { ClientUnitResult_$union } from '@fossa-app/bridge/Models/ApiModels/ClientUnitResults'; +import type { PagingResponseModel$1 } from '@fossa-app/bridge/Models/ApiModels/EnvelopeModels'; +import { + matchClientResult as matchBridgeClientResult, + matchClientUnitResult as matchBridgeClientUnitResult, + unwrapClientResult, + unwrapClientUnitResult, +} from '@fossa-app/bridge/Models/ApiModels/Helpers/ClientResultHelpers'; +import type { ProblemDetailsModel, PaginatedResponse } from 'shared/types'; +import { createProblemDetails } from 'shared/helpers/error.helpers'; + +export const matchClientResult = ( + result: unknown, + onSuccess: (value: any) => TSuccess, + onFailure: (problem: ProblemDetailsModel) => TFailure +): TSuccess | TFailure => { + return matchBridgeClientResult(result as ClientResult$1_$union, onSuccess, onFailure); +}; + +export const matchClientUnitResult = ( + result: unknown, + onSuccess: () => TSuccess, + onFailure: (problem: ProblemDetailsModel) => TFailure +): TSuccess | TFailure => { + return matchBridgeClientUnitResult(result as ClientUnitResult_$union, onSuccess, onFailure); +}; + +const toNumber = (value: unknown): number | undefined => { + if (value === null || value === undefined) { + return undefined; + } + + return Number(value); +}; + +export const unwrapBridgeValue = (result: unknown): T => { + return matchClientResult( + result as ClientResult$1_$union, + (value) => value, + (problem) => { + throw createProblemDetails(problem); + } + ); +}; + +export const unwrapBridgeUnitResult = (result: unknown): void => { + matchClientUnitResult( + result as ClientUnitResult_$union, + () => undefined, + (problem) => { + throw createProblemDetails(problem); + } + ); +}; + +export const toPaginatedResponse = (value: unknown): PaginatedResponse => { + const response = value as PagingResponseModel$1; + const items = [...(response.Items ?? [])] as T[]; + + return { + pageNumber: toNumber(response.PageNumber), + pageSize: toNumber(response.PageSize), + totalItems: toNumber(response.TotalItems), + totalPages: toNumber(response.TotalPages), + items, + }; +}; + +export const unwrapBridgePagingResponse = (result: unknown): PaginatedResponse => { + return toPaginatedResponse(unwrapBridgeValue>(result)); +}; + +export const createBridgeProblem = (problem: ProblemDetailsModel, overrides?: Partial): ProblemDetailsModel => { + return createProblemDetails(problem, overrides); +}; + +export const toBridgeValue = unwrapClientResult; +export const toBridgeUnit = unwrapClientUnitResult; diff --git a/src/shared/configs/BridgeTransport.ts b/src/shared/configs/BridgeTransport.ts new file mode 100644 index 00000000..95a5936b --- /dev/null +++ b/src/shared/configs/BridgeTransport.ts @@ -0,0 +1,114 @@ +import { IAccessTokenProvider } from '@fossa-app/bridge/Services/IAccessTokenProvider'; +import { IHttpRequestSender, HttpRequestMessage, HttpResponseMessage } from '@fossa-app/bridge/Services/IHttpRequestSender'; +import { unwrap } from '@fable-org/fable-library-ts/Option'; +import { createProblemDetails, getUserManager } from 'shared/helpers'; +import { MESSAGES } from 'shared/constants'; +import store from 'store'; +import { setError, removeUser } from 'store/features'; +import { getBackendOrigin } from '@fossa-app/bridge/Services/UrlHelpers'; + +export class AppAccessTokenProvider implements IAccessTokenProvider { + async GetTokenAsync(_cancellationToken: AbortSignal): Promise { + const userManager = getUserManager(); + let user = await userManager.getUser(); + + if (!user || user.expired) { + try { + user = await userManager.signinSilent(); + } catch { + return ''; + } + } + + return user?.access_token || ''; + } +} + +export class FetchHttpRequestSender implements IHttpRequestSender { + async SendAsync(request: HttpRequestMessage, cancellationToken: AbortSignal): Promise { + const methodMap: Record = { + 0: 'GET', + 1: 'POST', + 2: 'PUT', + 3: 'PATCH', + 4: 'DELETE', + }; + + const method = methodMap[request.Method.tag]; + + const beOrigin = getBackendOrigin(window.location.origin); + const url = `${beOrigin}/${request.Uri.replace(/^\//, '')}`; + + const body = unwrap(request.Content); + + const headers = new Headers(); + if (request.Headers) { + const headersArray = Array.from(request.Headers); + headersArray.forEach(([key, value]) => { + headers.set(key, value); + }); + } + + // Default fetch config + const init: RequestInit = { + method, + headers, + signal: cancellationToken, + }; + + if (body !== undefined && body !== null) { + init.body = body as string; + } + + let response: Response; + try { + response = await fetch(url, init); + } catch (_error) { + const problem = createProblemDetails(undefined, { Status: 599, Title: MESSAGES.error.general.network }); + + store.dispatch(setError(problem)); + + return new HttpResponseMessage(problem.Status, JSON.stringify(problem)); + } + + if (response.status === 401) { + const userManager = getUserManager(); + try { + await userManager.removeUser(); + } catch { + // Ignored + } + + store.dispatch(removeUser()); + const problem = createProblemDetails(undefined, { Status: 401, Title: MESSAGES.error.general.unAuthorized }); + store.dispatch(setError(problem)); + + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } + + return new HttpResponseMessage(problem.Status, JSON.stringify(problem)); + } + + if (!response.ok) { + let data = {}; + try { + data = await response.json(); + } catch { + // Ignored + } + + if (response.status >= 500) { + const problem = createProblemDetails(data, { Status: response.status, Title: MESSAGES.error.general.common }); + store.dispatch(setError(problem)); + + return new HttpResponseMessage(problem.Status, JSON.stringify(problem)); + } + + return new HttpResponseMessage(response.status, JSON.stringify(data)); + } + + const text = await response.text(); + return new HttpResponseMessage(response.status, text); + } +} diff --git a/src/shared/configs/axios.ts b/src/shared/configs/axios.ts deleted file mode 100644 index 527b3f22..00000000 --- a/src/shared/configs/axios.ts +++ /dev/null @@ -1,21 +0,0 @@ -import axios, { CreateAxiosDefaults } from 'axios'; -import { APP_CONFIG, ENDPOINTS } from 'shared/constants'; -import { getBackendOrigin } from 'shared/helpers'; - -const origin = window.location.origin; -const beOrigin = getBackendOrigin(origin); - -const defaultConfigs: CreateAxiosDefaults = { - baseURL: `${beOrigin}/${ENDPOINTS.base}`, - timeout: APP_CONFIG.httpTimeout, - headers: { - 'Content-Type': 'application/json', - }, - // BE sends big numbers in the response, parsing manually in AxiosInterceptor - transformResponse: [(response) => response], -}; - -const instance = axios.create(defaultConfigs); - -export * from 'axios'; -export default instance; diff --git a/src/shared/constants/configs.ts b/src/shared/constants/configs.ts index 5490d34d..89afd1fd 100644 --- a/src/shared/constants/configs.ts +++ b/src/shared/constants/configs.ts @@ -1,6 +1,8 @@ import { OidcClientSettings } from 'oidc-client-ts'; import { FieldErrors, FieldValues } from 'react-hook-form'; -import { ColorSchemeId, CompanySettings, EntityInput, PaginationParams } from 'shared/types'; +import type { CompanySettings } from 'shared/types/company-settings'; +import type { EntityInput } from 'shared/types/common'; +import type { ColorSchemeId } from 'shared/types/theme'; export const OIDC_INITIAL_CONFIG: OidcClientSettings = { authority: 'http://localhost:9011', @@ -15,7 +17,7 @@ export const APP_CONFIG = { snackbarAutoHideDuration: 10000, httpTimeout: 15000, table: { - defaultPagination: { pageNumber: 1, pageSize: 10, search: '', totalItems: undefined } as PaginationParams, + defaultPagination: { pageNumber: 1, pageSize: 10, search: '', totalItems: undefined, totalPages: undefined }, defaultPageSizeOptions: [10, 20, 50], containerWidth: 'calc(100vw - 32px)', containerMaxHeight: 'calc(100% - 70px)', diff --git a/src/shared/constants/endpoints.ts b/src/shared/constants/endpoints.ts deleted file mode 100644 index 728d89ae..00000000 --- a/src/shared/constants/endpoints.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const ENDPOINTS = { - base: 'api/1', - client: 'Identity/Client', - systemLicense: 'License/System', - companyLicense: 'License/Company', - company: 'Company', - companySettings: 'CompanySettings', - branches: 'Branches', - departments: 'Departments', - employee: 'Employee', - employees: 'Employees', -}; diff --git a/src/shared/constants/index.ts b/src/shared/constants/index.ts index 05cf5035..f87b0ffa 100644 --- a/src/shared/constants/index.ts +++ b/src/shared/constants/index.ts @@ -1,4 +1,3 @@ -export * from './endpoints'; export * from './routes'; export * from './configs'; export * from './flows'; diff --git a/src/shared/constants/routes.ts b/src/shared/constants/routes.ts index 37a48d17..54516636 100644 --- a/src/shared/constants/routes.ts +++ b/src/shared/constants/routes.ts @@ -1,4 +1,4 @@ -import { AppRoute } from 'shared/types'; +import type { AppRoute } from 'shared/types/route'; // TODO: use relative routes instead export const ROUTES: AppRoute = { diff --git a/src/shared/helpers/branch.helpers.ts b/src/shared/helpers/branch.helpers.ts index f55f0824..c3b85383 100644 --- a/src/shared/helpers/branch.helpers.ts +++ b/src/shared/helpers/branch.helpers.ts @@ -1,5 +1,5 @@ import { BRANCH_FIELDS } from 'shared/constants'; -import { Branch, BranchDTO, EntityInput, Company, Country, GeoAddress, TimeZone } from 'shared/types'; +import { Branch, EntityInput, Company, Country, GeoAddress, TimeZone } from 'shared/types'; import { FormFieldProps, FieldOption } from 'components/UI/Form'; import { mapCountryToFieldOption } from './company.helpers'; @@ -10,7 +10,7 @@ export const mapBranch = ({ countries, geoAddress, }: { - branch: BranchDTO; + branch: Branch; timeZones: TimeZone[]; companyCountryCode: Company['countryCode']; countries: Country[]; @@ -37,7 +37,7 @@ export const mapBranch = ({ }; }; -export const mapBranchDTO = (branch: Branch): EntityInput => { +export const mapBranchInput = (branch: Branch): EntityInput => { if (branch.noPhysicalAddress) { return { name: branch.name, @@ -71,7 +71,7 @@ export const mapBranchDTO = (branch: Branch): EntityInput => { }; export const mapBranches = ( - branches: BranchDTO[], + branches: Branch[], timeZones: TimeZone[], companyCountryCode: Company['countryCode'], countries: Country[] @@ -111,7 +111,7 @@ export const mapTimeZoneToFieldOption = (timeZone: TimeZone): FieldOption => { }; }; -export const mapBranchToFieldOption = (branch: Branch): FieldOption => { +export const mapBranchToFieldOption = (branch: Pick): FieldOption => { return { label: branch.name, value: String(branch?.id), diff --git a/src/shared/helpers/company.helpers.ts b/src/shared/helpers/company.helpers.ts index afa46083..a4369ac5 100644 --- a/src/shared/helpers/company.helpers.ts +++ b/src/shared/helpers/company.helpers.ts @@ -1,8 +1,8 @@ -import { Company, CompanyDTO, Country } from 'shared/types'; +import { Company, Country } from 'shared/types'; import { COMPANY_LICENSE_FIELDS } from 'shared/constants'; import { FormFieldProps, FormFieldType, FieldOption } from 'components/UI/Form'; -export const mapCompany = (company: CompanyDTO, countries: Country[]): Company => { +export const mapCompany = (company: Company, countries: Country[]): Company => { return { ...company, countryName: countries.find(({ code }) => code === company.countryCode)?.name, diff --git a/src/shared/helpers/data.helpers.ts b/src/shared/helpers/data.helpers.ts index d5927bb5..a170b1d2 100644 --- a/src/shared/helpers/data.helpers.ts +++ b/src/shared/helpers/data.helpers.ts @@ -1,3 +1,5 @@ +import { JsonSerializer } from '@fossa-app/bridge/Services/JsonSerializer'; + export const parseResponse = (response: any): T => { if (response.data === '') { return response as T; @@ -8,20 +10,10 @@ export const parseResponse = (response: any): T => { } if (typeof response.data === 'string') { - const isBigNumber = (num: string | number): boolean => { - const n = Number(num); - return !isNaN(n) && !Number.isSafeInteger(n); - }; - - const enquoteBigNumber = (jsonString: string, bigNumChecker: (num: string | number) => boolean): string => - jsonString.replace(/(:\s*)(\d{15,})(\s*[,}])/g, (match, prefix, numberPart, suffix) => - bigNumChecker(numberPart) ? `${prefix}"${numberPart}"${suffix}` : match - ); - try { return { ...response, - data: JSON.parse(enquoteBigNumber(response.data, isBigNumber)), + data: new JsonSerializer().Deserialize(response.data), } as T; } catch (error) { console.error('Error parsing response data:', error); diff --git a/src/shared/helpers/department.helpers.ts b/src/shared/helpers/department.helpers.ts index 17f37a82..41957e8f 100644 --- a/src/shared/helpers/department.helpers.ts +++ b/src/shared/helpers/department.helpers.ts @@ -1,9 +1,9 @@ import { DEPARTMENT_FIELDS } from 'shared/constants'; -import { Department, DepartmentDTO, Employee, EmployeeDTO, EntityInput } from 'shared/types'; +import { Department, Employee, EntityInput } from 'shared/types'; import { FormFieldProps, FieldOption } from 'components/UI/Form'; import { mapEmployeeToFieldOption } from './employee.helpers'; -export const mapDepartment = (department: DepartmentDTO, parentDepartment?: DepartmentDTO, employee?: EmployeeDTO): Department => { +export const mapDepartment = (department: Department, parentDepartment?: Department, employee?: Employee): Department => { const managerName = employee?.fullName; return { @@ -13,7 +13,7 @@ export const mapDepartment = (department: DepartmentDTO, parentDepartment?: Depa }; }; -export const mapDepartmentDTO = (department: Department): EntityInput => { +export const mapDepartmentInput = (department: Department): EntityInput => { return { name: department.name, parentDepartmentId: department.parentDepartmentId || null, @@ -22,9 +22,9 @@ export const mapDepartmentDTO = (department: Department): EntityInput { return departments.map((department) => { const manager = employees.find(({ id }) => id === department.managerId); @@ -36,8 +36,8 @@ export const mapDepartments = ( export const mapDepartmentFieldOptionsToFieldOptions = ( fields: FormFieldProps[], - departments?: Department[], - employees?: Employee[] + departments?: Pick[], + employees?: Pick[] ): FormFieldProps[] => { return fields.map((field) => ({ ...field, @@ -52,7 +52,7 @@ export const mapDepartmentFieldOptionsToFieldOptions = ( })); }; -export const mapDepartmentToFieldOption = (department: Department): FieldOption => { +export const mapDepartmentToFieldOption = (department: Pick): FieldOption => { return { label: department.name, value: String(department.id), diff --git a/src/shared/helpers/employee.helpers.ts b/src/shared/helpers/employee.helpers.ts index dc6ef447..df360e12 100644 --- a/src/shared/helpers/employee.helpers.ts +++ b/src/shared/helpers/employee.helpers.ts @@ -1,4 +1,4 @@ -import { AppUser, Branch, BranchDTO, Department, DepartmentDTO, Employee, EmployeeDTO, EntityInput } from 'shared/types'; +import { AppUser, Branch, Department, Employee, EntityInput } from 'shared/types'; import { FieldOption } from 'components/UI/Form'; import { mapUserProfileToEmployee } from './user.helpers'; @@ -9,7 +9,7 @@ export const mapEmployee = ({ department, manager, }: { - employee: EmployeeDTO; + employee: Employee; user?: AppUser; branch?: Branch; department?: Department; @@ -34,10 +34,10 @@ export const mapEmployees = ({ departments = [], managers = [], }: { - employees: EmployeeDTO[]; - branches?: BranchDTO[]; - departments?: DepartmentDTO[]; - managers?: EmployeeDTO[]; + employees: Employee[]; + branches?: Branch[]; + departments?: Department[]; + managers?: Employee[]; }): Employee[] => { return employees.map((employee) => { const branch = branches.find(({ id }) => id === employee.assignedBranchId); @@ -48,7 +48,7 @@ export const mapEmployees = ({ }); }; -export const mapEmployeeDTO = (employee: Employee): Omit, 'firstName' | 'lastName' | 'fullName'> => { +export const mapEmployeeInput = (employee: Employee): Omit, 'firstName' | 'lastName' | 'fullName'> => { return { jobTitle: employee.jobTitle, assignedBranchId: employee.assignedBranchId || null, @@ -57,7 +57,7 @@ export const mapEmployeeDTO = (employee: Employee): Omit): EntityInput => { +export const mapProfileInput = (employee: EntityInput): EntityInput => { return { firstName: employee.firstName, lastName: employee.lastName, @@ -69,7 +69,7 @@ export const mapProfileDTO = (employee: EntityInput): EntityInput { +export const mapEmployeeToFieldOption = (employee: Pick): FieldOption => { return { label: employee.fullName, value: String(employee.id), diff --git a/src/shared/helpers/error.helpers.ts b/src/shared/helpers/error.helpers.ts index 05748faf..d0cf6338 100644 --- a/src/shared/helpers/error.helpers.ts +++ b/src/shared/helpers/error.helpers.ts @@ -1,15 +1,83 @@ import { FieldErrors, FieldValues } from 'react-hook-form'; -import { ErrorResponseDTO, ErrorResponse } from 'shared/types'; +import { ProblemDetailsModel as BridgeProblemDetailsModel } from '@fossa-app/bridge/Models/ApiModels/SharedModels'; +import type { ProblemDetailsModel, ErrorResponse } from 'shared/types'; import { FormFieldProps } from 'components/UI/Form'; -export const mapError = (error: ErrorResponseDTO): ErrorResponse => { +type ProblemDetailsPatch = Partial>; + +const asRecord = (value: unknown): Record => { + return value && typeof value === 'object' ? (value as Record) : {}; +}; + +const read = ( + value: unknown, + pascalName: string, + camelName = `${pascalName.charAt(0).toLowerCase()}${pascalName.slice(1)}` +): T | undefined => { + const record = asRecord(value); + + return (record[camelName] ?? record[pascalName]) as T | undefined; +}; + +export const getProblemTitle = (problem: unknown): string | undefined => { + return read(problem, 'Title'); +}; + +export const getProblemStatus = (problem: unknown): number | undefined => { + const status = read(problem, 'Status'); + + return status === null || status === undefined ? undefined : Number(status); +}; + +export const getProblemErrors = (problem: unknown): Record | undefined => { + const errors = read(problem, 'Errors'); + + if (!errors) { + return undefined; + } + + if (errors instanceof Map) { + return Object.fromEntries(errors.entries()) as Record; + } + + return typeof errors === 'object' ? (errors as Record) : undefined; +}; + +const toProblemErrorsMap = (errors: unknown): Map => { + if (errors instanceof Map) { + return new Map(errors.entries()) as Map; + } + + if (!errors || typeof errors !== 'object') { + return new Map(); + } + + return new Map(Object.entries(errors as Record)); +}; + +export const createProblemDetails = (problem?: unknown, overrides: ProblemDetailsPatch = {}): ProblemDetailsModel => { + const status = read(overrides, 'Status') ?? read(problem, 'Status'); + + return new BridgeProblemDetailsModel( + read(overrides, 'Type') ?? read(problem, 'Type') ?? null, + read(overrides, 'Title') ?? read(problem, 'Title') ?? null, + status === null || status === undefined ? 0 : Number(status), + read(overrides, 'Detail') ?? read(problem, 'Detail') ?? null, + read(overrides, 'Instance') ?? read(problem, 'Instance') ?? null, + toProblemErrorsMap(read(overrides, 'Errors') ?? read(problem, 'Errors')), + read(overrides, 'TraceId') ?? read(problem, 'TraceId') ?? null + ); +}; + +export const mapError = (error: ProblemDetailsModel): ErrorResponse => { const errors: FieldErrors = {} as FieldErrors; + const problemErrors = getProblemErrors(error); - if (!error.errors) { + if (!problemErrors) { return { ...error, errors }; } - Object.entries(error.errors).forEach(([key, messages]) => { + Object.entries(problemErrors).forEach(([key, messages]) => { const pathParts = key.split('.').map((part) => part.charAt(0).toLowerCase() + part.slice(1)); let current = errors as FieldErrors; diff --git a/src/shared/helpers/url.helpers.ts b/src/shared/helpers/url.helpers.ts index c7838a5e..fee3c86f 100644 --- a/src/shared/helpers/url.helpers.ts +++ b/src/shared/helpers/url.helpers.ts @@ -1,20 +1,3 @@ -export const getBackendOrigin = (frontendOrigin: string): string => { - const suffixMappings = new Map([ - ['.dev.localhost:4211', '.dev.localhost:5210'], - ['.test.localhost:4210', '.test.localhost:5211'], - ['.test.localhost:4211', '.test.localhost:5211'], - ['.localhost:4210', '.localhost:5210'], - ]); - - for (const [frontendSuffix, backendSuffix] of suffixMappings) { - if (frontendOrigin.endsWith(frontendSuffix)) { - return `${frontendOrigin.slice(0, frontendOrigin.indexOf(frontendSuffix))}${backendSuffix}`; - } - } - - return frontendOrigin; -}; - export const prepareQueryParams = (params: Record): string => { const filtered = Object.fromEntries(Object.entries(params).filter(([, value]) => value !== null && value !== undefined && value !== '')); diff --git a/src/shared/helpers/user.helpers.ts b/src/shared/helpers/user.helpers.ts index b98fbe4d..18243910 100644 --- a/src/shared/helpers/user.helpers.ts +++ b/src/shared/helpers/user.helpers.ts @@ -13,7 +13,7 @@ export const mapUserProfileToEmployee = (userProfile?: UserProfile): Employee | } return { - id: userProfile.sub as unknown as number, + id: Number(userProfile.sub), firstName: userProfile.given_name!, lastName: userProfile.family_name!, fullName: userProfile.name!, diff --git a/src/shared/types/branch.ts b/src/shared/types/branch.ts index a89bce51..36e82f22 100644 --- a/src/shared/types/branch.ts +++ b/src/shared/types/branch.ts @@ -1,31 +1,24 @@ import { Country, FlattenField, GeoAddress, NonNullableFields, TimeZone } from './common'; -export interface AddressDTO { +export interface Address { line1?: string; line2?: string; city?: string; subdivision?: string; postalCode?: string; countryCode?: Country['code']; -} - -export interface Address extends AddressDTO { countryName?: string; } -export interface BranchDTO { +export interface Branch { id: number; companyId?: number; name: string; timeZoneId: TimeZone['id']; - address: AddressDTO | null; -} - -export interface Branch extends BranchDTO { + address: Address | null; timeZoneName?: TimeZone['name']; isValid?: boolean; noPhysicalAddress?: boolean; - address: Address | null; fullAddress?: string; geoAddress?: GeoAddress; } diff --git a/src/shared/types/client.ts b/src/shared/types/client.ts deleted file mode 100644 index 6748c2a9..00000000 --- a/src/shared/types/client.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface Client { - clientId: string; - clientName: string; - tenantId: string; -} diff --git a/src/shared/types/company-settings.ts b/src/shared/types/company-settings.ts index 4bb49208..4a2d9eee 100644 --- a/src/shared/types/company-settings.ts +++ b/src/shared/types/company-settings.ts @@ -1,13 +1,11 @@ -import { ColorSchemeId } from 'shared/types'; +import type { ColorSchemeId } from './theme'; -export interface CompanySettingsDTO { +export interface CompanySettings { id: number; companyId?: number; colorSchemeId?: ColorSchemeId; } -export interface CompanySettings extends CompanySettingsDTO {} - export type CompanySettingsFieldConfig = { [K in keyof CompanySettings]: { field: K; name: string }; }; diff --git a/src/shared/types/company.ts b/src/shared/types/company.ts index 65d7fb90..f88bb83b 100644 --- a/src/shared/types/company.ts +++ b/src/shared/types/company.ts @@ -1,13 +1,10 @@ import { Country, FlattenField } from './common'; import { CompanyLicense } from './license'; -export interface CompanyDTO { +export interface Company { id: number; name: string; countryCode: Country['code']; -} - -export interface Company extends CompanyDTO { countryName?: Country['name']; } diff --git a/src/shared/types/department.ts b/src/shared/types/department.ts index 13410ab4..53faebb7 100644 --- a/src/shared/types/department.ts +++ b/src/shared/types/department.ts @@ -1,11 +1,8 @@ -export interface DepartmentDTO { +export interface Department { id: number; name: string; parentDepartmentId: number | null; managerId: number | null; -} - -export interface Department extends DepartmentDTO { parentDepartmentName?: string; managerName?: string; } diff --git a/src/shared/types/employee.ts b/src/shared/types/employee.ts index 03b3b558..0e4de312 100644 --- a/src/shared/types/employee.ts +++ b/src/shared/types/employee.ts @@ -1,4 +1,4 @@ -export interface EmployeeDTO { +export interface Employee { id: number; companyId?: number; assignedBranchId: number | null; @@ -8,9 +8,6 @@ export interface EmployeeDTO { firstName: string; lastName: string; fullName: string; -} - -export interface Employee extends EmployeeDTO { picture?: string; isDraft?: boolean; assignedBranchName?: string; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 5856a7b5..4c2428f5 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1,4 +1,3 @@ -export * from './client'; export * from './route'; export * from './license'; export * from './response'; diff --git a/src/shared/types/response.ts b/src/shared/types/response.ts index 896efc78..09fddb91 100644 --- a/src/shared/types/response.ts +++ b/src/shared/types/response.ts @@ -1,26 +1,22 @@ import { FieldErrors, FieldValues } from 'react-hook-form'; +import type { ProblemDetailsModel } from '@fossa-app/bridge/Models/ApiModels/SharedModels'; -export interface ErrorResponseDTO { - type?: string; - title?: string; - traceId?: string; - status?: number; - errors?: Record; -} +type PagingResponseModel = import('@fossa-app/bridge/Models/ApiModels/EnvelopeModels').PagingResponseModel$1; +export type { ProblemDetailsModel } from '@fossa-app/bridge/Models/ApiModels/SharedModels'; -export interface ErrorResponse extends Omit { +export type ErrorResponse = Partial & { errors?: FieldErrors; -} +}; -export type GeneralErrorResponse = ErrorResponseDTO | ErrorResponse; +export type GeneralErrorResponse = ProblemDetailsModel | ErrorResponse; -export interface PaginatedResponse { +export type PaginatedResponse = Partial> & { pageNumber?: number; pageSize?: number; totalItems?: number; totalPages?: number; items: T[]; -} +}; export interface PaginationParams { pageNumber?: number; diff --git a/src/store/features/authSlice.ts b/src/store/features/authSlice.ts index 4cdac419..06c8ed9e 100644 --- a/src/store/features/authSlice.ts +++ b/src/store/features/authSlice.ts @@ -3,7 +3,7 @@ import { OidcClientSettings } from 'oidc-client-ts'; import { RootState, StateEntity } from 'store'; import { fetchUser } from 'store/thunks'; import { updateUserManager, decodeJwt } from 'shared/helpers'; -import { AppUser, ErrorResponseDTO, UserRole } from 'shared/types'; +import { AppUser, ProblemDetailsModel, UserRole } from 'shared/types'; import { OIDC_INITIAL_CONFIG } from 'shared/constants'; interface AuthState { @@ -45,7 +45,7 @@ const authSlice = createSlice({ .addCase(fetchUser.pending, (state) => { state.user.fetchStatus = 'loading'; }) - .addCase(fetchUser.rejected, (state, action: PayloadAction) => { + .addCase(fetchUser.rejected, (state, action: PayloadAction) => { state.user.item = undefined; state.user.fetchStatus = 'failed'; state.user.fetchError = action.payload; diff --git a/src/store/features/branchSlice.ts b/src/store/features/branchSlice.ts index cf11647f..0e8d45e3 100644 --- a/src/store/features/branchSlice.ts +++ b/src/store/features/branchSlice.ts @@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { FieldValues } from 'react-hook-form'; import { WritableDraft } from 'immer'; import { RootState, PaginatedStateEntity, StateEntity } from 'store'; -import { Branch, BranchDTO, ErrorResponseDTO, ErrorResponse, PaginatedResponse, PaginationParams } from 'shared/types'; +import { Branch, ProblemDetailsModel, ErrorResponse, PaginatedResponse, PaginationParams } from 'shared/types'; import { APP_CONFIG } from 'shared/constants'; import { mergePaginatedItems } from 'store/helpers'; import { @@ -18,7 +18,7 @@ import { interface BranchState { branch: StateEntity; branchCatalog: PaginatedStateEntity; - assignedBranches: PaginatedStateEntity; + assignedBranches: PaginatedStateEntity; } const initialState: BranchState = { @@ -73,7 +73,7 @@ const branchSlice = createSlice({ .addCase(fetchBranches.pending, (state) => { state.branchCatalog.status = 'loading'; }) - .addCase(fetchBranches.rejected, (state, action: PayloadAction) => { + .addCase(fetchBranches.rejected, (state, action: PayloadAction) => { state.branchCatalog.items = []; state.branchCatalog.status = 'failed'; state.branchCatalog.error = action.payload; @@ -92,7 +92,7 @@ const branchSlice = createSlice({ .addCase(fetchAssignedBranches.fulfilled, (state, action) => { const { items = [], ...page } = action.payload || {}; - state.assignedBranches.items = mergePaginatedItems(state.assignedBranches.items, items); + state.assignedBranches.items = mergePaginatedItems(state.assignedBranches.items, items); state.assignedBranches.page = page; state.assignedBranches.status = 'succeeded'; }) @@ -160,7 +160,7 @@ const branchSlice = createSlice({ .addCase(deleteBranch.pending, (state) => { state.branch.deleteStatus = 'loading'; }) - .addCase(deleteBranch.rejected, (state, action: PayloadAction) => { + .addCase(deleteBranch.rejected, (state, action: PayloadAction) => { state.branch.deleteStatus = 'failed'; state.branch.deleteError = action.payload; }) diff --git a/src/store/features/companySettingsSlice.ts b/src/store/features/companySettingsSlice.ts index 5f33fd66..21df6f5b 100644 --- a/src/store/features/companySettingsSlice.ts +++ b/src/store/features/companySettingsSlice.ts @@ -3,7 +3,7 @@ import { FieldValues } from 'react-hook-form'; import { WritableDraft } from 'immer'; import { RootState, StateEntity } from 'store'; import { createCompanySettings, deleteCompanySettings, editCompanySettings, fetchCompanySettings } from 'store/thunks'; -import { CompanySettings, CompanySettingsDTO, ErrorResponse, ErrorResponseDTO } from 'shared/types'; +import { CompanySettings, ErrorResponse, ProblemDetailsModel } from 'shared/types'; interface CompanySettingsState { companySettings: StateEntity; @@ -38,11 +38,11 @@ const companySettingsSlice = createSlice({ .addCase(fetchCompanySettings.pending, (state) => { state.companySettings.fetchStatus = 'loading'; }) - .addCase(fetchCompanySettings.rejected, (state, action: PayloadAction) => { + .addCase(fetchCompanySettings.rejected, (state, action: PayloadAction) => { state.companySettings.fetchStatus = 'failed'; state.companySettings.fetchError = action.payload; }) - .addCase(fetchCompanySettings.fulfilled, (state, action: PayloadAction) => { + .addCase(fetchCompanySettings.fulfilled, (state, action: PayloadAction) => { state.companySettings.item = action.payload; state.companySettings.fetchStatus = 'succeeded'; }) @@ -71,7 +71,7 @@ const companySettingsSlice = createSlice({ .addCase(deleteCompanySettings.pending, (state) => { state.companySettings.deleteStatus = 'loading'; }) - .addCase(deleteCompanySettings.rejected, (state, action: PayloadAction) => { + .addCase(deleteCompanySettings.rejected, (state, action: PayloadAction) => { state.companySettings.deleteStatus = 'failed'; state.companySettings.fetchError = action.payload; }) diff --git a/src/store/features/companySlice.ts b/src/store/features/companySlice.ts index 5377eceb..945e36ce 100644 --- a/src/store/features/companySlice.ts +++ b/src/store/features/companySlice.ts @@ -13,14 +13,13 @@ import { fetchEmployeesTotal, } from 'store/thunks'; import { - BranchDTO, + Branch, Company, CompanyDatasourceTotals, - CompanyDTO, - DepartmentDTO, - EmployeeDTO, + Department, + Employee, ErrorResponse, - ErrorResponseDTO, + ProblemDetailsModel, PaginatedResponse, } from 'shared/types'; import { calculateUsagePercent, filterUniqueByField } from 'shared/helpers'; @@ -62,12 +61,12 @@ const companySlice = createSlice({ .addCase(fetchCompany.pending, (state) => { state.company.fetchStatus = 'loading'; }) - .addCase(fetchCompany.rejected, (state, action: PayloadAction) => { + .addCase(fetchCompany.rejected, (state, action: PayloadAction) => { state.company.item = undefined; state.company.fetchStatus = 'failed'; state.company.fetchError = action.payload; }) - .addCase(fetchCompany.fulfilled, (state, action: PayloadAction) => { + .addCase(fetchCompany.fulfilled, (state, action: PayloadAction) => { state.company.item = action.payload; state.company.fetchStatus = 'succeeded'; }) @@ -96,7 +95,7 @@ const companySlice = createSlice({ .addCase(deleteCompany.pending, (state) => { state.company.deleteStatus = 'loading'; }) - .addCase(deleteCompany.rejected, (state, action: PayloadAction) => { + .addCase(deleteCompany.rejected, (state, action: PayloadAction) => { state.company.deleteStatus = 'failed'; state.company.deleteError = action.payload; }) @@ -113,13 +112,13 @@ const companySlice = createSlice({ .addCase(fetchDepartmentsTotal.rejected, (state) => { state.companyDatasourceTotals.item.departments = 0; }) - .addCase(fetchBranchesTotal.fulfilled, (state, action: PayloadAction | undefined>) => { + .addCase(fetchBranchesTotal.fulfilled, (state, action: PayloadAction | undefined>) => { state.companyDatasourceTotals.item.branches = action.payload?.totalItems; }) - .addCase(fetchEmployeesTotal.fulfilled, (state, action: PayloadAction | undefined>) => { + .addCase(fetchEmployeesTotal.fulfilled, (state, action: PayloadAction | undefined>) => { state.companyDatasourceTotals.item.employees = action.payload?.totalItems; }) - .addCase(fetchDepartmentsTotal.fulfilled, (state, action: PayloadAction | undefined>) => { + .addCase(fetchDepartmentsTotal.fulfilled, (state, action: PayloadAction | undefined>) => { state.companyDatasourceTotals.item.departments = action.payload?.totalItems; }) .addCase(fetchCompanyDatasourceTotals.pending, (state) => { diff --git a/src/store/features/departmentSlice.ts b/src/store/features/departmentSlice.ts index 85a025a2..2a0a38b3 100644 --- a/src/store/features/departmentSlice.ts +++ b/src/store/features/departmentSlice.ts @@ -12,7 +12,7 @@ import { fetchParentDepartments, fetchSearchedDepartments, } from 'store/thunks'; -import { ErrorResponseDTO, ErrorResponse, PaginatedResponse, PaginationParams, Department, DepartmentDTO } from 'shared/types'; +import { ProblemDetailsModel, ErrorResponse, PaginatedResponse, PaginationParams, Department } from 'shared/types'; import { APP_CONFIG } from 'shared/constants'; import { mergePaginatedItems } from 'store/helpers'; @@ -20,8 +20,8 @@ interface DepartmentState { department: StateEntity; departmentCatalog: PaginatedStateEntity; searchedDepartments: PaginatedStateEntity; - parentDepartments: PaginatedStateEntity; - assignedDepartments: PaginatedStateEntity; + parentDepartments: PaginatedStateEntity; + assignedDepartments: PaginatedStateEntity; } const initialState: DepartmentState = { @@ -96,7 +96,7 @@ const departmentSlice = createSlice({ .addCase(fetchDepartments.pending, (state) => { state.departmentCatalog.status = 'loading'; }) - .addCase(fetchDepartments.rejected, (state, action: PayloadAction) => { + .addCase(fetchDepartments.rejected, (state, action: PayloadAction) => { state.departmentCatalog.items = []; state.departmentCatalog.status = 'failed'; state.departmentCatalog.error = action.payload; @@ -112,7 +112,7 @@ const departmentSlice = createSlice({ .addCase(fetchSearchedDepartments.pending, (state) => { state.searchedDepartments.status = 'loading'; }) - .addCase(fetchSearchedDepartments.rejected, (state, action: PayloadAction) => { + .addCase(fetchSearchedDepartments.rejected, (state, action: PayloadAction) => { state.searchedDepartments.items = []; state.searchedDepartments.status = 'failed'; state.searchedDepartments.error = action.payload; @@ -131,7 +131,7 @@ const departmentSlice = createSlice({ .addCase(fetchParentDepartments.fulfilled, (state, action) => { const { items = [], ...page } = action.payload || {}; - state.parentDepartments.items = mergePaginatedItems(state.parentDepartments.items, items); + state.parentDepartments.items = mergePaginatedItems(state.parentDepartments.items, items); state.parentDepartments.page = page; state.parentDepartments.status = 'succeeded'; }) @@ -144,7 +144,7 @@ const departmentSlice = createSlice({ .addCase(fetchAssignedDepartments.fulfilled, (state, action) => { const { items = [], ...page } = action.payload || {}; - state.assignedDepartments.items = mergePaginatedItems(state.assignedDepartments.items, items); + state.assignedDepartments.items = mergePaginatedItems(state.assignedDepartments.items, items); state.assignedDepartments.page = page; state.assignedDepartments.status = 'succeeded'; }) @@ -201,7 +201,7 @@ const departmentSlice = createSlice({ .addCase(deleteDepartment.pending, (state) => { state.department.deleteStatus = 'loading'; }) - .addCase(deleteDepartment.rejected, (state, action: PayloadAction) => { + .addCase(deleteDepartment.rejected, (state, action: PayloadAction) => { state.department.deleteStatus = 'failed'; state.department.deleteError = action.payload; }) diff --git a/src/store/features/employeeSlice.ts b/src/store/features/employeeSlice.ts index 92541ef9..5f9475dd 100644 --- a/src/store/features/employeeSlice.ts +++ b/src/store/features/employeeSlice.ts @@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { FieldValues } from 'react-hook-form'; import { WritableDraft } from 'immer'; import { PaginatedStateEntity, RootState, StateEntity } from 'store'; -import { Employee, EmployeeDTO, ErrorResponse, ErrorResponseDTO, PaginatedResponse, PaginationParams } from 'shared/types'; +import { Employee, ErrorResponse, ProblemDetailsModel, PaginatedResponse, PaginationParams } from 'shared/types'; import { APP_CONFIG } from 'shared/constants'; import { mergePaginatedItems } from 'store/helpers'; import { editEmployee, fetchEmployeeById, fetchEmployees, fetchManagers, fetchOrgChartEmployees } from 'store/thunks'; @@ -10,8 +10,8 @@ import { editEmployee, fetchEmployeeById, fetchEmployees, fetchManagers, fetchOr interface EmployeeState { employee: StateEntity; employeeCatalog: PaginatedStateEntity; - managers: PaginatedStateEntity; - employeeOrgChart: PaginatedStateEntity; + managers: PaginatedStateEntity; + employeeOrgChart: PaginatedStateEntity; } const initialState: EmployeeState = { @@ -73,7 +73,7 @@ const employeeSlice = createSlice({ .addCase(fetchEmployees.pending, (state) => { state.employeeCatalog.status = 'loading'; }) - .addCase(fetchEmployees.rejected, (state, action: PayloadAction) => { + .addCase(fetchEmployees.rejected, (state, action: PayloadAction) => { state.employeeCatalog.items = []; state.employeeCatalog.status = 'failed'; state.employeeCatalog.error = action.payload; @@ -92,7 +92,7 @@ const employeeSlice = createSlice({ .addCase(fetchManagers.fulfilled, (state, action) => { const { items = [], ...page } = action.payload || {}; - state.managers.items = mergePaginatedItems(state.managers.items, items); + state.managers.items = mergePaginatedItems(state.managers.items, items); state.managers.page = page; state.managers.status = 'succeeded'; }) @@ -109,12 +109,12 @@ const employeeSlice = createSlice({ .addCase(fetchOrgChartEmployees.pending, (state) => { state.employeeOrgChart.status = 'loading'; }) - .addCase(fetchOrgChartEmployees.rejected, (state, action: PayloadAction) => { + .addCase(fetchOrgChartEmployees.rejected, (state, action: PayloadAction) => { state.employeeOrgChart.items = []; state.employeeOrgChart.status = 'failed'; state.employeeOrgChart.error = action.payload; }) - .addCase(fetchOrgChartEmployees.fulfilled, (state, action: PayloadAction | undefined>) => { + .addCase(fetchOrgChartEmployees.fulfilled, (state, action: PayloadAction | undefined>) => { const { items = [] } = action.payload || {}; state.employeeOrgChart.items = items; state.employeeOrgChart.status = 'succeeded'; diff --git a/src/store/features/identitySlice.ts b/src/store/features/identitySlice.ts index 58213673..0fa525d3 100644 --- a/src/store/features/identitySlice.ts +++ b/src/store/features/identitySlice.ts @@ -1,10 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { RootState, StateEntity } from 'store'; import { fetchClient } from 'store/thunks'; -import { Client, ErrorResponseDTO } from 'shared/types'; +import { ProblemDetailsModel } from 'shared/types'; +import { IdentityClientRetrievalModel } from '@fossa-app/bridge/Models/ApiModels/PayloadModels'; interface IdentityState { - client: StateEntity; + client: StateEntity; } const initialState: IdentityState = { @@ -23,12 +24,12 @@ const identitySlice = createSlice({ .addCase(fetchClient.pending, (state) => { state.client.fetchStatus = 'loading'; }) - .addCase(fetchClient.rejected, (state, action: PayloadAction) => { + .addCase(fetchClient.rejected, (state, action: PayloadAction) => { state.client.item = undefined; state.client.fetchStatus = 'failed'; state.client.fetchError = action.payload; }) - .addCase(fetchClient.fulfilled, (state, action: PayloadAction) => { + .addCase(fetchClient.fulfilled, (state, action: PayloadAction) => { state.client.item = action.payload; state.client.fetchStatus = 'succeeded'; }); diff --git a/src/store/features/licenseSlice.ts b/src/store/features/licenseSlice.ts index 90a33e33..281e6fcf 100644 --- a/src/store/features/licenseSlice.ts +++ b/src/store/features/licenseSlice.ts @@ -3,7 +3,7 @@ import { FieldValues } from 'react-hook-form'; import { WritableDraft } from 'immer'; import { RootState, StateEntity } from 'store'; import { fetchCompanyLicense, fetchSystemLicense, uploadCompanyLicense } from 'store/thunks'; -import { CompanyLicense, ErrorResponse, ErrorResponseDTO, SystemLicense } from 'shared/types'; +import { CompanyLicense, ErrorResponse, ProblemDetailsModel, SystemLicense } from 'shared/types'; interface LicenseState { system: StateEntity; @@ -31,7 +31,7 @@ const licenseSlice = createSlice({ .addCase(fetchSystemLicense.pending, (state) => { state.system.fetchStatus = 'loading'; }) - .addCase(fetchSystemLicense.rejected, (state, action: PayloadAction) => { + .addCase(fetchSystemLicense.rejected, (state, action: PayloadAction) => { state.system.item = undefined; state.system.fetchStatus = 'failed'; state.system.fetchError = action.payload; @@ -43,7 +43,7 @@ const licenseSlice = createSlice({ .addCase(fetchCompanyLicense.pending, (state) => { state.company.fetchStatus = 'loading'; }) - .addCase(fetchCompanyLicense.rejected, (state, action: PayloadAction) => { + .addCase(fetchCompanyLicense.rejected, (state, action: PayloadAction) => { state.company.item = undefined; state.company.fetchStatus = 'failed'; state.company.fetchError = action.payload; diff --git a/src/store/features/messageSlice.ts b/src/store/features/messageSlice.ts index 32f88e68..88b388f0 100644 --- a/src/store/features/messageSlice.ts +++ b/src/store/features/messageSlice.ts @@ -1,9 +1,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { RootState } from 'store'; -import { ErrorResponseDTO } from 'shared/types'; +import { ProblemDetailsModel } from 'shared/types'; interface MessageState { - error: ErrorResponseDTO | undefined; + error: ProblemDetailsModel | undefined; success: string | undefined; } @@ -16,7 +16,7 @@ const messageSlice = createSlice({ name: 'message', initialState, reducers: { - setError: (state, action: PayloadAction) => { + setError: (state, action: PayloadAction) => { state.error = action.payload; state.success = undefined; }, diff --git a/src/store/features/offboardingSlice.ts b/src/store/features/offboardingSlice.ts index 6d054581..fdba48da 100644 --- a/src/store/features/offboardingSlice.ts +++ b/src/store/features/offboardingSlice.ts @@ -12,11 +12,11 @@ import { fetchProfile, } from 'store/thunks'; import { - BranchDTO, + Branch, CompanyDatasourceTotals, CompanyOffboardingStep, - DepartmentDTO, - EmployeeDTO, + Department, + Employee, OffboardingStep, PaginatedResponse, } from 'shared/types'; @@ -108,7 +108,7 @@ const offboardingSlice = createSlice({ state.company.flags[OffboardingStep.companySettings] = true; evaluateCompanyOffboardingStep(state); }) - .addCase(fetchBranchesTotal.fulfilled, (state, action: PayloadAction | undefined>) => { + .addCase(fetchBranchesTotal.fulfilled, (state, action: PayloadAction | undefined>) => { state.company.flags[OffboardingStep.instructions].branches = action.payload?.totalItems; evaluateCompanyOffboardingStep(state); }) @@ -119,11 +119,11 @@ const offboardingSlice = createSlice({ state.company.flags[OffboardingStep.company] = true; evaluateCompanyOffboardingStep(state); }) - .addCase(fetchEmployeesTotal.fulfilled, (state, action: PayloadAction | undefined>) => { + .addCase(fetchEmployeesTotal.fulfilled, (state, action: PayloadAction | undefined>) => { state.company.flags[OffboardingStep.instructions].employees = action.payload?.totalItems; evaluateCompanyOffboardingStep(state); }) - .addCase(fetchDepartmentsTotal.fulfilled, (state, action: PayloadAction | undefined>) => { + .addCase(fetchDepartmentsTotal.fulfilled, (state, action: PayloadAction | undefined>) => { state.company.flags[OffboardingStep.instructions].departments = action.payload?.totalItems; evaluateCompanyOffboardingStep(state); }) diff --git a/src/store/features/profileSlice.ts b/src/store/features/profileSlice.ts index b0d5fb81..8e07289a 100644 --- a/src/store/features/profileSlice.ts +++ b/src/store/features/profileSlice.ts @@ -3,7 +3,7 @@ import { FieldValues } from 'react-hook-form'; import { WritableDraft } from 'immer'; import { RootState, StateEntity } from 'store'; import { createProfile, deleteProfile, editProfile, fetchProfile, fetchUser } from 'store/thunks'; -import { AppUser, Employee, ErrorResponse, ErrorResponseDTO } from 'shared/types'; +import { AppUser, Employee, ErrorResponse, ProblemDetailsModel } from 'shared/types'; import { mapUserProfileToEmployee } from 'shared/helpers'; interface ProfileState { @@ -31,7 +31,7 @@ const profileSlice = createSlice({ .addCase(fetchProfile.pending, (state) => { state.profile.fetchStatus = 'loading'; }) - .addCase(fetchProfile.rejected, (state, action: PayloadAction) => { + .addCase(fetchProfile.rejected, (state, action: PayloadAction) => { state.profile.fetchStatus = 'failed'; state.profile.fetchError = action.payload; }) @@ -70,7 +70,7 @@ const profileSlice = createSlice({ .addCase(deleteProfile.pending, (state) => { state.profile.deleteStatus = 'loading'; }) - .addCase(deleteProfile.rejected, (state, action: PayloadAction) => { + .addCase(deleteProfile.rejected, (state, action: PayloadAction) => { state.profile.deleteStatus = 'failed'; state.profile.fetchError = action.payload; }) diff --git a/src/store/thunks/authThunk.ts b/src/store/thunks/authThunk.ts index 79877e51..b7833e46 100644 --- a/src/store/thunks/authThunk.ts +++ b/src/store/thunks/authThunk.ts @@ -1,9 +1,9 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import { getUserManager, mapUser } from 'shared/helpers'; -import { AppUser, ErrorResponseDTO } from 'shared/types'; +import { getUserManager, mapUser, createProblemDetails } from 'shared/helpers'; +import { AppUser, ProblemDetailsModel } from 'shared/types'; import { MESSAGES } from 'shared/constants'; -export const fetchUser = createAsyncThunk( +export const fetchUser = createAsyncThunk( 'auth/fetchUser', async (_, { rejectWithValue }) => { try { @@ -13,12 +13,9 @@ export const fetchUser = createAsyncThunk | undefined, void, { rejectValue: ErrorResponseDTO }>( +import { Branch, ProblemDetailsModel, ErrorResponse, PaginatedResponse, PaginationParams, GeoAddress, EntityInput } from 'shared/types'; +import { MESSAGES } from 'shared/constants'; +import { mapBranch, mapBranches, mapError, getFullAddress, createProblemDetails } from 'shared/helpers'; +import { branchClient } from 'shared/configs/BridgeClients'; +import { matchClientResult, matchClientUnitResult, toPaginatedResponse } from 'shared/configs/BridgeResponses'; +import { BranchQueryRequestModel, BranchModificationModel } from '@fossa-app/bridge/Models/ApiModels/PayloadModels'; +import { AddressModel } from '@fossa-app/bridge/Models/ApiModels/SharedModels'; + +const toBranchModificationAddress = (address: EntityInput['address']): AddressModel | null => { + if (address === null || address === undefined) { + return null; + } + + return new AddressModel( + address.line1 ?? null, + address.line2 ?? null, + address.city ?? null, + address.subdivision ?? null, + address.postalCode ?? null, + address.countryCode ?? null + ); +}; + +export const fetchBranchesTotal = createAsyncThunk | undefined, void, { rejectValue: ProblemDetailsModel }>( 'branch/fetchBranchesTotal', async (_, { rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber: 1, pageSize: 1 }); - const { data } = await axios.get>(`${ENDPOINTS.branches}?${queryParams}`); + const query = new BranchQueryRequestModel([], '', 1, 1); - if (!data.items.length) { - return rejectWithValue({ - status: 404, - title: MESSAGES.error.branches.notFound, - }); - } + return matchClientResult( + await branchClient.GetBranchesAsync(query, new AbortController().signal), + (response) => { + const data = toPaginatedResponse(response); + if (!data.items.length) { + return rejectWithValue(createProblemDetails(undefined, { Status: 404, Title: MESSAGES.error.branches.notFound })); + } - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.branches.notFound, - }); - } + return data; + }, + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.branches.notFound })) + ); } ); export const fetchBranches = createAsyncThunk< PaginatedResponse | undefined, Partial, - { rejectValue: ErrorResponseDTO } + { rejectValue: ProblemDetailsModel } >('branch/fetchBranches', async ({ pageNumber, pageSize, search }, { getState, rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber, pageSize, search }); - const { data } = await axios.get>(`${ENDPOINTS.branches}?${queryParams}`); - - if (!data.items.length) { - return rejectWithValue({ - status: 404, - title: MESSAGES.error.branches.notFound, - }); - } + const query = new BranchQueryRequestModel([], search || '', pageNumber || null, pageSize || null); + + return matchClientResult( + await branchClient.GetBranchesAsync(query, new AbortController().signal), + (response) => { + const data = toPaginatedResponse(response); + if (!data.items.length) { + return rejectWithValue(createProblemDetails(undefined, { Status: 404, Title: MESSAGES.error.branches.notFound })); + } - const state = getState() as RootState; - const timeZones = state.license.system.item?.entitlements.timeZones || []; - const countries = state.license.system.item?.entitlements.countries || []; - const companyCountryCode = state.company.company.item!.countryCode; + const state = getState() as RootState; + const timeZones = state.license.system.item?.entitlements.timeZones || []; + const countries = state.license.system.item?.entitlements.countries || []; + const companyCountryCode = state.company.company.item!.countryCode; - return { - ...data, - items: mapBranches(data.items, timeZones, companyCountryCode, countries), - }; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.branches.notFound, - }); - } + return { + ...data, + items: mapBranches(data.items, timeZones, companyCountryCode, countries), + }; + }, + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.branches.notFound })) + ); }); export const fetchAssignedBranches = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { state: RootState; rejectValue: ErrorResponseDTO } + { state: RootState; rejectValue: ProblemDetailsModel } >('branch/fetchAssignedBranches', async ({ pageNumber, pageSize, search }, { rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber, pageSize, search }); - const { data } = await axios.get>(`${ENDPOINTS.branches}?${queryParams}`); + const query = new BranchQueryRequestModel([], search || '', pageNumber || null, pageSize || null); - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.branches.notFound, - }); - } + return matchClientResult( + await branchClient.GetBranchesAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.branches.notFound })) + ); }); -export const fetchBranchesByIds = createAsyncThunk | undefined, number[], { rejectValue: ErrorResponseDTO }>( +export const fetchBranchesByIds = createAsyncThunk | undefined, number[], { rejectValue: ProblemDetailsModel }>( 'branch/fetchBranchesByIds', async (ids, { rejectWithValue }) => { + let idList: bigint[] = []; try { - const queryParams = prepareCommaSeparatedQueryParamsByKey('id', ids); - const { data } = await axios.get>(`${ENDPOINTS.branches}?${queryParams}`); - - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.branches.notFound, - }); + idList = ids.map((id) => BigInt(id)); + } catch { + // Ignored } + + const query = new BranchQueryRequestModel(idList, '', null, null); + + return matchClientResult( + await branchClient.GetBranchesAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.branches.notFound })) + ); } ); export const fetchBranchById = createAsyncThunk< Branch, { id: string; skipState?: boolean; shouldFetchBranchGeoAddress?: boolean }, - { rejectValue: ErrorResponseDTO } + { rejectValue: ProblemDetailsModel } >('branch/fetchBranchById', async ({ id, shouldFetchBranchGeoAddress = true }, { getState, dispatch, rejectWithValue }) => { try { - const { data } = await axios.get(`${ENDPOINTS.branches}/${id}`); - const state = getState() as RootState; - const timeZones = state.license.system.item?.entitlements.timeZones || []; - const countries = state.license.system.item?.entitlements.countries || []; - const companyCountryCode = state.company.company.item!.countryCode; - const countryName = countries.find((country) => country.code === data.address?.countryCode)?.name; - const fullAddress = data.address ? getFullAddress({ ...data.address, countryName }, false) : ''; - let geoAddress: GeoAddress | undefined; - - if (shouldFetchBranchGeoAddress) { - geoAddress = await dispatch(fetchGeoAddress(fullAddress)).unwrap(); - } - - return mapBranch({ - branch: data, - timeZones, - companyCountryCode, - countries, - geoAddress, - }); + return matchClientResult( + await branchClient.GetBranchAsync(BigInt(id), new AbortController().signal), + async (data) => { + const state = getState() as RootState; + const timeZones = state.license.system.item?.entitlements.timeZones || []; + const countries = state.license.system.item?.entitlements.countries || []; + const companyCountryCode = state.company.company.item!.countryCode; + const countryName = countries.find((country) => country.code === data.address?.countryCode)?.name; + const fullAddress = data.address ? getFullAddress({ ...data.address, countryName }, false) : ''; + let geoAddress: GeoAddress | undefined; + + if (shouldFetchBranchGeoAddress) { + geoAddress = await dispatch(fetchGeoAddress(fullAddress)).unwrap(); + } + + return mapBranch({ + branch: data, + timeZones, + companyCountryCode, + countries, + geoAddress, + }); + }, + (problem) => rejectWithValue(problem) + ); } catch (error) { - return rejectWithValue(error as ErrorResponseDTO); + return rejectWithValue(error as ProblemDetailsModel); } }); -export const createOnboardingBranch = createAsyncThunk, { rejectValue: ErrorResponse }>( +export const createOnboardingBranch = createAsyncThunk, { rejectValue: ErrorResponse }>( 'branch/createOnboardingBranch', async (branch, { dispatch, rejectWithValue }) => { - try { - await axios.post(ENDPOINTS.branches, branch); + const modModel = new BranchModificationModel(branch.name, branch.timeZoneId, toBranchModificationAddress(branch.address)); - dispatch(resetBranchesFetchStatus()); - await dispatch(fetchBranchesTotal()).unwrap(); + return matchClientUnitResult( + await branchClient.CreateBranchAsync(modModel, new AbortController().signal), + async () => { + dispatch(resetBranchesFetchStatus()); + await dispatch(fetchBranchesTotal()).unwrap(); - dispatch(setSuccess(MESSAGES.success.branches.create)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.branches.create, - }) - ); + dispatch(setSuccess(MESSAGES.success.branches.create)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.branches.create }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(problem) as ErrorResponse; - return rejectWithValue(mappedError); - } + return rejectWithValue(mappedError); + } + ); } ); -export const createBranch = createAsyncThunk, { rejectValue: ErrorResponse }>( +export const createBranch = createAsyncThunk, { rejectValue: ErrorResponse }>( 'branch/createBranch', async (branch, { dispatch, rejectWithValue }) => { - try { - await axios.post(ENDPOINTS.branches, branch); - - dispatch(resetBranchesFetchStatus()); - dispatch(setBranchesSucceededFlag()); - dispatch(setSuccess(MESSAGES.success.branches.create)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.branches.create, - }) - ); - - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; - - return rejectWithValue(mappedError); - } + const modModel = new BranchModificationModel(branch.name, branch.timeZoneId, toBranchModificationAddress(branch.address)); + + return matchClientUnitResult( + await branchClient.CreateBranchAsync(modModel, new AbortController().signal), + () => { + dispatch(resetBranchesFetchStatus()); + dispatch(setBranchesSucceededFlag()); + dispatch(setSuccess(MESSAGES.success.branches.create)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.branches.create }))); + + const mappedError = mapError(problem) as ErrorResponse; + + return rejectWithValue(mappedError); + } + ); } ); -export const editBranch = createAsyncThunk], { rejectValue: ErrorResponse }>( +export const editBranch = createAsyncThunk], { rejectValue: ErrorResponse }>( 'branch/editBranch', async ([id, branch], { dispatch, rejectWithValue }) => { - try { - await axios.put(`${ENDPOINTS.branches}/${id}`, branch); + const modModel = new BranchModificationModel(branch.name, branch.timeZoneId, toBranchModificationAddress(branch.address)); - dispatch(setSuccess(MESSAGES.success.branches.update)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.branches.update, - }) - ); + return matchClientUnitResult( + await branchClient.UpdateBranchAsync(BigInt(id), modModel, new AbortController().signal), + () => { + dispatch(setSuccess(MESSAGES.success.branches.update)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.branches.update }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(problem) as ErrorResponse; - return rejectWithValue(mappedError); - } + return rejectWithValue(mappedError); + } + ); } ); -export const deleteBranch = createAsyncThunk( +export const deleteBranch = createAsyncThunk( 'branch/deleteBranch', async (id, { dispatch, getState, rejectWithValue }) => { - try { - await axios.delete(`${ENDPOINTS.branches}/${id}`); - - dispatch(resetBranchesFetchStatus()); - dispatch(resetCompanyDatasourceTotalsFetchStatus()); - dispatch(setSuccess(MESSAGES.success.branches.delete)); - - const state = getState() as RootState; - const { branchCatalog } = state.branch; + return matchClientUnitResult( + await branchClient.DeleteBranchAsync(BigInt(id), new AbortController().signal), + () => { + dispatch(resetBranchesFetchStatus()); + dispatch(resetCompanyDatasourceTotalsFetchStatus()); + dispatch(setSuccess(MESSAGES.success.branches.delete)); + + const state = getState() as RootState; + const { branchCatalog } = state.branch; + + if (branchCatalog.page.totalItems === 1) { + dispatch(setBranchesFailedFlag()); + } + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.branches.delete }))); - if (branchCatalog.page.totalItems === 1) { - dispatch(setBranchesFailedFlag()); + return rejectWithValue(problem); } - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.branches.delete, - }) - ); - - return rejectWithValue(error as ErrorResponseDTO); - } + ); } ); @@ -259,17 +251,18 @@ export const fetchGeoAddress = createAsyncThunk 0) { - const { lat, lon, display_name } = response.data[0]; + if (parsedData?.length > 0) { + const { lat, lon, display_name } = parsedData[0]; return { lat: Number(parseFloat(lat).toFixed(7)), diff --git a/src/store/thunks/companySettingsThunk.ts b/src/store/thunks/companySettingsThunk.ts index a25f681b..163b4453 100644 --- a/src/store/thunks/companySettingsThunk.ts +++ b/src/store/thunks/companySettingsThunk.ts @@ -1,100 +1,95 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { FieldValues } from 'react-hook-form'; import { setError, setSuccess } from 'store/features'; -import axios from 'shared/configs/axios'; -import { CompanySettings, CompanySettingsDTO, EntityInput, ErrorResponse, ErrorResponseDTO } from 'shared/types'; -import { MESSAGES, ENDPOINTS, COMPANY_SETTINGS_KEY } from 'shared/constants'; -import { mapError, saveToLocalStorage, removeFromLocalStorage } from 'shared/helpers'; +import { CompanySettings, EntityInput, ErrorResponse, ProblemDetailsModel } from 'shared/types'; +import { MESSAGES, COMPANY_SETTINGS_KEY } from 'shared/constants'; +import { mapError, saveToLocalStorage, removeFromLocalStorage, createProblemDetails } from 'shared/helpers'; +import { companySettingsClient } from 'shared/configs/BridgeClients'; +import { matchClientResult, matchClientUnitResult } from 'shared/configs/BridgeResponses'; +import { CompanySettingsModificationModel } from '@fossa-app/bridge/Models/ApiModels/PayloadModels'; -export const fetchCompanySettings = createAsyncThunk( +export const fetchCompanySettings = createAsyncThunk( 'companySettings/fetchCompanySettings', async (_, { rejectWithValue }) => { - try { - const { data } = await axios.get(ENDPOINTS.companySettings); + return matchClientResult( + await companySettingsClient.GetCompanySettingsAsync(new AbortController().signal), + (data) => { + saveToLocalStorage(COMPANY_SETTINGS_KEY, data); - saveToLocalStorage(COMPANY_SETTINGS_KEY, data); - - return data || {}; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.companySettings.notFound, - }); - } + return data || {}; + }, + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.companySettings.notFound })) + ); } ); -export const createCompanySettings = createAsyncThunk, { rejectValue: ErrorResponse }>( +export const createCompanySettings = createAsyncThunk, { rejectValue: ErrorResponse }>( 'companySettings/createCompanySettings', async (companySettings, { dispatch, rejectWithValue }) => { - try { - await axios.post(ENDPOINTS.companySettings, companySettings); - saveToLocalStorage(COMPANY_SETTINGS_KEY, companySettings); - await dispatch(fetchCompanySettings()).unwrap(); + const modModel = new CompanySettingsModificationModel(companySettings.colorSchemeId!); + + return matchClientUnitResult( + await companySettingsClient.CreateCompanySettingsAsync(modModel, new AbortController().signal), + async () => { + saveToLocalStorage(COMPANY_SETTINGS_KEY, companySettings); + await dispatch(fetchCompanySettings()).unwrap(); - dispatch(setSuccess(MESSAGES.success.companySettings.create)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.companySettings.create, - }) - ); + dispatch(setSuccess(MESSAGES.success.companySettings.create)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.companySettings.create }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(problem) as ErrorResponse; - return rejectWithValue(mappedError); - } + return rejectWithValue(mappedError); + } + ); } ); -export const editCompanySettings = createAsyncThunk, { rejectValue: ErrorResponse }>( +export const editCompanySettings = createAsyncThunk, { rejectValue: ErrorResponse }>( 'companySettings/editCompanySettings', async (companySettings, { dispatch, rejectWithValue }) => { - try { - await axios.put(ENDPOINTS.companySettings, companySettings); + const modModel = new CompanySettingsModificationModel(companySettings.colorSchemeId!); - await dispatch(fetchCompanySettings()).unwrap(); + return matchClientUnitResult( + await companySettingsClient.UpdateCompanySettingsAsync(modModel, new AbortController().signal), + async () => { + await dispatch(fetchCompanySettings()).unwrap(); - saveToLocalStorage(COMPANY_SETTINGS_KEY, companySettings); - dispatch(setSuccess(MESSAGES.success.companySettings.update)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.companySettings.update, - }) - ); + saveToLocalStorage(COMPANY_SETTINGS_KEY, companySettings); + dispatch(setSuccess(MESSAGES.success.companySettings.update)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.companySettings.update }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(problem) as ErrorResponse; - return rejectWithValue(mappedError); - } + return rejectWithValue(mappedError); + } + ); } ); -export const deleteCompanySettings = createAsyncThunk( +export const deleteCompanySettings = createAsyncThunk( 'companySettings/deleteCompanySettings', async (_, { dispatch, rejectWithValue }) => { - try { - await axios.delete(ENDPOINTS.companySettings); + return matchClientUnitResult( + await companySettingsClient.DeleteCompanySettingsAsync(new AbortController().signal), + async () => { + removeFromLocalStorage(COMPANY_SETTINGS_KEY); + dispatch(setSuccess(MESSAGES.success.companySettings.delete)); + try { + await dispatch(fetchCompanySettings()).unwrap(); + } catch { + // Ignored: fetchCompanySettings will return 404 after delete, which is expected. + } + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.companySettings.delete }))); - removeFromLocalStorage(COMPANY_SETTINGS_KEY); - dispatch(setSuccess(MESSAGES.success.companySettings.delete)); - try { - await dispatch(fetchCompanySettings()).unwrap(); - } catch { - // Ignored: fetchCompanySettings will return 404 after delete, which is expected. + return rejectWithValue(problem); } - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.companySettings.delete, - }) - ); - - return rejectWithValue(error as ErrorResponseDTO); - } + ); } ); diff --git a/src/store/thunks/companyThunk.ts b/src/store/thunks/companyThunk.ts index 57f274d9..34f630d1 100644 --- a/src/store/thunks/companyThunk.ts +++ b/src/store/thunks/companyThunk.ts @@ -3,113 +3,101 @@ import { FieldValues } from 'react-hook-form'; import { RootState } from 'store'; import { fetchBranchesTotal, fetchDepartmentsTotal, fetchEmployeesTotal } from 'store/thunks'; import { setError, setSuccess } from 'store/features'; -import axios from 'shared/configs/axios'; -import { Company, CompanyDTO, EntityInput, ErrorResponse, ErrorResponseDTO } from 'shared/types'; -import { mapCompany, mapError } from 'shared/helpers'; -import { MESSAGES, ENDPOINTS } from 'shared/constants'; - -export const fetchCompany = createAsyncThunk( +import { Company, EntityInput, ErrorResponse, ProblemDetailsModel } from 'shared/types'; +import { getProblemStatus, mapCompany, mapError, createProblemDetails } from 'shared/helpers'; +import { MESSAGES } from 'shared/constants'; +import { companyClient } from 'shared/configs/BridgeClients'; +import { matchClientResult, matchClientUnitResult } from 'shared/configs/BridgeResponses'; +import { CompanyModificationModel } from '@fossa-app/bridge/Models/ApiModels/PayloadModels'; + +export const fetchCompany = createAsyncThunk( 'company/fetchCompany', async (_, { getState, rejectWithValue }) => { - try { - const { data } = await axios.get(ENDPOINTS.company); - - if (data) { + return matchClientResult( + await companyClient.GetCompanyAsync(new AbortController().signal), + (data) => { const state = getState() as RootState; const countries = state.license.system.item?.entitlements.countries || []; return mapCompany(data, countries); - } - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.company.notFound, - }); - } + }, + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.company.notFound })) + ); } ); -export const createCompany = createAsyncThunk }>( +export const createCompany = createAsyncThunk }>( 'company/createCompany', async (company, { dispatch, rejectWithValue }) => { - try { - await axios.post(ENDPOINTS.company, company); - await dispatch(fetchCompany(false)).unwrap(); + const modModel = new CompanyModificationModel(company.name, company.countryCode ?? null); - dispatch(setSuccess(MESSAGES.success.company.create)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.company.create, - }) - ); + return matchClientUnitResult( + await companyClient.CreateCompanyAsync(modModel, new AbortController().signal), + async () => { + await dispatch(fetchCompany(false)).unwrap(); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + dispatch(setSuccess(MESSAGES.success.company.create)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.company.create }))); - return rejectWithValue(mappedError); - } + const mappedError = mapError(problem) as ErrorResponse; + + return rejectWithValue(mappedError); + } + ); } ); -export const editCompany = createAsyncThunk, { rejectValue: ErrorResponse }>( +export const editCompany = createAsyncThunk, { rejectValue: ErrorResponse }>( 'company/editCompany', async (company, { dispatch, rejectWithValue }) => { - try { - await axios.put(ENDPOINTS.company, company); + const modModel = new CompanyModificationModel(company.name, company.countryCode ?? null); - dispatch(setSuccess(MESSAGES.success.company.update)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.company.update, - }) - ); + return matchClientUnitResult( + await companyClient.UpdateCompanyAsync(modModel, new AbortController().signal), + () => { + dispatch(setSuccess(MESSAGES.success.company.update)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.company.update }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(problem) as ErrorResponse; - return rejectWithValue(mappedError); - } + return rejectWithValue(mappedError); + } + ); } ); -export const deleteCompany = createAsyncThunk( +export const deleteCompany = createAsyncThunk( 'company/deleteCompany', async (_, { dispatch, rejectWithValue }) => { - try { - await axios.delete(ENDPOINTS.company); - - dispatch(setSuccess(MESSAGES.success.company.delete)); - - try { - await dispatch(fetchCompany()).unwrap(); - } catch { - // Ignored: fetchCompany will return 404 after delete, which is expected. - } - } catch (error) { - if ((error as ErrorResponseDTO).status === 424) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.company.deleteDependency, - }) - ); - } else { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.company.delete, - }) - ); + return matchClientUnitResult( + await companyClient.DeleteCompanyAsync(new AbortController().signal), + async () => { + dispatch(setSuccess(MESSAGES.success.company.delete)); + + try { + await dispatch(fetchCompany()).unwrap(); + } catch { + // Ignored: fetchCompany will return 404 after delete, which is expected. + } + }, + (problem) => { + if (getProblemStatus(problem) === 424) { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.company.deleteDependency }))); + } else { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.company.delete }))); + } + + return rejectWithValue(problem); } - - return rejectWithValue(error as ErrorResponseDTO); - } + ); } ); -export const fetchCompanyDatasourceTotals = createAsyncThunk( +export const fetchCompanyDatasourceTotals = createAsyncThunk( 'company/fetchCompanyDatasourceTotals', async (_, { dispatch }) => { try { diff --git a/src/store/thunks/departmentThunk.ts b/src/store/thunks/departmentThunk.ts index 64bb2b9b..d70f5be8 100644 --- a/src/store/thunks/departmentThunk.ts +++ b/src/store/thunks/departmentThunk.ts @@ -9,28 +9,22 @@ import { resetDepartmentsFetchStatus, } from 'store/features'; import { fetchEmployeeById, fetchEmployeesByIds } from 'store/thunks'; -import axios from 'shared/configs/axios'; -import { - ErrorResponseDTO, - ErrorResponse, - PaginatedResponse, - PaginationParams, - Department, - DepartmentDTO, - EmployeeDTO, - EntityInput, -} from 'shared/types'; -import { MESSAGES, ENDPOINTS } from 'shared/constants'; -import { - prepareQueryParams, - mapDepartments, - mapError, - mapDepartment, - prepareCommaSeparatedQueryParamsByKey, - getEntityIdsByField, -} from 'shared/helpers'; - -const fetchParentDepartment = async (dispatch: ThunkDispatch, id: string) => { +import { ProblemDetailsModel, ErrorResponse, PaginatedResponse, PaginationParams, Department, Employee, EntityInput } from 'shared/types'; +import { MESSAGES } from 'shared/constants'; +import { departmentClient } from 'shared/configs/BridgeClients'; +import { matchClientResult, matchClientUnitResult, toPaginatedResponse } from 'shared/configs/BridgeResponses'; +import { DepartmentQueryRequestModel, DepartmentModificationModel } from '@fossa-app/bridge/Models/ApiModels/PayloadModels'; +import { mapDepartments, mapError, mapDepartment, getEntityIdsByField, createProblemDetails } from 'shared/helpers'; + +const nullableBigInt = (value: number | null | undefined): bigint | null => { + if (value === null || value === undefined) { + return null; + } + + return BigInt(value); +}; + +const fetchParentDepartment = async (dispatch: ThunkDispatch, id: string): Promise => { return dispatch( fetchDepartmentById({ id, @@ -42,37 +36,34 @@ const fetchParentDepartment = async (dispatch: ThunkDispatch | undefined, + PaginatedResponse | undefined, void, - { rejectValue: ErrorResponseDTO } + { rejectValue: ProblemDetailsModel } >('department/fetchDepartmentsTotal', async (_, { rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber: 1, pageSize: 1 }); - const { data } = await axios.get>(`${ENDPOINTS.departments}?${queryParams}`); - - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.departments.notFound, - }); - } + const query = new DepartmentQueryRequestModel([], '', 1, 1); + + return matchClientResult( + await departmentClient.GetDepartmentsAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.departments.notFound })) + ); }); export const fetchDepartments = createAsyncThunk< PaginatedResponse | undefined, Partial & { shouldFetchEmployees?: boolean }, - { rejectValue: ErrorResponseDTO } + { rejectValue: ProblemDetailsModel } >('department/fetchDepartments', async ({ pageNumber, pageSize, search, shouldFetchEmployees = true }, { dispatch, rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber, pageSize, search }); - const { data } = await axios.get>(`${ENDPOINTS.departments}?${queryParams}`); + const query = new DepartmentQueryRequestModel([], search || '', pageNumber || null, pageSize || null); - if (data) { + return matchClientResult( + await departmentClient.GetDepartmentsAsync(query, new AbortController().signal), + async (response) => { + const data = toPaginatedResponse(response); const departmentsManagerIds = getEntityIdsByField(data.items, 'managerId'); const parentDepartmentsIds = getEntityIdsByField(data.items, 'parentDepartmentId'); - let employees: PaginatedResponse | undefined; - let parentDepartments: PaginatedResponse | undefined; + let employees: PaginatedResponse | undefined; + let parentDepartments: PaginatedResponse | undefined; if (shouldFetchEmployees && departmentsManagerIds.length) { employees = await dispatch(fetchEmployeesByIds(departmentsManagerIds)).unwrap(); @@ -86,85 +77,72 @@ export const fetchDepartments = createAsyncThunk< ...data, items: mapDepartments(data.items, parentDepartments?.items, employees?.items), }; - } - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.departments.notFound, - }); - } + }, + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.departments.notFound })) + ); }); export const fetchSearchedDepartments = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { rejectValue: ErrorResponseDTO } + { rejectValue: ProblemDetailsModel } >('department/fetchSearchedDepartments', async ({ pageNumber, pageSize, search }, { rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber, pageSize, search }); - const { data } = await axios.get>(`${ENDPOINTS.departments}?${queryParams}`); - - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.departments.notFound, - }); - } + const query = new DepartmentQueryRequestModel([], search || '', pageNumber || null, pageSize || null); + + return matchClientResult( + await departmentClient.GetDepartmentsAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.departments.notFound })) + ); }); export const fetchParentDepartments = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { state: RootState; rejectValue: ErrorResponseDTO } + { state: RootState; rejectValue: ProblemDetailsModel } >('department/fetchParentDepartments', async ({ pageNumber, pageSize, search }, { rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber, pageSize, search }); - const { data } = await axios.get>(`${ENDPOINTS.departments}?${queryParams}`); - - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.departments.notFound, - }); - } + const query = new DepartmentQueryRequestModel([], search || '', pageNumber || null, pageSize || null); + + return matchClientResult( + await departmentClient.GetDepartmentsAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.departments.notFound })) + ); }); export const fetchAssignedDepartments = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { state: RootState; rejectValue: ErrorResponseDTO } + { state: RootState; rejectValue: ProblemDetailsModel } >('department/fetchAssignedDepartments', async ({ pageNumber, pageSize, search }, { rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber, pageSize, search }); - const { data } = await axios.get>(`${ENDPOINTS.departments}?${queryParams}`); - - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.departments.notFound, - }); - } + const query = new DepartmentQueryRequestModel([], search || '', pageNumber || null, pageSize || null); + + return matchClientResult( + await departmentClient.GetDepartmentsAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.departments.notFound })) + ); }); export const fetchDepartmentsByIds = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, number[], - { rejectValue: ErrorResponseDTO } + { rejectValue: ProblemDetailsModel } >('department/fetchDepartmentsByIds', async (ids, { rejectWithValue }) => { + let idList: bigint[] = []; try { - const queryParams = prepareCommaSeparatedQueryParamsByKey('id', ids); - const { data } = await axios.get>(`${ENDPOINTS.departments}?${queryParams}`); - - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.departments.notFound, - }); + idList = ids.map((id) => BigInt(id)); + } catch { + // Ignored } + + const query = new DepartmentQueryRequestModel(idList, '', null, null); + + return matchClientResult( + await departmentClient.GetDepartmentsAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.departments.notFound })) + ); }); export const fetchDepartmentById = createAsyncThunk< @@ -175,106 +153,113 @@ export const fetchDepartmentById = createAsyncThunk< shouldFetchParent?: boolean; shouldFetchDepartmentManager?: boolean; }, - { rejectValue: ErrorResponseDTO } + { rejectValue: ProblemDetailsModel } >( 'department/fetchDepartmentById', async ({ id, shouldFetchParent = true, shouldFetchDepartmentManager = true }, { dispatch, rejectWithValue }): Promise => { try { - const { data } = await axios.get(`${ENDPOINTS.departments}/${id}`); - - let parentDepartment: Department | undefined; - let manager: EmployeeDTO | undefined; - - if (data.parentDepartmentId && shouldFetchParent) { - parentDepartment = await fetchParentDepartment(dispatch, String(data.parentDepartmentId)); - } - - if (data.managerId && shouldFetchDepartmentManager) { - manager = await dispatch( - fetchEmployeeById({ - id: String(data.managerId), - skipState: true, - shouldFetchBranch: false, - shouldFetchDepartment: false, - shouldFetchEmployeeManager: false, - }) - ).unwrap(); - } - - return mapDepartment(data, parentDepartment, manager); + return matchClientResult( + await departmentClient.GetDepartmentAsync(BigInt(id), new AbortController().signal), + async (data) => { + let parentDepartment: Department | undefined; + let manager: Employee | undefined; + + if (data.parentDepartmentId && shouldFetchParent) { + parentDepartment = await fetchParentDepartment(dispatch, String(data.parentDepartmentId)); + } + + if (data.managerId && shouldFetchDepartmentManager) { + manager = await dispatch( + fetchEmployeeById({ + id: String(data.managerId), + skipState: true, + shouldFetchBranch: false, + shouldFetchDepartment: false, + shouldFetchEmployeeManager: false, + }) + ).unwrap(); + } + + return mapDepartment(data, parentDepartment, manager); + }, + (problem) => rejectWithValue(problem) as never + ); } catch (error) { - return rejectWithValue(error as ErrorResponseDTO) as unknown as Department; + return rejectWithValue(error as ProblemDetailsModel) as never; } } ); -export const createDepartment = createAsyncThunk, { rejectValue: ErrorResponse }>( +export const createDepartment = createAsyncThunk, { rejectValue: ErrorResponse }>( 'department/createDepartment', async (department, { dispatch, rejectWithValue }) => { - try { - await axios.post(ENDPOINTS.departments, department); + const modModel = new DepartmentModificationModel( + department.name, + nullableBigInt(department.parentDepartmentId), + nullableBigInt(department.managerId) + ); - dispatch(resetParentDepartments()); + return matchClientUnitResult( + await departmentClient.CreateDepartmentAsync(modModel, new AbortController().signal), + () => { + dispatch(resetParentDepartments()); - dispatch(setSuccess(MESSAGES.success.departments.create)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.departments.create, - }) - ); + dispatch(setSuccess(MESSAGES.success.departments.create)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.departments.create }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(problem) as ErrorResponse; - return rejectWithValue(mappedError); - } + return rejectWithValue(mappedError); + } + ); } ); -export const editDepartment = createAsyncThunk], { rejectValue: ErrorResponse }>( +export const editDepartment = createAsyncThunk], { rejectValue: ErrorResponse }>( 'department/editDepartment', async ([id, department], { dispatch, rejectWithValue }) => { - try { - await axios.put(`${ENDPOINTS.departments}/${id}`, department); + const modModel = new DepartmentModificationModel( + department.name, + nullableBigInt(department.parentDepartmentId), + nullableBigInt(department.managerId) + ); - dispatch(resetParentDepartments()); + return matchClientUnitResult( + await departmentClient.UpdateDepartmentAsync(BigInt(id), modModel, new AbortController().signal), + () => { + dispatch(resetParentDepartments()); - dispatch(setSuccess(MESSAGES.success.departments.update)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.departments.update, - }) - ); + dispatch(setSuccess(MESSAGES.success.departments.update)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.departments.update }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(problem) as ErrorResponse; - return rejectWithValue(mappedError); - } + return rejectWithValue(mappedError); + } + ); } ); -export const deleteDepartment = createAsyncThunk( +export const deleteDepartment = createAsyncThunk( 'department/deleteDepartment', async (id, { dispatch, rejectWithValue }) => { - try { - await axios.delete(`${ENDPOINTS.departments}/${id}`); - - dispatch(resetDepartmentsFetchStatus()); - dispatch(resetParentDepartments()); - dispatch(resetCompanyDatasourceTotalsFetchStatus()); - dispatch(setSuccess(MESSAGES.success.departments.delete)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.departments.delete, - }) - ); - - return rejectWithValue(error as ErrorResponseDTO); - } + return matchClientUnitResult( + await departmentClient.DeleteDepartmentAsync(BigInt(id), new AbortController().signal), + () => { + dispatch(resetDepartmentsFetchStatus()); + dispatch(resetParentDepartments()); + dispatch(resetCompanyDatasourceTotalsFetchStatus()); + dispatch(setSuccess(MESSAGES.success.departments.delete)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.departments.delete }))); + + return rejectWithValue(problem); + } + ); } ); diff --git a/src/store/thunks/employeeThunk.ts b/src/store/thunks/employeeThunk.ts index 95846222..4ef0c359 100644 --- a/src/store/thunks/employeeThunk.ts +++ b/src/store/thunks/employeeThunk.ts @@ -3,28 +3,20 @@ import { FieldValues } from 'react-hook-form'; import { RootState } from 'store'; import { fetchBranchById, fetchBranchesByIds, fetchDepartmentById, fetchDepartmentsByIds } from 'store/thunks'; import { resetOrgChartEmployeesFetchStatus, setError, setSuccess } from 'store/features'; -import axios from 'shared/configs/axios'; -import { - Branch, - BranchDTO, - Department, - DepartmentDTO, - Employee, - EmployeeDTO, - ErrorResponse, - ErrorResponseDTO, - PaginatedResponse, - PaginationParams, -} from 'shared/types'; -import { MESSAGES, ENDPOINTS } from 'shared/constants'; -import { - mapEmployee, - mapEmployees, - prepareQueryParams, - mapError, - prepareCommaSeparatedQueryParamsByKey, - getEntityIdsByField, -} from 'shared/helpers'; +import { Branch, Department, Employee, ErrorResponse, ProblemDetailsModel, PaginatedResponse, PaginationParams } from 'shared/types'; +import { MESSAGES } from 'shared/constants'; +import { employeeClient } from 'shared/configs/BridgeClients'; +import { matchClientResult, matchClientUnitResult, toPaginatedResponse } from 'shared/configs/BridgeResponses'; +import { EmployeeQueryRequestModel, EmployeeManagementModel } from '@fossa-app/bridge/Models/ApiModels/PayloadModels'; +import { mapEmployee, mapEmployees, mapError, getEntityIdsByField, createProblemDetails } from 'shared/helpers'; + +const nullableBigInt = (value: number | null | undefined): bigint | null => { + if (value === null || value === undefined) { + return null; + } + + return BigInt(value); +}; const fetchManager = async (dispatch: ThunkDispatch, id: string) => { return dispatch( @@ -46,24 +38,25 @@ export const fetchEmployees = createAsyncThunk< shouldFetchDepartments?: boolean; shouldFetchManagers?: boolean; }, - { rejectValue: ErrorResponseDTO } + { rejectValue: ProblemDetailsModel } >( 'employee/fetchEmployees', async ( { pageNumber, pageSize, search, shouldFetchBranches = true, shouldFetchDepartments = true, shouldFetchManagers = true }, { dispatch, rejectWithValue } ) => { - try { - const queryParams = prepareQueryParams({ pageNumber, pageSize, search }); - const { data } = await axios.get>(`${ENDPOINTS.employees}?${queryParams}`); + const query = new EmployeeQueryRequestModel([], search || '', pageNumber || null, pageSize || null, null, null); - if (data) { + return matchClientResult( + await employeeClient.GetEmployeesAsync(query, new AbortController().signal), + async (response) => { + const data = toPaginatedResponse(response); const assignedBranchIds = getEntityIdsByField(data.items, 'assignedBranchId'); const assignedDepartmentIds = getEntityIdsByField(data.items, 'assignedDepartmentId'); const managerIds = getEntityIdsByField(data.items, 'reportsToId'); - let branches: PaginatedResponse | undefined; - let departments: PaginatedResponse | undefined; - let managers: PaginatedResponse | undefined; + let branches: PaginatedResponse | undefined; + let departments: PaginatedResponse | undefined; + let managers: PaginatedResponse | undefined; if (shouldFetchBranches && assignedBranchIds.length) { branches = await dispatch(fetchBranchesByIds(assignedBranchIds)).unwrap(); @@ -86,44 +79,48 @@ export const fetchEmployees = createAsyncThunk< managers: managers?.items, }), }; - } - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.notFound, - }); - } + }, + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.employee.notFound })) + ); } ); export const fetchManagers = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { state: RootState; rejectValue: ErrorResponseDTO } + { state: RootState; rejectValue: ProblemDetailsModel } >('employee/fetchManagers', async ({ pageNumber, pageSize, search }, { rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber, pageSize, search }); - const { data } = await axios.get>(`${ENDPOINTS.employees}?${queryParams}`); + const query = new EmployeeQueryRequestModel([], search || '', pageNumber || null, pageSize || null, null, null); - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.notFound, - }); - } + return matchClientResult( + await employeeClient.GetEmployeesAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.employee.notFound })) + ); }); export const fetchOrgChartEmployees = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { state: RootState; rejectValue: ErrorResponseDTO } + { state: RootState; rejectValue: ProblemDetailsModel } >('employee/fetchOrgChartEmployees', async ({ pageNumber, pageSize }, { rejectWithValue }) => { try { - const fetchSubordinates = async (reportsTo?: EmployeeDTO): Promise => { - const queryParams = { pageNumber, pageSize, reportsToId: reportsTo?.id, topLevelOnly: !reportsTo }; - const subordinatesQuery = prepareQueryParams(queryParams); - const { data: subordinateData } = await axios.get>(`${ENDPOINTS.employees}?${subordinatesQuery}`); + const fetchSubordinates = async (reportsTo?: Employee): Promise => { + const query = new EmployeeQueryRequestModel( + [], + '', + pageNumber || null, + pageSize || null, + reportsTo?.id ? BigInt(reportsTo.id) : null, + !reportsTo + ); + const subordinateData = await matchClientResult( + await employeeClient.GetEmployeesAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => { + throw createProblemDetails(problem, { Title: MESSAGES.error.employee.notFound }); + } + ); return subordinateData.items.concat((await Promise.all(subordinateData.items.map(fetchSubordinates))).flat()); }; @@ -132,27 +129,20 @@ export const fetchOrgChartEmployees = createAsyncThunk< items: await fetchSubordinates(), }; } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.notFound, - }); + return rejectWithValue(createProblemDetails(error, { Title: MESSAGES.error.employee.notFound })); } }); -export const fetchEmployeesTotal = createAsyncThunk | undefined, void, { rejectValue: ErrorResponseDTO }>( +export const fetchEmployeesTotal = createAsyncThunk | undefined, void, { rejectValue: ProblemDetailsModel }>( 'employee/fetchEmployeesTotal', async (_, { rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber: 1, pageSize: 1 }); - const { data } = await axios.get>(`${ENDPOINTS.employees}?${queryParams}`); + const query = new EmployeeQueryRequestModel([], '', 1, 1, null, null); - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.notFound, - }); - } + return matchClientResult( + await employeeClient.GetEmployeesAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.employee.notFound })) + ); } ); @@ -166,7 +156,7 @@ export const fetchEmployeeById = createAsyncThunk< shouldFetchDepartment?: boolean; shouldFetchEmployeeManager?: boolean; }, - { rejectValue: ErrorResponseDTO } + { rejectValue: ProblemDetailsModel } >( 'employee/fetchEmployeeById', async ( @@ -174,69 +164,101 @@ export const fetchEmployeeById = createAsyncThunk< { dispatch, rejectWithValue } ) => { try { - const { data } = await axios.get(`${ENDPOINTS.employees}/${id}`); - let branch: Branch | undefined; - let department: Department | undefined; - let manager: Employee | undefined; - - if (data.assignedBranchId && shouldFetchBranch) { - branch = await dispatch( - fetchBranchById({ id: String(data.assignedBranchId), skipState: true, shouldFetchBranchGeoAddress }) - ).unwrap(); - } + return matchClientResult( + await employeeClient.GetEmployeeAsync(BigInt(id), new AbortController().signal), + async (data) => { + let branch: Branch | undefined; + let department: Department | undefined; + let manager: Employee | undefined; - if (data.assignedDepartmentId && shouldFetchDepartment) { - department = await dispatch(fetchDepartmentById({ id: String(data.assignedDepartmentId), skipState: true })).unwrap(); - } + if (data.assignedBranchId && shouldFetchBranch) { + branch = await dispatch( + fetchBranchById({ id: String(data.assignedBranchId), skipState: true, shouldFetchBranchGeoAddress }) + ).unwrap(); + } - if (data.reportsToId && shouldFetchEmployeeManager) { - manager = (await fetchManager(dispatch, String(data.reportsToId))) as Employee; - } + if (data.assignedDepartmentId && shouldFetchDepartment) { + department = await dispatch(fetchDepartmentById({ id: String(data.assignedDepartmentId), skipState: true })).unwrap(); + } - return mapEmployee({ branch, department, manager, employee: data, user: undefined }); + if (data.reportsToId && shouldFetchEmployeeManager) { + manager = (await fetchManager(dispatch, String(data.reportsToId))) as Employee; + } + + return mapEmployee({ branch, department, manager, employee: data, user: undefined }); + }, + (problem) => rejectWithValue(problem) as never + ); } catch (error) { - return rejectWithValue(error as ErrorResponseDTO); + return rejectWithValue(error as ProblemDetailsModel); } } ); export const fetchEmployeesByIds = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, number[], - { rejectValue: ErrorResponseDTO } + { rejectValue: ProblemDetailsModel } >('employee/fetchEmployeesByIds', async (ids, { rejectWithValue }) => { + let idList: bigint[] = []; try { - const queryParams = prepareCommaSeparatedQueryParamsByKey('id', ids); - const { data } = await axios.get>(`${ENDPOINTS.employees}?${queryParams}`); - - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.notFound, - }); + idList = ids.map((id) => BigInt(id)); + } catch { + // Ignored } + + const query = new EmployeeQueryRequestModel(idList, '', null, null, null, null); + + return matchClientResult( + await employeeClient.GetEmployeesAsync(query, new AbortController().signal), + (response) => toPaginatedResponse(response), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.employee.notFound })) + ); }); export const editEmployee = createAsyncThunk< void, - [string, Pick], + [string, Pick], { rejectValue: ErrorResponse } >('employee/editEmployee', async ([id, employee], { dispatch, rejectWithValue }) => { try { - await axios.put(`${ENDPOINTS.employees}/${id}`, employee); + return matchClientResult( + await employeeClient.GetEmployeeAsync(BigInt(id), new AbortController().signal), + async (curEmp) => { + const modModel = new EmployeeManagementModel( + nullableBigInt(employee.assignedBranchId), + nullableBigInt(curEmp.assignedDepartmentId), + nullableBigInt(curEmp.reportsToId), + curEmp.jobTitle ?? null + ); - dispatch(resetOrgChartEmployeesFetchStatus()); - dispatch(setSuccess(MESSAGES.success.employee.updateEmployee)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.updateEmployee, - }) + return matchClientUnitResult( + await employeeClient.ManageEmployeeAsync(BigInt(id), modModel, new AbortController().signal), + () => { + dispatch(resetOrgChartEmployeesFetchStatus()); + dispatch(setSuccess(MESSAGES.success.employee.updateEmployee)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.employee.updateEmployee }))); + + const mappedError = mapError(problem) as ErrorResponse; + + return rejectWithValue(mappedError); + } + ); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.employee.updateEmployee }))); + + const mappedError = mapError(problem) as ErrorResponse; + + return rejectWithValue(mappedError); + } ); + } catch (error) { + dispatch(setError(createProblemDetails(error, { Title: MESSAGES.error.employee.updateEmployee }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(error as ProblemDetailsModel) as ErrorResponse; return rejectWithValue(mappedError); } diff --git a/src/store/thunks/identityThunk.ts b/src/store/thunks/identityThunk.ts index d87e7aa6..e4735e5f 100644 --- a/src/store/thunks/identityThunk.ts +++ b/src/store/thunks/identityThunk.ts @@ -1,33 +1,29 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; +import { createProblemDetails } from 'shared/helpers'; import { updateAuthSettings } from 'store/features'; -import axios from 'shared/configs/axios'; -import { Client, ErrorResponseDTO } from 'shared/types'; -import { MESSAGES, ROUTES, ENDPOINTS } from 'shared/constants'; -import { parseResponse } from 'shared/helpers'; +import { ProblemDetailsModel } from 'shared/types'; +import { IdentityClientRetrievalModel } from '@fossa-app/bridge/Models/ApiModels/PayloadModels'; +import { MESSAGES, ROUTES } from 'shared/constants'; +import { identityClient } from 'shared/configs/BridgeClients'; +import { matchClientResult } from 'shared/configs/BridgeResponses'; -export const fetchClient = createAsyncThunk( +export const fetchClient = createAsyncThunk( 'identity/fetchClient', async (_, { dispatch, rejectWithValue }) => { - try { - const response = await axios.get<{ data: Client }>(`${ENDPOINTS.client}?origin=${window.location.origin}`); - // TODO: this should be handled in AxiosInterceptor, but this method is not being called in axios response - const parsedResponse = parseResponse<{ data: Client }>(response); - - if (parsedResponse?.data) { + return matchClientResult( + await identityClient.GetClientAsync(window.location.origin, new AbortController().signal), + (parsedResponse) => { dispatch( updateAuthSettings({ - client_id: parsedResponse.data.clientId, + client_id: parsedResponse.ClientId ?? undefined, redirect_uri: `${window.location.origin}${ROUTES.callback.path}`, post_logout_redirect_uri: `${window.location.origin}/`, }) ); - return parsedResponse.data; - } - - return rejectWithValue({ title: MESSAGES.error.client.notFound }); - } catch (error: any) { - return rejectWithValue(parseResponse<{ data: ErrorResponseDTO }>(error.response).data); - } + return parsedResponse; + }, + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.client.notFound })) + ); } ); diff --git a/src/store/thunks/licenseThunk.ts b/src/store/thunks/licenseThunk.ts index 722fa47a..7b53e9b5 100644 --- a/src/store/thunks/licenseThunk.ts +++ b/src/store/thunks/licenseThunk.ts @@ -1,39 +1,34 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { FieldValues } from 'react-hook-form'; import { setError, setSuccess } from 'store/features'; -import axios from 'shared/configs/axios'; -import { CompanyLicense, ErrorResponse, ErrorResponseDTO, SystemLicense } from 'shared/types'; -import { MESSAGES, ENDPOINTS } from 'shared/constants'; -import { mapCompanyLicense, mapError, parseResponse } from 'shared/helpers'; +import { CompanyLicense, ErrorResponse, ProblemDetailsModel, SystemLicense } from 'shared/types'; +import { MESSAGES } from 'shared/constants'; +import { Endpoints_CompanyLicense, Endpoints_BasePath } from '@fossa-app/bridge/Services/Endpoints'; +import { mapCompanyLicense, mapError, createProblemDetails } from 'shared/helpers'; +import { systemLicenseClient, companyLicenseClient } from 'shared/configs/BridgeClients'; +import { matchClientResult } from 'shared/configs/BridgeResponses'; +import { AppAccessTokenProvider } from 'shared/configs/BridgeTransport'; +import { getBackendOrigin } from '@fossa-app/bridge/Services/UrlHelpers'; -export const fetchSystemLicense = createAsyncThunk( +export const fetchSystemLicense = createAsyncThunk( 'license/fetchSystemLicense', async (_, { rejectWithValue }) => { - try { - const response = await axios.get<{ data: SystemLicense }>(ENDPOINTS.systemLicense); - // TODO: this should be handled in AxiosInterceptor, but this method is not being called in axios response - const parsedResponse = parseResponse<{ data: SystemLicense }>(response); - - return parsedResponse.data || rejectWithValue({ title: MESSAGES.error.license.system.notFound }); - } catch (error: any) { - return rejectWithValue(parseResponse<{ data: ErrorResponseDTO }>(error.response).data); - } + return matchClientResult( + await systemLicenseClient.GetLicenseAsync(new AbortController().signal), + (data) => data || rejectWithValue(createProblemDetails(undefined, { Title: MESSAGES.error.license.system.notFound })), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.license.system.notFound })) + ); } ); -export const fetchCompanyLicense = createAsyncThunk( +export const fetchCompanyLicense = createAsyncThunk( 'license/fetchCompanyLicense', async (_, { rejectWithValue }) => { - try { - const { data } = await axios.get(ENDPOINTS.companyLicense); - - return mapCompanyLicense(data); - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.license.company.notFound, - }); - } + return matchClientResult( + await companyLicenseClient.GetLicenseAsync(new AbortController().signal), + (data) => mapCompanyLicense(data), + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.license.company.notFound })) + ); } ); @@ -45,24 +40,28 @@ export const uploadCompanyLicense = createAsyncThunk(ENDPOINTS.companyLicense, formData, config); dispatch(fetchCompanyLicense()); dispatch(setSuccess(MESSAGES.success.license.company.create)); } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.license.company.create, - }) - ); + dispatch(setError(createProblemDetails(error, { Title: MESSAGES.error.license.company.create }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(error as ProblemDetailsModel) as ErrorResponse; return rejectWithValue(mappedError); } diff --git a/src/store/thunks/onboardingThunk.ts b/src/store/thunks/onboardingThunk.ts index d49ca093..8368294a 100644 --- a/src/store/thunks/onboardingThunk.ts +++ b/src/store/thunks/onboardingThunk.ts @@ -1,8 +1,8 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { fetchBranchesTotal, fetchCompanySettings, fetchCompany, fetchCompanyLicense, fetchProfile } from 'store/thunks'; -import { ErrorResponseDTO } from 'shared/types'; +import { ProblemDetailsModel } from 'shared/types'; -export const fetchOnboardingData = createAsyncThunk( +export const fetchOnboardingData = createAsyncThunk( 'onboarding/fetchOnboardingData', async (_, { dispatch }) => { try { diff --git a/src/store/thunks/profileThunk.ts b/src/store/thunks/profileThunk.ts index 033db1c7..e278db86 100644 --- a/src/store/thunks/profileThunk.ts +++ b/src/store/thunks/profileThunk.ts @@ -2,104 +2,91 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { FieldValues } from 'react-hook-form'; import { RootState } from 'store'; import { setError, setSuccess, resetCompanyDatasourceTotalsFetchStatus } from 'store/features'; -import axios from 'shared/configs/axios'; -import { Employee, EmployeeDTO, EntityInput, ErrorResponse, ErrorResponseDTO } from 'shared/types'; -import { MESSAGES, ENDPOINTS } from 'shared/constants'; -import { mapEmployee, mapError } from 'shared/helpers'; +import { Employee, EntityInput, ErrorResponse, ProblemDetailsModel } from 'shared/types'; +import { MESSAGES } from 'shared/constants'; +import { employeeClient } from 'shared/configs/BridgeClients'; +import { matchClientResult, matchClientUnitResult } from 'shared/configs/BridgeResponses'; +import { EmployeeModificationModel } from '@fossa-app/bridge/Models/ApiModels/PayloadModels'; +import { getProblemStatus, mapEmployee, mapError, createProblemDetails } from 'shared/helpers'; -export const fetchProfile = createAsyncThunk( +export const fetchProfile = createAsyncThunk( 'profile/fetchProfile', async (_, { getState, rejectWithValue }) => { - try { - const { data } = await axios.get(ENDPOINTS.employee); - - if (data) { + return matchClientResult( + await employeeClient.GetCurrentEmployeeAsync(new AbortController().signal), + (data) => { const state = getState() as RootState; const user = state.auth.user.item; return mapEmployee({ user, employee: data }); - } - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.notFound, - }); - } + }, + (problem) => rejectWithValue(createProblemDetails(problem, { Title: MESSAGES.error.employee.notFound })) + ); } ); -export const createProfile = createAsyncThunk< - void, - EntityInput, - { state: RootState; rejectValue: ErrorResponse } ->('profile/createProfile', async (employee, { dispatch, rejectWithValue }) => { - try { - await axios.post(ENDPOINTS.employee, employee); - await dispatch(fetchProfile()).unwrap(); +export const createProfile = createAsyncThunk, { state: RootState; rejectValue: ErrorResponse }>( + 'profile/createProfile', + async (employee, { dispatch, rejectWithValue }) => { + const modModel = new EmployeeModificationModel(employee.firstName, employee.lastName, employee.fullName ?? null); - dispatch(setSuccess(MESSAGES.success.employee.create)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.create, - }) - ); + return matchClientUnitResult( + await employeeClient.CreateEmployeeAsync(modModel, new AbortController().signal), + async () => { + await dispatch(fetchProfile()).unwrap(); + + dispatch(setSuccess(MESSAGES.success.employee.create)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.employee.create }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(problem) as ErrorResponse; - return rejectWithValue(mappedError); + return rejectWithValue(mappedError); + } + ); } -}); +); -export const editProfile = createAsyncThunk, { rejectValue: ErrorResponse }>( +export const editProfile = createAsyncThunk, { rejectValue: ErrorResponse }>( 'profile/editProfile', async (employee, { dispatch, rejectWithValue }) => { - try { - await axios.put(ENDPOINTS.employee, employee); + const modModel = new EmployeeModificationModel(employee.firstName, employee.lastName, employee.fullName ?? null); - dispatch(setSuccess(MESSAGES.success.employee.updateProfile)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.updateProfile, - }) - ); + return matchClientUnitResult( + await employeeClient.UpdateCurrentEmployeeAsync(modModel, new AbortController().signal), + () => { + dispatch(setSuccess(MESSAGES.success.employee.updateProfile)); + }, + (problem) => { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.employee.updateProfile }))); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(problem) as ErrorResponse; - return rejectWithValue(mappedError); - } + return rejectWithValue(mappedError); + } + ); } ); -export const deleteProfile = createAsyncThunk( +export const deleteProfile = createAsyncThunk( 'profile/deleteProfile', async (_, { dispatch, rejectWithValue }) => { - try { - await axios.delete(ENDPOINTS.employee); + return matchClientUnitResult( + await employeeClient.DeleteCurrentEmployeeAsync(new AbortController().signal), + () => { + dispatch(resetCompanyDatasourceTotalsFetchStatus()); + dispatch(setSuccess(MESSAGES.success.employee.deleteProfile)); + }, + (problem) => { + if (getProblemStatus(problem) === 424) { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.employee.deleteProfileDependency }))); + } else { + dispatch(setError(createProblemDetails(problem, { Title: MESSAGES.error.employee.deleteProfile }))); + } - dispatch(resetCompanyDatasourceTotalsFetchStatus()); - dispatch(setSuccess(MESSAGES.success.employee.deleteProfile)); - } catch (error) { - if ((error as ErrorResponseDTO).status === 424) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.deleteProfileDependency, - }) - ); - } else { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.deleteProfile, - }) - ); + return rejectWithValue(problem); } - - return rejectWithValue(error as ErrorResponseDTO); - } + ); } ); diff --git a/tsconfig.json b/tsconfig.json index eb1d4568..2e626c85 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, + "allowImportingTsExtensions": true, "jsx": "react-jsx", "baseUrl": "src", "types": ["node", "vite/client"],