From df2281c549435cd934eaab7fa2b5fd93036159e3 Mon Sep 17 00:00:00 2001 From: Tigran TIKSN Torosyan Date: Wed, 4 Mar 2026 22:19:27 -0600 Subject: [PATCH 1/5] Install Bridge NPM Package --- package-lock.json | 16 ++++++++++++++++ package.json | 1 + 2 files changed, 17 insertions(+) diff --git a/package-lock.json b/package-lock.json index c01d89d1..ea9b993f 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.31", "@mui/icons-material": "^6.1.6", "@mui/material": "^6.4.8", "@reduxjs/toolkit": "^2.11.2", @@ -1759,6 +1760,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.31", + "resolved": "https://registry.npmjs.org/@fossa-app/bridge/-/bridge-0.1.31.tgz", + "integrity": "sha512-xgHQo0dDCFhQ99euIb6tehD0/AV8ixGGHzza+8RAk+ScrFd4o+u52T1G6mFMivb9bE1Ja8PmVLAoHU7IUKOAbA==", + "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 79e02f5b..b2500c6d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.13.0", + "@fossa-app/bridge": "^0.1.31", "@mui/icons-material": "^6.1.6", "@mui/material": "^6.4.8", "@reduxjs/toolkit": "^2.11.2", From 736c4d3c5c8c69987a114b0f51f93a4e64522416 Mon Sep 17 00:00:00 2001 From: Tigran TIKSN Torosyan Date: Thu, 9 Apr 2026 16:30:12 -0500 Subject: [PATCH 2/5] Update Jest Config --- jest.config.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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, From f494bb638dac5279c303405cfdfddfaf27a2bd3a Mon Sep 17 00:00:00 2001 From: Tigran TIKSN Torosyan Date: Thu, 9 Apr 2026 16:14:35 -0500 Subject: [PATCH 3/5] Update TS Config --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) 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"], From a50b5cfd2c42151d886db2915381aa9fcd86a38f Mon Sep 17 00:00:00 2001 From: Tigran TIKSN Torosyan Date: Fri, 10 Apr 2026 13:31:20 -0500 Subject: [PATCH 4/5] Remove axios --- package.json | 1 - src/AxiosInterceptor.tsx | 124 ------------------------------------ src/pages/Root.tsx | 27 ++++---- src/shared/configs/axios.ts | 21 ------ 4 files changed, 12 insertions(+), 161 deletions(-) delete mode 100644 src/AxiosInterceptor.tsx delete mode 100644 src/shared/configs/axios.ts diff --git a/package.json b/package.json index b2500c6d..6bef5864 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "@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/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/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; From daae00332c04a8412b800a93fdd777bab5a976d7 Mon Sep 17 00:00:00 2001 From: Tigran TIKSN Torosyan Date: Thu, 9 Apr 2026 16:15:50 -0500 Subject: [PATCH 5/5] Use Bridge NPM Package --- src/components/Catalog.tsx | 4 +- src/components/Entity/EntityManager.tsx | 28 ++-- src/components/Entity/EntityViewer.tsx | 4 +- src/layout/MessageLayout.tsx | 2 +- .../Manage/Branch/pages/BranchManagement.tsx | 8 +- .../Manage/Company/pages/CompanyEdit.tsx | 4 +- .../Manage/Company/pages/CompanySettings.tsx | 6 +- .../Department/pages/DepartmentManagement.tsx | 12 +- .../Manage/Employee/pages/EmployeeEdit.tsx | 12 +- .../Onboarding/pages/Company/BranchCreate.tsx | 4 +- .../pages/Company/CompanyCreate.tsx | 4 +- .../pages/Company/CompanySettingsCreate.tsx | 6 +- .../pages/Employee/EmployeeCreate.tsx | 4 +- .../Manage/Profile/pages/EditProfile.tsx | 4 +- src/shared/configs/BridgeClients.ts | 25 +++ src/shared/configs/BridgeResponses.ts | 64 ++++++++ src/shared/configs/BridgeTransport.ts | 118 ++++++++++++++ src/shared/constants/endpoints.ts | 12 -- src/shared/constants/index.ts | 1 - src/shared/helpers/branch.helpers.ts | 10 +- src/shared/helpers/company.helpers.ts | 4 +- src/shared/helpers/data.helpers.ts | 14 +- src/shared/helpers/department.helpers.ts | 18 +- src/shared/helpers/employee.helpers.ts | 18 +- src/shared/helpers/error.helpers.ts | 4 +- src/shared/helpers/url.helpers.ts | 17 -- src/shared/helpers/user.helpers.ts | 2 +- src/shared/types/branch.ts | 13 +- src/shared/types/client.ts | 5 - src/shared/types/company-settings.ts | 4 +- src/shared/types/company.ts | 5 +- src/shared/types/department.ts | 5 +- src/shared/types/employee.ts | 5 +- src/shared/types/index.ts | 1 - src/shared/types/response.ts | 27 +-- src/store/features/authSlice.ts | 4 +- src/store/features/branchSlice.ts | 10 +- src/store/features/companySettingsSlice.ts | 8 +- src/store/features/companySlice.ts | 21 ++- src/store/features/departmentSlice.ts | 16 +- src/store/features/employeeSlice.ts | 14 +- src/store/features/identitySlice.ts | 9 +- src/store/features/licenseSlice.ts | 6 +- src/store/features/messageSlice.ts | 6 +- src/store/features/offboardingSlice.ts | 12 +- src/store/features/profileSlice.ts | 6 +- src/store/thunks/authThunk.ts | 6 +- src/store/thunks/branchThunks.ts | 154 ++++++++++-------- src/store/thunks/companySettingsThunk.ts | 40 +++-- src/store/thunks/companyThunk.ts | 46 +++--- src/store/thunks/departmentThunk.ts | 143 +++++++++------- src/store/thunks/employeeThunk.ts | 150 +++++++++-------- src/store/thunks/identityThunk.ts | 25 +-- src/store/thunks/licenseThunk.ts | 49 +++--- src/store/thunks/onboardingThunk.ts | 4 +- src/store/thunks/profileThunk.ts | 73 +++++---- 56 files changed, 755 insertions(+), 521 deletions(-) create mode 100644 src/shared/configs/BridgeClients.ts create mode 100644 src/shared/configs/BridgeResponses.ts create mode 100644 src/shared/configs/BridgeTransport.ts delete mode 100644 src/shared/constants/endpoints.ts delete mode 100644 src/shared/types/client.ts diff --git a/src/components/Catalog.tsx b/src/components/Catalog.tsx index 3f94bf00..73770a2b 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, ValidationProblemDetails, 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: ValidationProblemDetails; dispatch?: AppDispatch; } diff --git a/src/components/Entity/EntityManager.tsx b/src/components/Entity/EntityManager.tsx index be00e561..ce749f3b 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, ValidationProblemDetails, 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..ae10414b 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, ValidationProblemDetails, 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..1a01da72 100644 --- a/src/layout/MessageLayout.tsx +++ b/src/layout/MessageLayout.tsx @@ -24,7 +24,7 @@ const MessageLayout: React.FC = () => { data-cy={testSelector} type={type} open={!!error || !!success} - message={error ? error.title : success} + message={error ? (error.title ?? undefined) : success} onClose={handleSnackbarClose} /> ); diff --git a/src/pages/Manage/Branch/pages/BranchManagement.tsx b/src/pages/Manage/Branch/pages/BranchManagement.tsx index e7372708..9e04351b 100644 --- a/src/pages/Manage/Branch/pages/BranchManagement.tsx +++ b/src/pages/Manage/Branch/pages/BranchManagement.tsx @@ -19,10 +19,10 @@ 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, @@ -70,7 +70,7 @@ const BranchManagementPage: React.FC = () => { }; return ( - + module={testModule} subModule={testSubModule} pageTitle={{ create: 'Create Branch', edit: 'Edit Branch' }} @@ -88,7 +88,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..4c85818f 100644 --- a/src/pages/Manage/Company/pages/CompanyEdit.tsx +++ b/src/pages/Manage/Company/pages/CompanyEdit.tsx @@ -16,7 +16,7 @@ import { ROUTES, USER_PERMISSION_GENERAL_ERROR, } from 'shared/constants'; -import { Company, CompanyDTO, EntityInput } from 'shared/types'; +import { Company, EntityInput } from 'shared/types'; import { deepCopyObject, mapCountriesToFieldOptions, mapDisabledFields } from 'shared/helpers'; import { useOnFormSubmitEffect } from 'shared/hooks'; import PageLayout from 'components/layouts/PageLayout'; @@ -37,7 +37,7 @@ const CompanyEditPage: React.FC = () => { const errors = isUserAdmin ? deepCopyObject(error?.errors) : 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..51a556fa 100644 --- a/src/pages/Manage/Department/pages/DepartmentManagement.tsx +++ b/src/pages/Manage/Department/pages/DepartmentManagement.tsx @@ -23,8 +23,8 @@ 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 } from 'shared/helpers'; import EntityManager from 'components/Entity/EntityManager'; const testModule = DEPARTMENT_MANAGEMENT_DETAILS_FORM_SCHEMA.module; @@ -68,12 +68,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 +110,7 @@ const DepartmentManagementPage: React.FC = () => { }, [parentDepartmentsFetchStatus, parentDepartmentsPage, dispatch]); return ( - + module={testModule} subModule={testSubModule} pageTitle={{ create: 'Create Department', edit: 'Edit Department' }} @@ -127,7 +127,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..88150b3f 100644 --- a/src/pages/Manage/Employee/pages/EmployeeEdit.tsx +++ b/src/pages/Manage/Employee/pages/EmployeeEdit.tsx @@ -20,13 +20,13 @@ 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, } from 'shared/helpers'; import { useOnFormSubmitEffect, useSafeNavigateBack } from 'shared/hooks'; @@ -93,7 +93,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 +102,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/Onboarding/pages/Company/BranchCreate.tsx b/src/pages/Manage/Onboarding/pages/Company/BranchCreate.tsx index 865b4e37..fcba5b80 100644 --- a/src/pages/Manage/Onboarding/pages/Company/BranchCreate.tsx +++ b/src/pages/Manage/Onboarding/pages/Company/BranchCreate.tsx @@ -12,7 +12,7 @@ import { createOnboardingBranch } from 'store/thunks'; import { Branch } from 'shared/types'; import { getBranchManagementDetailsByAddressFormSchema, - mapBranchDTO, + mapBranchInput, mapDisabledFields, mapBranchFieldOptionsToFieldOptions, deepCopyObject, @@ -46,7 +46,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..6930d3ff 100644 --- a/src/pages/Manage/Onboarding/pages/Company/CompanyCreate.tsx +++ b/src/pages/Manage/Onboarding/pages/Company/CompanyCreate.tsx @@ -2,7 +2,7 @@ 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 { Company } from 'shared/types'; import { deepCopyObject, 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'; @@ -24,7 +24,7 @@ const CompanyCreatePage: React.FC = () => { ); const errors = isUserAdmin ? deepCopyObject(error?.errors) : USER_PERMISSION_GENERAL_ERROR; - const handleSubmit = (data: CompanyDTO) => { + const handleSubmit = (data: Company) => { dispatch(createCompany(data)); }; diff --git a/src/pages/Manage/Onboarding/pages/Company/CompanySettingsCreate.tsx b/src/pages/Manage/Onboarding/pages/Company/CompanySettingsCreate.tsx index 3670cf30..cfafd367 100644 --- a/src/pages/Manage/Onboarding/pages/Company/CompanySettingsCreate.tsx +++ b/src/pages/Manage/Onboarding/pages/Company/CompanySettingsCreate.tsx @@ -15,7 +15,7 @@ import { CREATE_COMPANY_SETTINGS_DETAILS_FORM_SCHEMA, USER_PERMISSION_GENERAL_ERROR, } from 'shared/constants'; -import { CompanySettings, CompanySettingsDTO, EntityInput, ThemeMode } from 'shared/types'; +import { CompanySettings, EntityInput, ThemeMode } from 'shared/types'; import { deepCopyObject, hasAllowedRole, mapDisabledFields } from 'shared/helpers'; import { COLOR_SCHEMES } from 'shared/themes'; import { useUnmount } from 'shared/hooks'; @@ -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..9b7d7be8 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, mapProfileInput } from 'shared/helpers'; import Form, { FormActionName } from 'components/UI/Form'; const testModule = CREATE_EMPLOYEE_DETAILS_FORM_SCHEMA.module; @@ -20,7 +20,7 @@ const EmployeeCreatePage: React.FC = () => { ); 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..67636544 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, mapProfileInput } from 'shared/helpers'; import { useOnFormSubmitEffect } from 'shared/hooks'; import PageLayout from 'components/layouts/PageLayout'; import Form, { FormActionName } from 'components/UI/Form'; @@ -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/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..d107f1f0 --- /dev/null +++ b/src/shared/configs/BridgeResponses.ts @@ -0,0 +1,64 @@ +import type { ValidationProblemDetails, PaginatedResponse } from 'shared/types'; + +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; +}; + +const toNumber = (value: unknown): number | undefined => { + if (value === null || value === undefined) { + return undefined; + } + + return Number(value); +}; + +const toErrorResponse = (problem: unknown): ValidationProblemDetails => { + return { + type: read(problem, 'Type'), + title: read(problem, 'Title'), + status: read(problem, 'Status'), + }; +}; + +export const unwrapBridgeValue = (result: unknown): T => { + if (!read(result, 'Succeeded')) { + throw toErrorResponse(read(result, 'Problem')); + } + + const value = read(result, 'Value'); + + if (value === null || value === undefined) { + throw {}; + } + + return value as T; +}; + +export const unwrapBridgeUnitResult = (result: unknown): void => { + if (!read(result, 'Succeeded')) { + throw toErrorResponse(read(result, 'Problem')); + } +}; + +export const unwrapBridgePagingResponse = (result: unknown): PaginatedResponse => { + const value = unwrapBridgeValue(result); + const items = read(value, 'Items') ?? []; + + return { + pageNumber: toNumber(read(value, 'PageNumber')), + pageSize: toNumber(read(value, 'PageSize')), + totalItems: toNumber(read(value, 'TotalItems')), + totalPages: toNumber(read(value, 'TotalPages')), + items, + }; +}; diff --git a/src/shared/configs/BridgeTransport.ts b/src/shared/configs/BridgeTransport.ts new file mode 100644 index 00000000..129ed095 --- /dev/null +++ b/src/shared/configs/BridgeTransport.ts @@ -0,0 +1,118 @@ +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 { 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) { + // Network Error + store.dispatch(setError({ title: MESSAGES.error.general.network })); + throw { status: 599, title: MESSAGES.error.general.network }; + } + + if (response.status === 401) { + // Token is presumably dead or strictly unauthorized + const userManager = getUserManager(); + try { + await userManager.removeUser(); + } catch { + // Ignored + } + + store.dispatch(removeUser()); + store.dispatch(setError({ title: MESSAGES.error.general.unAuthorized })); + + // Let it redirect using our auth observer or via location + if (typeof window !== 'undefined') { + window.location.href = '/login'; + } + + throw { status: 401, title: MESSAGES.error.general.unAuthorized }; + } + + if (response.status >= 500) { + let data = {}; + try { + data = await response.json(); + } catch { + // Ignored + } + + store.dispatch(setError({ title: MESSAGES.error.general.common })); + throw { ...data, title: MESSAGES.error.general.common }; + } + + if (!response.ok) { + // 4xx errors + let data = {}; + try { + data = await response.json(); + } catch { + // Ignored + } + throw data; + } + + const text = await response.text(); + return new HttpResponseMessage(response.status, text); + } +} 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/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..ad5a4697 100644 --- a/src/shared/helpers/error.helpers.ts +++ b/src/shared/helpers/error.helpers.ts @@ -1,8 +1,8 @@ import { FieldErrors, FieldValues } from 'react-hook-form'; -import { ErrorResponseDTO, ErrorResponse } from 'shared/types'; +import { ValidationProblemDetails, ErrorResponse } from 'shared/types'; import { FormFieldProps } from 'components/UI/Form'; -export const mapError = (error: ErrorResponseDTO): ErrorResponse => { +export const mapError = (error: ValidationProblemDetails): ErrorResponse => { const errors: FieldErrors = {} as FieldErrors; if (!error.errors) { 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..1249c3f3 100644 --- a/src/shared/types/company-settings.ts +++ b/src/shared/types/company-settings.ts @@ -1,13 +1,11 @@ import { ColorSchemeId } from 'shared/types'; -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..0aa1a2f1 100644 --- a/src/shared/types/response.ts +++ b/src/shared/types/response.ts @@ -1,26 +1,33 @@ import { FieldErrors, FieldValues } from 'react-hook-form'; +import type { PagingResponseModel$1 } from '@fossa-app/bridge/Models/ApiModels/EnvelopeModels'; +import type { ProblemDetailsModel } from '@fossa-app/bridge/Models/ApiModels/SharedModels'; -export interface ErrorResponseDTO { - type?: string; - title?: string; +type UiProblemDetails = Partial & { + type?: ProblemDetailsModel['Type']; + title?: ProblemDetailsModel['Title']; + status?: ProblemDetailsModel['Status']; + detail?: ProblemDetailsModel['Detail']; + instance?: ProblemDetailsModel['Instance']; traceId?: string; - status?: number; +}; + +export type ValidationProblemDetails = UiProblemDetails & { errors?: Record; -} +}; -export interface ErrorResponse extends Omit { +export type ErrorResponse = Omit & { errors?: FieldErrors; -} +}; -export type GeneralErrorResponse = ErrorResponseDTO | ErrorResponse; +export type GeneralErrorResponse = ValidationProblemDetails | 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..d8c32657 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, ValidationProblemDetails, 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..8d0baa89 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, ValidationProblemDetails, 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..00dddda0 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, ValidationProblemDetails } 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..085d2d6a 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, + ValidationProblemDetails, 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..ef9e6ed1 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 { ValidationProblemDetails, 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..8cb395b0 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, ValidationProblemDetails, 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..ff9ebd69 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 { ValidationProblemDetails } 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..0af0e6c1 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, ValidationProblemDetails, 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..d7e24a41 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 { ValidationProblemDetails } from 'shared/types'; interface MessageState { - error: ErrorResponseDTO | undefined; + error: ValidationProblemDetails | 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..a5aedafe 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, ValidationProblemDetails } 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..65983cd8 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 { AppUser, ValidationProblemDetails } from 'shared/types'; import { MESSAGES } from 'shared/constants'; -export const fetchUser = createAsyncThunk( +export const fetchUser = createAsyncThunk( 'auth/fetchUser', async (_, { rejectWithValue }) => { try { @@ -18,7 +18,7 @@ export const fetchUser = createAsyncThunk | undefined, void, { rejectValue: ErrorResponseDTO }>( +import { MESSAGES } from 'shared/constants'; +import { mapBranch, mapBranches, mapError, getFullAddress } from 'shared/helpers'; +import { branchClient } from 'shared/configs/BridgeClients'; +import { unwrapBridgePagingResponse, unwrapBridgeUnitResult, unwrapBridgeValue } 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: ValidationProblemDetails }>( '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); + const data = unwrapBridgePagingResponse(await branchClient.GetBranchesAsync(query, new AbortController().signal)); if (!data.items.length) { return rejectWithValue({ @@ -47,7 +57,7 @@ export const fetchBranchesTotal = createAsyncThunk return data; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.branches.notFound, }); } @@ -57,11 +67,11 @@ export const fetchBranchesTotal = createAsyncThunk export const fetchBranches = createAsyncThunk< PaginatedResponse | undefined, Partial, - { rejectValue: ErrorResponseDTO } + { rejectValue: ValidationProblemDetails } >('branch/fetchBranches', async ({ pageNumber, pageSize, search }, { getState, 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); + const data = unwrapBridgePagingResponse(await branchClient.GetBranchesAsync(query, new AbortController().signal)); if (!data.items.length) { return rejectWithValue({ @@ -81,54 +91,62 @@ export const fetchBranches = createAsyncThunk< }; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.branches.notFound, }); } }); export const fetchAssignedBranches = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { state: RootState; rejectValue: ErrorResponseDTO } + { state: RootState; rejectValue: ValidationProblemDetails } >('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); + const data = unwrapBridgePagingResponse(await branchClient.GetBranchesAsync(query, new AbortController().signal)); return data; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.branches.notFound, }); } }); -export const fetchBranchesByIds = createAsyncThunk | undefined, number[], { rejectValue: ErrorResponseDTO }>( - 'branch/fetchBranchesByIds', - async (ids, { rejectWithValue }) => { +export const fetchBranchesByIds = createAsyncThunk< + PaginatedResponse | undefined, + number[], + { rejectValue: ValidationProblemDetails } +>('branch/fetchBranchesByIds', async (ids, { rejectWithValue }) => { + try { + 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); + const data = unwrapBridgePagingResponse(await branchClient.GetBranchesAsync(query, new AbortController().signal)); + + return data; + } catch (error) { + return rejectWithValue({ + ...(error as ValidationProblemDetails), + title: MESSAGES.error.branches.notFound, + }); } -); +}); export const fetchBranchById = createAsyncThunk< Branch, { id: string; skipState?: boolean; shouldFetchBranchGeoAddress?: boolean }, - { rejectValue: ErrorResponseDTO } + { rejectValue: ValidationProblemDetails } >('branch/fetchBranchById', async ({ id, shouldFetchBranchGeoAddress = true }, { getState, dispatch, rejectWithValue }) => { try { - const { data } = await axios.get(`${ENDPOINTS.branches}/${id}`); + const data = unwrapBridgeValue(await branchClient.GetBranchAsync(BigInt(id), new AbortController().signal)); const state = getState() as RootState; const timeZones = state.license.system.item?.entitlements.timeZones || []; const countries = state.license.system.item?.entitlements.countries || []; @@ -149,15 +167,16 @@ export const fetchBranchById = createAsyncThunk< geoAddress, }); } catch (error) { - return rejectWithValue(error as ErrorResponseDTO); + return rejectWithValue(error as ValidationProblemDetails); } }); -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)); + unwrapBridgeUnitResult(await branchClient.CreateBranchAsync(modModel, new AbortController().signal)); dispatch(resetBranchesFetchStatus()); await dispatch(fetchBranchesTotal()).unwrap(); @@ -166,23 +185,24 @@ export const createOnboardingBranch = createAsyncThunk; + const mappedError = mapError(error as ValidationProblemDetails) as ErrorResponse; 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); + const modModel = new BranchModificationModel(branch.name, branch.timeZoneId, toBranchModificationAddress(branch.address)); + unwrapBridgeUnitResult(await branchClient.CreateBranchAsync(modModel, new AbortController().signal)); dispatch(resetBranchesFetchStatus()); dispatch(setBranchesSucceededFlag()); @@ -190,45 +210,46 @@ export const createBranch = createAsyncThunk, { rej } catch (error) { dispatch( setError({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.branches.create, }) ); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(error as ValidationProblemDetails) 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)); + unwrapBridgeUnitResult(await branchClient.UpdateBranchAsync(BigInt(id), modModel, new AbortController().signal)); dispatch(setSuccess(MESSAGES.success.branches.update)); } catch (error) { dispatch( setError({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.branches.update, }) ); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(error as ValidationProblemDetails) as ErrorResponse; 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}`); + unwrapBridgeUnitResult(await branchClient.DeleteBranchAsync(BigInt(id), new AbortController().signal)); dispatch(resetBranchesFetchStatus()); dispatch(resetCompanyDatasourceTotalsFetchStatus()); @@ -243,12 +264,12 @@ export const deleteBranch = 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..bfe4a4f8 100644 --- a/src/store/thunks/companySettingsThunk.ts +++ b/src/store/thunks/companySettingsThunk.ts @@ -1,34 +1,37 @@ 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 { CompanySettings, EntityInput, ErrorResponse, ValidationProblemDetails } from 'shared/types'; +import { MESSAGES, COMPANY_SETTINGS_KEY } from 'shared/constants'; import { mapError, saveToLocalStorage, removeFromLocalStorage } from 'shared/helpers'; +import { companySettingsClient } from 'shared/configs/BridgeClients'; +import { unwrapBridgeUnitResult, unwrapBridgeValue } 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); + const data = unwrapBridgeValue(await companySettingsClient.GetCompanySettingsAsync(new AbortController().signal)); saveToLocalStorage(COMPANY_SETTINGS_KEY, data); return data || {}; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), 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); + const modModel = new CompanySettingsModificationModel(companySettings.colorSchemeId!); + unwrapBridgeUnitResult(await companySettingsClient.CreateCompanySettingsAsync(modModel, new AbortController().signal)); saveToLocalStorage(COMPANY_SETTINGS_KEY, companySettings); await dispatch(fetchCompanySettings()).unwrap(); @@ -36,23 +39,24 @@ export const createCompanySettings = createAsyncThunk; + const mappedError = mapError(error as ValidationProblemDetails) as ErrorResponse; 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!); + unwrapBridgeUnitResult(await companySettingsClient.UpdateCompanySettingsAsync(modModel, new AbortController().signal)); await dispatch(fetchCompanySettings()).unwrap(); @@ -61,23 +65,23 @@ export const editCompanySettings = createAsyncThunk; + const mappedError = mapError(error as ValidationProblemDetails) as ErrorResponse; return rejectWithValue(mappedError); } } ); -export const deleteCompanySettings = createAsyncThunk( +export const deleteCompanySettings = createAsyncThunk( 'companySettings/deleteCompanySettings', async (_, { dispatch, rejectWithValue }) => { try { - await axios.delete(ENDPOINTS.companySettings); + unwrapBridgeUnitResult(await companySettingsClient.DeleteCompanySettingsAsync(new AbortController().signal)); removeFromLocalStorage(COMPANY_SETTINGS_KEY); dispatch(setSuccess(MESSAGES.success.companySettings.delete)); @@ -89,12 +93,12 @@ export const deleteCompanySettings = createAsyncThunk( +export const fetchCompany = createAsyncThunk( 'company/fetchCompany', async (_, { getState, rejectWithValue }) => { try { - const { data } = await axios.get(ENDPOINTS.company); + const data = unwrapBridgeValue(await companyClient.GetCompanyAsync(new AbortController().signal)); if (data) { const state = getState() as RootState; @@ -22,63 +24,65 @@ export const fetchCompany = createAsyncThunk }>( +export const createCompany = createAsyncThunk }>( 'company/createCompany', async (company, { dispatch, rejectWithValue }) => { try { - await axios.post(ENDPOINTS.company, company); + const modModel = new CompanyModificationModel(company.name, company.countryCode ?? null); + unwrapBridgeUnitResult(await companyClient.CreateCompanyAsync(modModel, new AbortController().signal)); await dispatch(fetchCompany(false)).unwrap(); dispatch(setSuccess(MESSAGES.success.company.create)); } catch (error) { dispatch( setError({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.company.create, }) ); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(error as ValidationProblemDetails) 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); + unwrapBridgeUnitResult(await companyClient.UpdateCompanyAsync(modModel, new AbortController().signal)); dispatch(setSuccess(MESSAGES.success.company.update)); } catch (error) { dispatch( setError({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.company.update, }) ); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(error as ValidationProblemDetails) as ErrorResponse; return rejectWithValue(mappedError); } } ); -export const deleteCompany = createAsyncThunk( +export const deleteCompany = createAsyncThunk( 'company/deleteCompany', async (_, { dispatch, rejectWithValue }) => { try { - await axios.delete(ENDPOINTS.company); + unwrapBridgeUnitResult(await companyClient.DeleteCompanyAsync(new AbortController().signal)); dispatch(setSuccess(MESSAGES.success.company.delete)); @@ -88,28 +92,28 @@ export const deleteCompany = 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..8cf032d9 100644 --- a/src/store/thunks/departmentThunk.ts +++ b/src/store/thunks/departmentThunk.ts @@ -9,28 +9,30 @@ import { resetDepartmentsFetchStatus, } from 'store/features'; import { fetchEmployeeById, fetchEmployeesByIds } from 'store/thunks'; -import axios from 'shared/configs/axios'; import { - ErrorResponseDTO, + ValidationProblemDetails, ErrorResponse, PaginatedResponse, PaginationParams, Department, - DepartmentDTO, - EmployeeDTO, + Employee, 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 { MESSAGES } from 'shared/constants'; +import { departmentClient } from 'shared/configs/BridgeClients'; +import { unwrapBridgePagingResponse, unwrapBridgeUnitResult, unwrapBridgeValue } from 'shared/configs/BridgeResponses'; +import { DepartmentQueryRequestModel, DepartmentModificationModel } from '@fossa-app/bridge/Models/ApiModels/PayloadModels'; +import { mapDepartments, mapError, mapDepartment, getEntityIdsByField } 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,18 +44,18 @@ const fetchParentDepartment = async (dispatch: ThunkDispatch | undefined, + PaginatedResponse | undefined, void, - { rejectValue: ErrorResponseDTO } + { rejectValue: ValidationProblemDetails } >('department/fetchDepartmentsTotal', async (_, { rejectWithValue }) => { try { - const queryParams = prepareQueryParams({ pageNumber: 1, pageSize: 1 }); - const { data } = await axios.get>(`${ENDPOINTS.departments}?${queryParams}`); + const query = new DepartmentQueryRequestModel([], '', 1, 1); + const data = unwrapBridgePagingResponse(await departmentClient.GetDepartmentsAsync(query, new AbortController().signal)); return data; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.departments.notFound, }); } @@ -62,17 +64,17 @@ export const fetchDepartmentsTotal = createAsyncThunk< export const fetchDepartments = createAsyncThunk< PaginatedResponse | undefined, Partial & { shouldFetchEmployees?: boolean }, - { rejectValue: ErrorResponseDTO } + { rejectValue: ValidationProblemDetails } >('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); + const data = unwrapBridgePagingResponse(await departmentClient.GetDepartmentsAsync(query, new AbortController().signal)); if (data) { 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(); @@ -89,79 +91,86 @@ export const fetchDepartments = createAsyncThunk< } } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.departments.notFound, }); } }); export const fetchSearchedDepartments = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { rejectValue: ErrorResponseDTO } + { rejectValue: ValidationProblemDetails } >('department/fetchSearchedDepartments', async ({ pageNumber, pageSize, search }, { 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); + const data = unwrapBridgePagingResponse(await departmentClient.GetDepartmentsAsync(query, new AbortController().signal)); return data; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.departments.notFound, }); } }); export const fetchParentDepartments = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { state: RootState; rejectValue: ErrorResponseDTO } + { state: RootState; rejectValue: ValidationProblemDetails } >('department/fetchParentDepartments', async ({ pageNumber, pageSize, search }, { 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); + const data = unwrapBridgePagingResponse(await departmentClient.GetDepartmentsAsync(query, new AbortController().signal)); return data; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.departments.notFound, }); } }); export const fetchAssignedDepartments = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { state: RootState; rejectValue: ErrorResponseDTO } + { state: RootState; rejectValue: ValidationProblemDetails } >('department/fetchAssignedDepartments', async ({ pageNumber, pageSize, search }, { 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); + const data = unwrapBridgePagingResponse(await departmentClient.GetDepartmentsAsync(query, new AbortController().signal)); return data; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.departments.notFound, }); } }); export const fetchDepartmentsByIds = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, number[], - { rejectValue: ErrorResponseDTO } + { rejectValue: ValidationProblemDetails } >('department/fetchDepartmentsByIds', async (ids, { rejectWithValue }) => { try { - const queryParams = prepareCommaSeparatedQueryParamsByKey('id', ids); - const { data } = await axios.get>(`${ENDPOINTS.departments}?${queryParams}`); + let idList: bigint[] = []; + try { + idList = ids.map((id) => BigInt(id)); + } catch { + // Ignored + } + + const query = new DepartmentQueryRequestModel(idList, '', null, null); + const data = unwrapBridgePagingResponse(await departmentClient.GetDepartmentsAsync(query, new AbortController().signal)); return data; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.departments.notFound, }); } @@ -175,15 +184,15 @@ export const fetchDepartmentById = createAsyncThunk< shouldFetchParent?: boolean; shouldFetchDepartmentManager?: boolean; }, - { rejectValue: ErrorResponseDTO } + { rejectValue: ValidationProblemDetails } >( 'department/fetchDepartmentById', async ({ id, shouldFetchParent = true, shouldFetchDepartmentManager = true }, { dispatch, rejectWithValue }): Promise => { try { - const { data } = await axios.get(`${ENDPOINTS.departments}/${id}`); + const data = unwrapBridgeValue(await departmentClient.GetDepartmentAsync(BigInt(id), new AbortController().signal)); let parentDepartment: Department | undefined; - let manager: EmployeeDTO | undefined; + let manager: Employee | undefined; if (data.parentDepartmentId && shouldFetchParent) { parentDepartment = await fetchParentDepartment(dispatch, String(data.parentDepartmentId)); @@ -203,16 +212,21 @@ export const fetchDepartmentById = createAsyncThunk< return mapDepartment(data, parentDepartment, manager); } catch (error) { - return rejectWithValue(error as ErrorResponseDTO) as unknown as Department; + return rejectWithValue(error as ValidationProblemDetails) 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) + ); + unwrapBridgeUnitResult(await departmentClient.CreateDepartmentAsync(modModel, new AbortController().signal)); dispatch(resetParentDepartments()); @@ -220,23 +234,28 @@ export const createDepartment = createAsyncThunk; + const mappedError = mapError(error as ValidationProblemDetails) as ErrorResponse; 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) + ); + unwrapBridgeUnitResult(await departmentClient.UpdateDepartmentAsync(BigInt(id), modModel, new AbortController().signal)); dispatch(resetParentDepartments()); @@ -244,23 +263,23 @@ export const editDepartment = createAsyncThunk; + const mappedError = mapError(error as ValidationProblemDetails) as ErrorResponse; return rejectWithValue(mappedError); } } ); -export const deleteDepartment = createAsyncThunk( +export const deleteDepartment = createAsyncThunk( 'department/deleteDepartment', async (id, { dispatch, rejectWithValue }) => { try { - await axios.delete(`${ENDPOINTS.departments}/${id}`); + unwrapBridgeUnitResult(await departmentClient.DeleteDepartmentAsync(BigInt(id), new AbortController().signal)); dispatch(resetDepartmentsFetchStatus()); dispatch(resetParentDepartments()); @@ -269,12 +288,12 @@ export const deleteDepartment = createAsyncThunk { + if (value === null || value === undefined) { + return null; + } + + return BigInt(value); +}; const fetchManager = async (dispatch: ThunkDispatch, id: string) => { return dispatch( @@ -46,7 +38,7 @@ export const fetchEmployees = createAsyncThunk< shouldFetchDepartments?: boolean; shouldFetchManagers?: boolean; }, - { rejectValue: ErrorResponseDTO } + { rejectValue: ValidationProblemDetails } >( 'employee/fetchEmployees', async ( @@ -54,16 +46,16 @@ export const fetchEmployees = createAsyncThunk< { 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); + const data = unwrapBridgePagingResponse(await employeeClient.GetEmployeesAsync(query, new AbortController().signal)); if (data) { 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(); @@ -89,7 +81,7 @@ export const fetchEmployees = createAsyncThunk< } } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.employee.notFound, }); } @@ -97,33 +89,41 @@ export const fetchEmployees = createAsyncThunk< ); export const fetchManagers = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { state: RootState; rejectValue: ErrorResponseDTO } + { state: RootState; rejectValue: ValidationProblemDetails } >('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); + const data = unwrapBridgePagingResponse(await employeeClient.GetEmployeesAsync(query, new AbortController().signal)); return data; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.employee.notFound, }); } }); export const fetchOrgChartEmployees = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, Partial, - { state: RootState; rejectValue: ErrorResponseDTO } + { state: RootState; rejectValue: ValidationProblemDetails } >('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 = unwrapBridgePagingResponse( + await employeeClient.GetEmployeesAsync(query, new AbortController().signal) + ); return subordinateData.items.concat((await Promise.all(subordinateData.items.map(fetchSubordinates))).flat()); }; @@ -133,28 +133,29 @@ export const fetchOrgChartEmployees = createAsyncThunk< }; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.employee.notFound, }); } }); -export const fetchEmployeesTotal = createAsyncThunk | undefined, void, { rejectValue: ErrorResponseDTO }>( - 'employee/fetchEmployeesTotal', - async (_, { rejectWithValue }) => { - try { - const queryParams = prepareQueryParams({ pageNumber: 1, pageSize: 1 }); - const { data } = await axios.get>(`${ENDPOINTS.employees}?${queryParams}`); +export const fetchEmployeesTotal = createAsyncThunk< + PaginatedResponse | undefined, + void, + { rejectValue: ValidationProblemDetails } +>('employee/fetchEmployeesTotal', async (_, { rejectWithValue }) => { + try { + const query = new EmployeeQueryRequestModel([], '', 1, 1, null, null); + const data = unwrapBridgePagingResponse(await employeeClient.GetEmployeesAsync(query, new AbortController().signal)); - return data; - } catch (error) { - return rejectWithValue({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.notFound, - }); - } + return data; + } catch (error) { + return rejectWithValue({ + ...(error as ValidationProblemDetails), + title: MESSAGES.error.employee.notFound, + }); } -); +}); export const fetchEmployeeById = createAsyncThunk< Employee, @@ -166,7 +167,7 @@ export const fetchEmployeeById = createAsyncThunk< shouldFetchDepartment?: boolean; shouldFetchEmployeeManager?: boolean; }, - { rejectValue: ErrorResponseDTO } + { rejectValue: ValidationProblemDetails } >( 'employee/fetchEmployeeById', async ( @@ -174,7 +175,7 @@ export const fetchEmployeeById = createAsyncThunk< { dispatch, rejectWithValue } ) => { try { - const { data } = await axios.get(`${ENDPOINTS.employees}/${id}`); + const data = unwrapBridgeValue(await employeeClient.GetEmployeeAsync(BigInt(id), new AbortController().signal)); let branch: Branch | undefined; let department: Department | undefined; let manager: Employee | undefined; @@ -195,24 +196,31 @@ export const fetchEmployeeById = createAsyncThunk< return mapEmployee({ branch, department, manager, employee: data, user: undefined }); } catch (error) { - return rejectWithValue(error as ErrorResponseDTO); + return rejectWithValue(error as ValidationProblemDetails); } } ); export const fetchEmployeesByIds = createAsyncThunk< - PaginatedResponse | undefined, + PaginatedResponse | undefined, number[], - { rejectValue: ErrorResponseDTO } + { rejectValue: ValidationProblemDetails } >('employee/fetchEmployeesByIds', async (ids, { rejectWithValue }) => { try { - const queryParams = prepareCommaSeparatedQueryParamsByKey('id', ids); - const { data } = await axios.get>(`${ENDPOINTS.employees}?${queryParams}`); + let idList: bigint[] = []; + try { + idList = ids.map((id) => BigInt(id)); + } catch { + // Ignored + } + + const query = new EmployeeQueryRequestModel(idList, '', null, null, null, null); + const data = unwrapBridgePagingResponse(await employeeClient.GetEmployeesAsync(query, new AbortController().signal)); return data; } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.employee.notFound, }); } @@ -220,23 +228,31 @@ export const fetchEmployeesByIds = createAsyncThunk< 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); + const curEmp = unwrapBridgeValue(await employeeClient.GetEmployeeAsync(BigInt(id), new AbortController().signal)); + + const modModel = new EmployeeManagementModel( + nullableBigInt(employee.assignedBranchId), + nullableBigInt(curEmp.assignedDepartmentId), + nullableBigInt(curEmp.reportsToId), + curEmp.jobTitle ?? null + ); + unwrapBridgeUnitResult(await employeeClient.ManageEmployeeAsync(BigInt(id), modModel, new AbortController().signal)); dispatch(resetOrgChartEmployeesFetchStatus()); dispatch(setSuccess(MESSAGES.success.employee.updateEmployee)); } catch (error) { dispatch( setError({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.employee.updateEmployee, }) ); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(error as ValidationProblemDetails) as ErrorResponse; return rejectWithValue(mappedError); } diff --git a/src/store/thunks/identityThunk.ts b/src/store/thunks/identityThunk.ts index d87e7aa6..c60d88d7 100644 --- a/src/store/thunks/identityThunk.ts +++ b/src/store/thunks/identityThunk.ts @@ -1,33 +1,34 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; 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 { ValidationProblemDetails } 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 { unwrapBridgeValue } 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); + const parsedResponse = unwrapBridgeValue( + await identityClient.GetClientAsync(window.location.origin, new AbortController().signal) + ); - if (parsedResponse?.data) { + if (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 parsedResponse; } return rejectWithValue({ title: MESSAGES.error.client.notFound }); } catch (error: any) { - return rejectWithValue(parseResponse<{ data: ErrorResponseDTO }>(error.response).data); + return rejectWithValue(error as ValidationProblemDetails); } } ); diff --git a/src/store/thunks/licenseThunk.ts b/src/store/thunks/licenseThunk.ts index 722fa47a..6a58e873 100644 --- a/src/store/thunks/licenseThunk.ts +++ b/src/store/thunks/licenseThunk.ts @@ -1,36 +1,38 @@ 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, ValidationProblemDetails, SystemLicense } from 'shared/types'; +import { MESSAGES } from 'shared/constants'; +import { Endpoints_CompanyLicense, Endpoints_BasePath } from '@fossa-app/bridge/Services/Endpoints'; +import { mapCompanyLicense, mapError } from 'shared/helpers'; +import { systemLicenseClient, companyLicenseClient } from 'shared/configs/BridgeClients'; +import { unwrapBridgeValue } 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); + const data = unwrapBridgeValue(await systemLicenseClient.GetLicenseAsync(new AbortController().signal)); - return parsedResponse.data || rejectWithValue({ title: MESSAGES.error.license.system.notFound }); + return data || rejectWithValue({ title: MESSAGES.error.license.system.notFound }); } catch (error: any) { - return rejectWithValue(parseResponse<{ data: ErrorResponseDTO }>(error.response).data); + return rejectWithValue(error as ValidationProblemDetails); } } ); -export const fetchCompanyLicense = createAsyncThunk( +export const fetchCompanyLicense = createAsyncThunk( 'license/fetchCompanyLicense', async (_, { rejectWithValue }) => { try { - const { data } = await axios.get(ENDPOINTS.companyLicense); + const data = unwrapBridgeValue(await companyLicenseClient.GetLicenseAsync(new AbortController().signal)); return mapCompanyLicense(data); } catch (error) { return rejectWithValue({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.license.company.notFound, }); } @@ -45,24 +47,33 @@ export const uploadCompanyLicense = createAsyncThunk(ENDPOINTS.companyLicense, formData, config); dispatch(fetchCompanyLicense()); dispatch(setSuccess(MESSAGES.success.license.company.create)); } catch (error) { dispatch( setError({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.license.company.create, }) ); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(error as ValidationProblemDetails) as ErrorResponse; return rejectWithValue(mappedError); } diff --git a/src/store/thunks/onboardingThunk.ts b/src/store/thunks/onboardingThunk.ts index d49ca093..a0e62775 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 { ValidationProblemDetails } 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..524af66d 100644 --- a/src/store/thunks/profileThunk.ts +++ b/src/store/thunks/profileThunk.ts @@ -2,16 +2,18 @@ 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 { Employee, EntityInput, ErrorResponse, ValidationProblemDetails } from 'shared/types'; +import { MESSAGES } from 'shared/constants'; +import { employeeClient } from 'shared/configs/BridgeClients'; +import { unwrapBridgeUnitResult, unwrapBridgeValue } from 'shared/configs/BridgeResponses'; +import { EmployeeModificationModel } from '@fossa-app/bridge/Models/ApiModels/PayloadModels'; import { mapEmployee, mapError } from 'shared/helpers'; -export const fetchProfile = createAsyncThunk( +export const fetchProfile = createAsyncThunk( 'profile/fetchProfile', async (_, { getState, rejectWithValue }) => { try { - const { data } = await axios.get(ENDPOINTS.employee); + const data = unwrapBridgeValue(await employeeClient.GetCurrentEmployeeAsync(new AbortController().signal)); if (data) { const state = getState() as RootState; @@ -21,85 +23,86 @@ export const fetchProfile = createAsyncThunk, - { 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 }) => { + try { + const modModel = new EmployeeModificationModel(employee.firstName, employee.lastName, employee.fullName ?? null); + unwrapBridgeUnitResult(await employeeClient.CreateEmployeeAsync(modModel, new AbortController().signal)); + await dispatch(fetchProfile()).unwrap(); - dispatch(setSuccess(MESSAGES.success.employee.create)); - } catch (error) { - dispatch( - setError({ - ...(error as ErrorResponseDTO), - title: MESSAGES.error.employee.create, - }) - ); + dispatch(setSuccess(MESSAGES.success.employee.create)); + } catch (error) { + dispatch( + setError({ + ...(error as ValidationProblemDetails), + title: MESSAGES.error.employee.create, + }) + ); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(error as ValidationProblemDetails) 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); + unwrapBridgeUnitResult(await employeeClient.UpdateCurrentEmployeeAsync(modModel, new AbortController().signal)); dispatch(setSuccess(MESSAGES.success.employee.updateProfile)); } catch (error) { dispatch( setError({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.employee.updateProfile, }) ); - const mappedError = mapError(error as ErrorResponseDTO) as ErrorResponse; + const mappedError = mapError(error as ValidationProblemDetails) as ErrorResponse; return rejectWithValue(mappedError); } } ); -export const deleteProfile = createAsyncThunk( +export const deleteProfile = createAsyncThunk( 'profile/deleteProfile', async (_, { dispatch, rejectWithValue }) => { try { - await axios.delete(ENDPOINTS.employee); + unwrapBridgeUnitResult(await employeeClient.DeleteCurrentEmployeeAsync(new AbortController().signal)); dispatch(resetCompanyDatasourceTotalsFetchStatus()); dispatch(setSuccess(MESSAGES.success.employee.deleteProfile)); } catch (error) { - if ((error as ErrorResponseDTO).status === 424) { + if ((error as ValidationProblemDetails).status === 424) { dispatch( setError({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.employee.deleteProfileDependency, }) ); } else { dispatch( setError({ - ...(error as ErrorResponseDTO), + ...(error as ValidationProblemDetails), title: MESSAGES.error.employee.deleteProfile, }) ); } - return rejectWithValue(error as ErrorResponseDTO); + return rejectWithValue(error as ValidationProblemDetails); } } );