diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 429eb58..0c2ff5c 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -757,6 +757,25 @@ export interface GetPartnerLicensesResponse { */ 'childrenPartnerLicenses': Array; } +/** + * + * @export + * @interface GetPartnersResponse + */ +export interface GetPartnersResponse { + /** + * 合計件数 + * @type {number} + * @memberof GetPartnersResponse + */ + 'total': number; + /** + * + * @type {Array} + * @memberof GetPartnersResponse + */ + 'partners': Array; +} /** * * @export @@ -1028,6 +1047,55 @@ export interface OptionItemList { */ 'optionItemList': Array; } +/** + * + * @export + * @interface Partner + */ +export interface Partner { + /** + * 会社名 + * @type {string} + * @memberof Partner + */ + 'name': string; + /** + * 階層 + * @type {number} + * @memberof Partner + */ + 'tier': number; + /** + * アカウントID + * @type {number} + * @memberof Partner + */ + 'accountId': number; + /** + * 国 + * @type {string} + * @memberof Partner + */ + 'country': string; + /** + * プライマリ管理者 + * @type {string} + * @memberof Partner + */ + 'primaryAdmin': string; + /** + * プライマリ管理者メールアドレス + * @type {string} + * @memberof Partner + */ + 'email': string; + /** + * 代行操作許可 + * @type {boolean} + * @memberof Partner + */ + 'dealerManagement': boolean; +} /** * * @export @@ -1528,6 +1596,25 @@ export interface UpdateTypistGroupRequest { */ 'typistIds': Array; } +/** + * + * @export + * @interface UpdateWorktypesRequest + */ +export interface UpdateWorktypesRequest { + /** + * WorktypeID + * @type {string} + * @memberof UpdateWorktypesRequest + */ + 'worktypeId': string; + /** + * Worktypeの説明 + * @type {string} + * @memberof UpdateWorktypesRequest + */ + 'description'?: string; +} /** * * @export @@ -2037,6 +2124,54 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartners: async (limit: number, offset: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'limit' is not null or undefined + assertParamExists('getPartners', 'limit', limit) + // verify required parameter 'offset' is not null or undefined + assertParamExists('getPartners', 'offset', offset) + const localVarPath = `/accounts/partners`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (offset !== undefined) { + localVarQueryParameter['offset'] = offset; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します * @summary @@ -2256,6 +2391,50 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; localVarRequestOptions.data = serializeDataIfNeeded(updateTypistGroupRequest, localVarRequestOptions, configuration) + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorktype: async (id: number, updateWorktypesRequest: UpdateWorktypesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updateWorktype', 'id', id) + // verify required parameter 'updateWorktypesRequest' is not null or undefined + assertParamExists('updateWorktype', 'updateWorktypesRequest', updateWorktypesRequest) + const localVarPath = `/accounts/worktypes/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateWorktypesRequest, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -2379,6 +2558,18 @@ export const AccountsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getPartnerLicenses(getPartnerLicensesRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPartners(limit: number, offset: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(limit, offset, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します * @summary @@ -2443,6 +2634,18 @@ export const AccountsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.updateTypistGroup(typistGroupId, updateTypistGroupRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateWorktype(id, updateWorktypesRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -2551,6 +2754,17 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: any): AxiosPromise { return localVarFp.getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartners(limit: number, offset: number, options?: any): AxiosPromise { + return localVarFp.getPartners(limit, offset, options).then((request) => request(axios, basePath)); + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します * @summary @@ -2609,6 +2823,17 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP updateTypistGroup(typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options?: any): AxiosPromise { return localVarFp.updateTypistGroup(typistGroupId, updateTypistGroupRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: any): AxiosPromise { + return localVarFp.updateWorktype(id, updateWorktypesRequest, options).then((request) => request(axios, basePath)); + }, }; }; @@ -2737,6 +2962,19 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getPartners(limit: number, offset: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getPartners(limit, offset, options).then((request) => request(this.axios, this.basePath)); + } + /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します * @summary @@ -2806,6 +3044,19 @@ export class AccountsApi extends BaseAPI { public updateTypistGroup(typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options?: AxiosRequestConfig) { return AccountsApiFp(this.configuration).updateTypistGroup(typistGroupId, updateTypistGroupRequest, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateWorktype(id, updateWorktypesRequest, options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/dictation_client/src/features/workflow/worktype/operations.ts b/dictation_client/src/features/workflow/worktype/operations.ts index 5f754c9..4945831 100644 --- a/dictation_client/src/features/workflow/worktype/operations.ts +++ b/dictation_client/src/features/workflow/worktype/operations.ts @@ -110,3 +110,66 @@ export const addWorktypeAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const editWorktypeAsync = createAsyncThunk< + { + // return empty + }, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/editWorktypeAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + // stateからworktypeIdとdescriptionを取得する + const { selectedId, worktypeId, description } = state.worktype.apps; + + try { + await accountsApi.updateWorktype( + selectedId, + { + worktypeId, + description, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + // 既に同じworktypeIdが存在する場合 + if (error.code === "E011001") { + errorMessage = getTranslationID( + "worktypeIdSetting.message.alreadyWorktypeIdExistError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/workflow/worktype/selectors.ts b/dictation_client/src/features/workflow/worktype/selectors.ts index e1ec412..41d3a75 100644 --- a/dictation_client/src/features/workflow/worktype/selectors.ts +++ b/dictation_client/src/features/workflow/worktype/selectors.ts @@ -2,9 +2,19 @@ import { RootState } from "app/store"; export const selectWorktypes = (state: RootState) => state.worktype.domain.worktypes; + export const selectIsLoading = (state: RootState) => state.worktype.apps.isLoading; +export const selectIsAddLoading = (state: RootState) => + state.worktype.apps.isAddLoading; + +export const selectIsEditLoading = (state: RootState) => + state.worktype.apps.isEditLoading; + +export const selectSelectedId = (state: RootState) => + state.worktype.apps.selectedId; + export const selectWorktypeId = (state: RootState) => state.worktype.apps.worktypeId; diff --git a/dictation_client/src/features/workflow/worktype/state.ts b/dictation_client/src/features/workflow/worktype/state.ts index 2e101f7..d2006e2 100644 --- a/dictation_client/src/features/workflow/worktype/state.ts +++ b/dictation_client/src/features/workflow/worktype/state.ts @@ -7,6 +7,9 @@ export interface WorktypeState { export interface Apps { isLoading: boolean; + isAddLoading: boolean; + isEditLoading: boolean; + selectedId: number; worktypeId: string; description?: string; } diff --git a/dictation_client/src/features/workflow/worktype/worktypeSlice.ts b/dictation_client/src/features/workflow/worktype/worktypeSlice.ts index d0345e5..a064ec6 100644 --- a/dictation_client/src/features/workflow/worktype/worktypeSlice.ts +++ b/dictation_client/src/features/workflow/worktype/worktypeSlice.ts @@ -1,11 +1,19 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { WorktypeState } from "./state"; -import { addWorktypeAsync, listWorktypesAsync } from "./operations"; +import { + addWorktypeAsync, + editWorktypeAsync, + listWorktypesAsync, +} from "./operations"; const initialState: WorktypeState = { apps: { isLoading: false, + isAddLoading: false, + isEditLoading: false, + selectedId: NaN, worktypeId: "", + description: undefined, }, domain: {}, }; @@ -15,8 +23,13 @@ export const worktypeSlice = createSlice({ initialState, reducers: { cleanupWorktype: (state) => { + state.apps.selectedId = initialState.apps.selectedId; state.apps.worktypeId = initialState.apps.worktypeId; - state.apps.description = undefined; + state.apps.description = initialState.apps.description; + }, + changeSelectedId: (state, action: PayloadAction<{ id: number }>) => { + const { id } = action.payload; + state.apps.selectedId = id; }, changeWorktypeId: ( state, @@ -46,13 +59,29 @@ export const worktypeSlice = createSlice({ state.apps.isLoading = false; }); builder.addCase(addWorktypeAsync.pending, (state) => { - state.apps.isLoading = true; + state.apps.isAddLoading = true; }); builder.addCase(addWorktypeAsync.rejected, (state) => { - state.apps.isLoading = false; + state.apps.isAddLoading = false; + }); + builder.addCase(addWorktypeAsync.fulfilled, (state) => { + state.apps.isAddLoading = false; + }); + builder.addCase(editWorktypeAsync.pending, (state) => { + state.apps.isEditLoading = true; + }); + builder.addCase(editWorktypeAsync.rejected, (state) => { + state.apps.isEditLoading = false; + }); + builder.addCase(editWorktypeAsync.fulfilled, (state) => { + state.apps.isEditLoading = false; }); }, }); -export const { changeDescription, changeWorktypeId, cleanupWorktype } = - worktypeSlice.actions; +export const { + changeDescription, + changeWorktypeId, + changeSelectedId, + cleanupWorktype, +} = worktypeSlice.actions; export default worktypeSlice.reducer; diff --git a/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx b/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx index 1d2909e..6b7b79a 100644 --- a/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx +++ b/dictation_client/src/pages/LicensePage/cardLicenseActivatePopup.tsx @@ -46,24 +46,6 @@ export const CardLicenseActivatePopup: React.FC< onClose(); }, [isLoading, onClose]); - // ブラウザのウィンドウが閉じられようとしている場合に発火するイベントハンドラ - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - // isLoadingがtrueの場合は確認ダイアログを表示する - if (isLoading) { - e.preventDefault(); - // ChromeではreturnValueが必要 - e.returnValue = ""; - } - }; - // コンポーネントがマウントされた時にイベントハンドラを登録する - useEffect(() => { - window.addEventListener("beforeunload", handleBeforeUnload); - // コンポーネントがアンマウントされるときにイベントハンドラを解除する - return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); - }; - }); - useEffect( () => () => { // useEffectのreturnとしてcleanupAppsを実行することで、ポップアップのアンマウント時に初期化を行う diff --git a/dictation_client/src/pages/LicensePage/cardLicenseIssuePopup.tsx b/dictation_client/src/pages/LicensePage/cardLicenseIssuePopup.tsx index 84b466d..9298a50 100644 --- a/dictation_client/src/pages/LicensePage/cardLicenseIssuePopup.tsx +++ b/dictation_client/src/pages/LicensePage/cardLicenseIssuePopup.tsx @@ -31,7 +31,7 @@ export const CardLicenseIssuePopup: React.FC = ( // ブラウザのウィンドウが閉じられようとしている場合に発火するイベントハンドラ const handleBeforeUnload = (e: BeforeUnloadEvent) => { - // isLoadingがtrueの場合は確認ダイアログを表示する + // 後続の処理で、ブラウザのCSVダウンロードを行うため、ダイアログを表示させる if (isLoading) { e.preventDefault(); // ChromeではreturnValueが必要 diff --git a/dictation_client/src/pages/LicensePage/licenseOrderHistory.tsx b/dictation_client/src/pages/LicensePage/licenseOrderHistory.tsx index cc6d2ca..aee1606 100644 --- a/dictation_client/src/pages/LicensePage/licenseOrderHistory.tsx +++ b/dictation_client/src/pages/LicensePage/licenseOrderHistory.tsx @@ -291,8 +291,7 @@ export const LicenseOrderHistory: React.FC = ( ? styles.isActive : "" }`} - onClick={(event) => { - event.preventDefault(); + onClick={() => { issueLicense(x.poNumber); }} > @@ -312,8 +311,7 @@ export const LicenseOrderHistory: React.FC = ( ? styles.isActive : "" }`} - onClick={(event) => { - event.preventDefault(); + onClick={() => { onCancelIssue( selectedRow.accountId, x.poNumber diff --git a/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx b/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx index 565bfe7..43453b8 100644 --- a/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx +++ b/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx @@ -51,24 +51,7 @@ export const AddPartnerAccountPopup: React.FC = ( const email = useSelector(selectEmail); const isLoading = useSelector(selectIsLoading); - // ブラウザのウィンドウが閉じられようとしている場合に発火するイベントハンドラ - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - // isLoadingがtrueの場合は確認ダイアログを表示する - if (isLoading) { - e.preventDefault(); - // ChromeではreturnValueが必要 - e.returnValue = ""; - } - }; - // コンポーネントがマウントされた時にイベントハンドラを登録する - useEffect(() => { - window.addEventListener("beforeunload", handleBeforeUnload); - // コンポーネントがアンマウントされるときにイベントハンドラを解除する - return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); - }; - }); - + // ポップアップを閉じる処理 const closePopup = useCallback(() => { if (isLoading) { return; diff --git a/dictation_client/src/pages/SignupPage/signupInput.tsx b/dictation_client/src/pages/SignupPage/signupInput.tsx index fd9bef5..296d559 100644 --- a/dictation_client/src/pages/SignupPage/signupInput.tsx +++ b/dictation_client/src/pages/SignupPage/signupInput.tsx @@ -144,7 +144,7 @@ const SignupInput: React.FC = (): JSX.Element => { {t(getTranslationID("signupPage.text.pageExplanation"))}

-
+
diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx index e611f9f..7f4934f 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/addWorktypeIdPopup.tsx @@ -10,11 +10,13 @@ import { listWorktypesAsync, selectDescription, selectHasErrorWorktypeId, + selectIsAddLoading, selectWorktypeId, } from "features/workflow/worktype"; import { AppDispatch } from "app/store"; import { getTranslationID } from "translation"; import close from "../../assets/images/close.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; // popupのpropsの型定義 interface AddWorktypeIdPopupProps { @@ -28,6 +30,8 @@ export const AddWorktypeIdPopup: React.FC = ( const { onClose, isOpen } = props; const [t] = useTranslation(); const dispatch: AppDispatch = useDispatch(); + + const isAddLoading = useSelector(selectIsAddLoading); const worktypeId = useSelector(selectWorktypeId); const description = useSelector(selectDescription); // 追加ボタンを押したかどうか @@ -39,10 +43,13 @@ export const AddWorktypeIdPopup: React.FC = ( // ×ボタンを押した時の処理 const closePopup = useCallback(() => { + if (isAddLoading) { + return; + } dispatch(cleanupWorktype()); setIsPushAddButton(false); onClose(); - }, [onClose, dispatch]); + }, [onClose, dispatch, isAddLoading]); // 追加ボタンを押した時の処理 const addWorktypeId = useCallback(async () => { @@ -70,7 +77,7 @@ export const AddWorktypeIdPopup: React.FC = ( onClick={closePopup} />

- +
{t(getTranslationID("worktypeIdSetting.label.worktypeId"))}
@@ -119,9 +126,12 @@ export const AddWorktypeIdPopup: React.FC = ( value={description ?? ""} className={styles.formInput} onChange={(e) => { - const description = - e.target.value === "" ? undefined : e.target.value; - dispatch(changeDescription({ description })); + dispatch( + changeDescription({ + description: + e.target.value === "" ? undefined : e.target.value, + }) + ); }} /> @@ -132,9 +142,18 @@ export const AddWorktypeIdPopup: React.FC = ( value={t( getTranslationID("worktypeIdSetting.label.addWorktype") )} - className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`} + className={`${styles.formSubmit} ${styles.marginBtm1} ${ + !isAddLoading ? styles.isActive : "" + }`} onClick={addWorktypeId} /> + {isAddLoading && ( + Loading + )}
diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx new file mode 100644 index 0000000..248005b --- /dev/null +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/editWorktypeIdPopup.tsx @@ -0,0 +1,162 @@ +import React, { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import styles from "styles/app.module.scss"; +import { useDispatch, useSelector } from "react-redux"; +import { + editWorktypeAsync, + changeDescription, + changeWorktypeId, + cleanupWorktype, + listWorktypesAsync, + selectDescription, + selectHasErrorWorktypeId, + selectIsEditLoading, + selectWorktypeId, +} from "features/workflow/worktype"; +import { AppDispatch } from "app/store"; +import { getTranslationID } from "translation"; +import close from "../../assets/images/close.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +// popupのpropsの型定義 +interface EditWorktypeIdPopupProps { + onClose: () => void; + isOpen: boolean; +} + +export const EditWorktypeIdPopup: React.FC = ( + props: EditWorktypeIdPopupProps +): JSX.Element => { + const { onClose, isOpen } = props; + const [t] = useTranslation(); + const dispatch: AppDispatch = useDispatch(); + const isEditLoading = useSelector(selectIsEditLoading); + const worktypeId = useSelector(selectWorktypeId); + const description = useSelector(selectDescription); + // 保存ボタンを押したかどうか + const [isPushSaveButton, setIsPushSaveButton] = useState(false); + // WorktypeIdのバリデーションチェック + const { hasIncorrectPatternWorktypeId, isEmptyWorktypeId } = useSelector( + selectHasErrorWorktypeId + ); + + // ×ボタンを押した時の処理 + const closePopup = useCallback(() => { + if (isEditLoading) { + return; + } + dispatch(cleanupWorktype()); + setIsPushSaveButton(false); + onClose(); + }, [onClose, dispatch, isEditLoading]); + + // 保存ボタンを押した時の処理 + const saveWorktypeId = useCallback(async () => { + setIsPushSaveButton(true); + if (isEmptyWorktypeId || hasIncorrectPatternWorktypeId) { + return; + } + const { meta } = await dispatch(editWorktypeAsync()); + if (meta.requestStatus === "fulfilled") { + dispatch(listWorktypesAsync()); + closePopup(); + } + }, [closePopup, dispatch, hasIncorrectPatternWorktypeId, isEmptyWorktypeId]); + + return ( +
+
+

+ {t(getTranslationID("worktypeIdSetting.label.editWorktypeId"))} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */} + close +

+
+
+
+
{t(getTranslationID("worktypeIdSetting.label.worktypeId"))}
+
+ { + dispatch(changeWorktypeId({ worktypeId: e.target.value })); + }} + /> + {isPushSaveButton && isEmptyWorktypeId && ( + + {t(getTranslationID("common.message.inputEmptyError"))} + + )} + {isPushSaveButton && hasIncorrectPatternWorktypeId && ( + + {t( + getTranslationID( + "worktypeIdSetting.message.worktypeIdIncorrectError" + ) + )} + + )} + + {t(getTranslationID("worktypeIdSetting.label.worktypeIdTerms"))} + +
+
+ {t( + getTranslationID("worktypeIdSetting.label.descriptionOptional") + )} +
+
+ { + dispatch( + changeDescription({ + description: + e.target.value === "" ? undefined : e.target.value, + }) + ); + }} + /> +
+
+ + {isEditLoading && ( + Loading + )} +
+
+
+
+
+ ); +}; diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx index ef20581..6d3e7cf 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx @@ -10,12 +10,16 @@ import progress_activit from "assets/images/progress_activit.svg"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { + changeSelectedId, + changeWorktypeId, + changeDescription, listWorktypesAsync, selectIsLoading, selectWorktypes, } from "features/workflow/worktype"; import { AppDispatch } from "app/store"; import { AddWorktypeIdPopup } from "./addWorktypeIdPopup"; +import { EditWorktypeIdPopup } from "./editWorktypeIdPopup"; const WorktypeIdSettingPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -25,6 +29,8 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { const [selectedRow, setSelectedRow] = useState(NaN); // 追加Popupの表示制御 const [isShowAddPopup, setIsShowAddPopup] = useState(false); + // 編集Popupの表示制御 + const [isShowEditPopup, setIsShowEditPopup] = useState(false); useEffect(() => { dispatch(listWorktypesAsync()); }, [dispatch]); @@ -37,6 +43,12 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { }} isOpen={isShowAddPopup} /> + { + setIsShowEditPopup(false); + }} + isOpen={isShowEditPopup} + />
@@ -131,9 +143,23 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { className={`${styles.menuAction} ${styles.inTable}`} >
  • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} { + dispatch(changeSelectedId({ id: worktype.id })); + dispatch( + changeWorktypeId({ + worktypeId: worktype.worktypeId, + }) + ); + dispatch( + changeDescription({ + description: worktype.description, + }) + ); + setIsShowEditPopup(true); + }} > {t(getTranslationID("common.label.edit"))} diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 995e34a..e42918c 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -555,6 +555,9 @@ h3 + .brCrumb .tlIcon { border-top: 2px #282828 solid; background: #fafafa; } +.form select { + text-overflow: ellipsis; +} .form select:invalid { color: #999999; } @@ -689,6 +692,7 @@ h3 + .brCrumb .tlIcon { padding: 0.6rem 0.6rem; background: #f0f0f0; box-sizing: border-box; + word-wrap: break-word; } .formSubmit { min-width: 15rem; @@ -2288,10 +2292,11 @@ tr.isSelected .menuInTable li a { font-size: 0.9rem; } .workflow .menuAction.worktype .formInput { - width: inherit; + max-width: 350px; margin-left: 0.5rem; padding: 0.2rem 0.8rem; font-size: 0.9rem; + text-overflow: ellipsis; } .formList dd.formChange { diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index db871fe..5a7bd67 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -390,8 +390,10 @@ "description": "(de)Description", "descriptionOptional": "(de)Description (Optional)", "optionItem": "(de)Option Item", - "worktypeIdTerms": "(de)WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .", - "addWorktype": "(de)Add Worktype" + "worktypeIdTerms": "(de)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", + "addWorktype": "(de)Add Worktype", + "editWorktypeId": "(de)Edit Worktype ID", + "saveChange": "(de)Save Changes" }, "message": { "worktypeIdIncorrectError": "(de)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", @@ -414,4 +416,4 @@ "deleteAccount": "(de)Delete Account" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index a041725..63ce4e3 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -390,8 +390,10 @@ "description": "Description", "descriptionOptional": "Description (Optional)", "optionItem": "Option Item", - "worktypeIdTerms": "WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .", - "addWorktype": "Add Worktype" + "worktypeIdTerms": "WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", + "addWorktype": "Add Worktype", + "editWorktypeId": "Edit Worktype ID", + "saveChange": "Save Changes" }, "message": { "worktypeIdIncorrectError": "入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", @@ -414,4 +416,4 @@ "deleteAccount": "Delete Account" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 3fe365a..3edb794 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -390,8 +390,10 @@ "description": "(es)Description", "descriptionOptional": "(es)Description (Optional)", "optionItem": "(es)Option Item", - "worktypeIdTerms": "(es)WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .", - "addWorktype": "(es)Add Worktype" + "worktypeIdTerms": "(es)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", + "addWorktype": "(es)Add Worktype", + "editWorktypeId": "(es)Edit Worktype ID", + "saveChange": "(es)Save Changes" }, "message": { "worktypeIdIncorrectError": "(es)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", @@ -414,4 +416,4 @@ "deleteAccount": "(es)Delete Account" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 741646d..5ccc10f 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -390,8 +390,10 @@ "description": "(fr)Description", "descriptionOptional": "(fr)Description (Optional)", "optionItem": "(fr)Option Item", - "worktypeIdTerms": "(fr)WorktypeID should be alphanumeric and symbols,\n but not include: \\ / : * ? “ < > | .", - "addWorktype": "(fr)Add Worktype" + "worktypeIdTerms": "(fr)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", + "addWorktype": "(fr)Add Worktype", + "editWorktypeId": "(fr)Edit Worktype ID", + "saveChange": "(fr)Save Changes" }, "message": { "worktypeIdIncorrectError": "(fr)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", @@ -414,4 +416,4 @@ "deleteAccount": "(fr)Delete Account" } } -} +} \ No newline at end of file diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index da51092..7ecacde 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -901,6 +901,120 @@ "security": [{ "bearer": [] }] } }, + "/accounts/worktypes/{id}/option-items": { + "get": { + "operationId": "getOptionItems", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOptionItemsResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "updateOptionItems", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOptionItemsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOptionItemsResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/accounts/partners": { "get": { "operationId": "getPartners", @@ -2901,6 +3015,44 @@ "required": ["worktypeId"] }, "UpdateWorktypeResponse": { "type": "object", "properties": {} }, + "WorktypeOptionItem": { + "type": "object", + "properties": { + "itemLabel": { "type": "string", "maxLength": 16 }, + "defaultValueType": { + "type": "string", + "maxLength": 20, + "description": "Default / Blank / LastInput" + }, + "initialValue": { "type": "string", "maxLength": 20 } + }, + "required": ["itemLabel", "defaultValueType", "initialValue"] + }, + "GetOptionItemsResponse": { + "type": "object", + "properties": { + "optionItems": { + "maxItems": 10, + "minItems": 10, + "type": "array", + "items": { "$ref": "#/components/schemas/WorktypeOptionItem" } + } + }, + "required": ["optionItems"] + }, + "UpdateOptionItemsRequest": { + "type": "object", + "properties": { + "optionItems": { + "maxItems": 10, + "minItems": 10, + "type": "array", + "items": { "$ref": "#/components/schemas/WorktypeOptionItem" } + } + }, + "required": ["optionItems"] + }, + "UpdateOptionItemsResponse": { "type": "object", "properties": {} }, "Partner": { "type": "object", "properties": { diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index ec2b531..014c7e1 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -9,6 +9,7 @@ import { UsersRepositoryService } from '../../repositories/users/users.repositor import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; import { Account } from '../../repositories/accounts/entity/account.entity'; +import { AdB2cUser } from '../../gateways/adb2c/types/types'; // ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ### @@ -28,6 +29,10 @@ export const overrideAdB2cService = ( username: string, ) => Promise<{ sub: string } | ConflictError>; deleteUser?: (externalId: string, context: Context) => Promise; + getUsers?: ( + context: Context, + externalIds: string[], + ) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -44,6 +49,12 @@ export const overrideAdB2cService = ( writable: true, }); } + if (overrides.getUsers) { + Object.defineProperty(obj, obj.getUsers.name, { + value: overrides.getUsers, + writable: true, + }); + } }; /** diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts index a2f1005..a5c998b 100644 --- a/dictation_server/src/common/test/utility.ts +++ b/dictation_server/src/common/test/utility.ts @@ -149,6 +149,8 @@ export const makeTestAccount = async ( datasource: DataSource, defaultAccountValue?: AccountDefault, defaultAdminUserValue?: UserDefault, + isPrimaryAdminNotExist?: boolean, + isSecondaryAdminNotExist?: boolean, ): Promise<{ account: Account; admin: User }> => { let accountId: number; let userId: number; @@ -198,10 +200,15 @@ export const makeTestAccount = async ( } // Accountの管理者を設定する + let secondaryAdminUserId = null; + if (isPrimaryAdminNotExist && !isSecondaryAdminNotExist) { + secondaryAdminUserId = userId; + } await datasource.getRepository(Account).update( { id: accountId }, { - primary_admin_user_id: userId, + primary_admin_user_id: isPrimaryAdminNotExist ? null : userId, + secondary_admin_user_id: secondaryAdminUserId, }, ); diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index ca38ec5..66471b9 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -238,3 +238,11 @@ export const OPTION_ITEM_VALUE_TYPE = { BLANK: 'Blank', LAST_INPUT: 'LastInput', } as const; + +/** + * ADB2Cユーザのidentity.signInType + * @const {string[]} + */ +export const ADB2C_SIGN_IN_TYPE = { + EAMILADDRESS: 'emailAddress', +} as const; diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 93abc48..7099620 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -51,8 +51,18 @@ import { UpdateWorktypeRequestParam, UpdateWorktypeResponse, UpdateWorktypesRequest, + GetOptionItemsRequestParam, + GetOptionItemsResponse, + UpdateOptionItemsResponse, + UpdateOptionItemsRequestParam, + UpdateOptionItemsRequest, } from './types/types'; -import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; +import { + USER_ROLES, + ADMIN_ROLES, + TIERS, + OPTION_ITEM_VALUE_TYPE, +} from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { retrieveAuthorizationToken } from '../../common/http/helper'; @@ -770,6 +780,144 @@ export class AccountsController { return {}; } + @Get('/worktypes/:id/option-items') + @ApiResponse({ + status: HttpStatus.OK, + type: GetOptionItemsResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'WorktypeIDが不在', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'getOptionItems' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) + async getOptionItems( + @Req() req: Request, + @Param() param: GetOptionItemsRequestParam, + ): Promise { + const { id } = param; + const token = retrieveAuthorizationToken(req); + const { userId } = jwt.decode(token, { json: true }) as AccessToken; + + const context = makeContext(userId); + + console.log('id', id); + console.log(context.trackingId); + + return { + optionItems: [ + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + { + itemLabel: '', + defaultValueType: OPTION_ITEM_VALUE_TYPE.DEFAULT, + initialValue: '', + }, + ], + }; + } + + @Post('/worktypes/:id/option-items') + @ApiResponse({ + status: HttpStatus.OK, + type: UpdateOptionItemsResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'WorktypeIDが不在', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'updateOptionItems' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] })) + async updateOptionItems( + @Req() req: Request, + @Param() param: UpdateOptionItemsRequestParam, + @Body() body: UpdateOptionItemsRequest, + ): Promise { + const { optionItems } = body; + const { id } = param; + const token = retrieveAuthorizationToken(req); + const { userId } = jwt.decode(token, { json: true }) as AccessToken; + + const context = makeContext(userId); + + console.log('id', id); + console.log('optionItems', optionItems); + console.log(context.trackingId); + + return {}; + } + @Get('/partners') @ApiResponse({ status: HttpStatus.OK, @@ -809,45 +957,13 @@ export class AccountsController { const { userId } = jwt.decode(token, { json: true }) as AccessToken; const context = makeContext(userId); - // TODO: パートナー取得APIで実装 - // await this.accountService.getPartners( - // context, - // body.limit, - // body.offset, - // ); + const response = await this.accountService.getPartners( + context, + userId, + limit, + offset, + ); - // 仮のreturn - return { - total: 1, - partners: [ - { - name: 'testA', - tier: 5, - accountId: 1, - country: 'US', - primaryAdmin: 'nameA', - email: 'aaa@example.com', - dealerManagement: true, - }, - { - name: 'testB', - tier: 5, - accountId: 2, - country: 'US', - primaryAdmin: 'nameB', - email: 'bbb@example.com', - dealerManagement: false, - }, - { - name: 'testC', - tier: 5, - accountId: 1, - country: 'US', - primaryAdmin: 'nothing', - email: 'nothing', - dealerManagement: false, - }, - ], - }; + return response; } } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 618abf9..6932651 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -37,6 +37,7 @@ import { import { AccountsService } from './accounts.service'; import { Context, makeContext } from '../../common/log'; import { + ADB2C_SIGN_IN_TYPE, LICENSE_ALLOCATED_STATUS, LICENSE_ISSUE_STATUS, LICENSE_TYPE, @@ -61,6 +62,7 @@ import { selectOrderLicense, } from '../licenses/test/utility'; import { WorktypesRepositoryService } from '../../repositories/worktypes/worktypes.repository.service'; +import { AdB2cUser } from '../../gateways/adb2c/types/types'; import { Worktype } from '../../repositories/worktypes/entity/worktype.entity'; describe('createAccount', () => { @@ -4155,3 +4157,144 @@ describe('ライセンス発行キャンセル', () => { ); }); }); + +describe('パートナー一覧取得', () => { + let source: DataSource = null; + beforeEach(async () => { + source = new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: false, + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + await source.destroy(); + source = null; + }); + it('パートナー一覧を取得する', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const { tier1Accounts: tier1Accounts, tier2Accounts: tier2Accounts } = + await makeHierarchicalAccounts(source); + const tier1Difference = await makeTestAccount(source, { + tier: 1, + }); + const tier2_3 = await makeTestAccount( + source, + { + parent_account_id: tier1Accounts[0].account.id, + tier: 2, + }, + {}, + true, + false, + ); + const tier2_4 = await makeTestAccount( + source, + { + parent_account_id: tier1Accounts[0].account.id, + tier: 2, + }, + {}, + true, + true, + ); + + await makeTestAccount(source, { + parent_account_id: tier1Difference.account.id, + tier: 2, + }); + + const adb2cReturn = [ + { + id: tier2Accounts[0].users[0].external_id, + displayName: 'partner1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'partner1@example.com', + }, + ], + }, + { + id: tier2Accounts[1].users[0].external_id, + displayName: 'partner2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'partner2@example.com', + }, + ], + }, + ] as AdB2cUser[]; + + overrideAdB2cService(service, { + getUsers: async (_context: Context, _externalIds: string[]) => { + return adb2cReturn; + }, + }); + + const partners = await service.getPartners( + makeContext('trackingId'), + tier1Accounts[0].users[0].external_id, + 15, + 0, + ); + + // 違うアカウントのパートナーは取得していないこと + expect(partners.total).toBe(4); + // 会社名の昇順に取得できていること + expect(partners.partners[0].name).toBe( + tier2Accounts[1].account.company_name, + ); + expect(partners.partners[1].name).toBe( + tier2Accounts[0].account.company_name, + ); + expect(partners.partners[2].name).toBe(tier2_3.account.company_name); + expect(partners.partners[3].name).toBe(tier2_4.account.company_name); + expect(partners.partners[0].email).toBe('partner2@example.com'); + expect(partners.partners[1].email).toBe('partner1@example.com'); + expect(partners.partners[2].email).toBeUndefined; + expect(partners.partners[3].email).toBeUndefined; + + expect(partners.partners[0].tier).toBe(tier2Accounts[1].account.tier); + expect(partners.partners[0].country).toBe(tier2Accounts[1].account.country); + expect(partners.partners[0].accountId).toBe(tier2Accounts[1].account.id); + expect(partners.partners[0].tier).toBe(tier2Accounts[1].account.tier); + expect(partners.partners[0].primaryAdmin).toBe('partner2'); + expect(partners.partners[0].dealerManagement).toBe( + tier2Accounts[1].account.delegation_permission, + ); + }); + it('パートナー一覧を取得する(パートナーが0件の場合)', async () => { + const module = await makeTestingModule(source); + const service = module.get(AccountsService); + const account = await makeTestAccount(source, { + tier: 1, + }); + + const adb2cReturn = [{}] as AdB2cUser[]; + + overrideAdB2cService(service, { + getUsers: async (_context: Context, _externalIds: string[]) => { + return adb2cReturn; + }, + }); + + const partners = await service.getPartners( + makeContext('trackingId'), + account.admin.external_id, + 15, + 0, + ); + + // 結果が0件で成功となること + expect(partners.total).toBe(0); + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index bdfb545..23a9fc2 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -10,7 +10,7 @@ import { } from '../../gateways/adb2c/adb2c.service'; import { Account } from '../../repositories/accounts/entity/account.entity'; import { User } from '../../repositories/users/entity/user.entity'; -import { TIERS, USER_ROLES } from '../../constants'; +import { TIERS, USER_ROLES, ADB2C_SIGN_IN_TYPE } from '../../constants'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { TypistGroup, @@ -23,6 +23,7 @@ import { GetMyAccountResponse, GetTypistGroupResponse, GetWorktypesResponse, + GetPartnersResponse, } from './types/types'; import { DateWithZeroTime, @@ -1312,4 +1313,85 @@ export class AccountsService { ); } } + + /** + * パートナー一覧を取得します + * @param context + * @param externalId + * @param limit + * @param offset + * @returns GetPartnersResponse + */ + async getPartners( + context: Context, + externalId: string, + limit: number, + offset: number, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.getPartners.name} | params: { ` + + `externalId: ${externalId}, ` + + `limit: ${limit}, ` + + `offset: ${offset}, };`, + ); + + try { + const { account_id: accountId } = + await this.usersRepository.findUserByExternalId(externalId); + + const partners = await this.accountRepository.getPartners( + accountId, + limit, + offset, + ); + + // DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する + let externalIds = partners.partnersInfo.map( + (x) => x.primaryAccountExternalId, + ); + externalIds = externalIds.filter((item) => item !== undefined); + const adb2cUsers = await this.adB2cService.getUsers(context, externalIds); + + // DBから取得した情報とADB2Cから取得した情報をマージ + const response = partners.partnersInfo.map((db) => { + const adb2cUser = adb2cUsers.find( + (adb2c) => db.primaryAccountExternalId === adb2c.id, + ); + + let primaryAdmin = undefined; + let mail = undefined; + if (adb2cUser) { + primaryAdmin = adb2cUser.displayName; + mail = adb2cUser.identities.find( + (identity) => + identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS, + ).issuerAssignedId; + } + return { + name: db.name, + tier: db.tier, + accountId: db.accountId, + country: db.country, + primaryAdmin: primaryAdmin, + email: mail, + dealerManagement: db.dealerManagement, + }; + }); + + return { + total: partners.total, + partners: response, + }; + } catch (e) { + this.logger.error(`error=${e}`); + if (e instanceof Error) { + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } finally { + this.logger.log(`[OUT] [${context.trackingId}] ${this.getPartners.name}`); + } + } } diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 9679586..216b3cb 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -9,11 +9,15 @@ import { ArrayMinSize, MinLength, IsArray, + IsIn, + ArrayMaxSize, + ValidateNested, } from 'class-validator'; import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator'; import { IsUnique } from '../../../common/validators/IsUnique.validator'; import { Type } from 'class-transformer'; import { IsWorktypeId } from '../../../common/validators/worktype.validator'; +import { OPTION_ITEM_VALUE_TYPE } from '../../../constants'; export class CreateAccountRequest { @ApiProperty() @@ -376,6 +380,62 @@ export class UpdateWorktypesRequest { export class UpdateWorktypeResponse {} +export class WorktypeOptionItem { + @ApiProperty({ maxLength: 16 }) + @MaxLength(16) + itemLabel: string; + @ApiProperty({ + maxLength: 20, + description: `${Object.values(OPTION_ITEM_VALUE_TYPE).join(' / ')}`, + }) + @MaxLength(20) + @IsIn(Object.values(OPTION_ITEM_VALUE_TYPE)) + defaultValueType: string; + @ApiProperty({ maxLength: 20 }) + @MaxLength(20) + initialValue: string; +} +export class GetOptionItemsResponse { + @ApiProperty({ + maxItems: 10, + minItems: 10, + type: [WorktypeOptionItem], + }) + optionItems: WorktypeOptionItem[]; +} + +export class GetOptionItemsRequestParam { + @ApiProperty({ description: 'Worktypeの内部ID' }) + @Type(() => Number) + @IsInt() + @Min(0) + id: number; +} + +export class UpdateOptionItemsRequest { + @ApiProperty({ + maxItems: 10, + minItems: 10, + type: [WorktypeOptionItem], + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => WorktypeOptionItem) + @ArrayMinSize(10) + @ArrayMaxSize(10) + optionItems: WorktypeOptionItem[]; +} + +export class UpdateOptionItemsResponse {} + +export class UpdateOptionItemsRequestParam { + @ApiProperty({ description: 'Worktypeの内部ID' }) + @Type(() => Number) + @IsInt() + @Min(0) + id: number; +} + export class UpdateWorktypeRequestParam { @ApiProperty({ description: 'Worktypeの内部ID' }) @Type(() => Number) @@ -420,3 +480,13 @@ export class GetPartnersResponse { @ApiProperty({ type: [Partner] }) partners: Partner[]; } + +// RepositoryからPartnerLicenseInfoに関する情報を取得する際の型 +export type PartnerInfoFromDb = { + name: string; + tier: number; + accountId: number; + country: string; + primaryAccountExternalId: string; + dealerManagement: boolean; +}; diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index 4882ce2..d874872 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -17,6 +17,7 @@ import { TaskListSortableAttribute, } from '../../../common/types/sort'; import { AdB2cUser } from '../../../gateways/adb2c/types/types'; +import { ADB2C_SIGN_IN_TYPE } from '../../../constants'; export type SortCriteriaRepositoryMockValue = { updateSortCriteria: SortCriteria | Error; @@ -403,7 +404,7 @@ const AdB2cMockUsers: AdB2cUser[] = [ displayName: 'test1', identities: [ { - signInType: 'emailAddress', + signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, issuer: 'issuer', issuerAssignedId: 'test1@mail.com', }, @@ -414,7 +415,7 @@ const AdB2cMockUsers: AdB2cUser[] = [ displayName: 'test2', identities: [ { - signInType: 'emailAddress', + signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, issuer: 'issuer', issuerAssignedId: 'test2@mail.com', }, @@ -425,7 +426,7 @@ const AdB2cMockUsers: AdB2cUser[] = [ displayName: 'test3', identities: [ { - signInType: 'emailAddress', + signInType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, issuer: 'issuer', issuerAssignedId: 'test3@mail.com', }, diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 0fc460c..112140b 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -33,6 +33,7 @@ import { UserNotFoundError, } from '../../repositories/users/errors/types'; import { + ADB2C_SIGN_IN_TYPE, LICENSE_EXPIRATION_THRESHOLD_DAYS, USER_LICENSE_STATUS, USER_ROLES, @@ -470,7 +471,7 @@ export class UsersService { // メールアドレスを取得する const mail = adb2cUser.identities.find( - (identity) => identity.signInType === 'emailAddress', + (identity) => identity.signInType === ADB2C_SIGN_IN_TYPE.EAMILADDRESS, ).issuerAssignedId; let status = USER_LICENSE_STATUS.NORMAL; diff --git a/dictation_server/src/gateways/adb2c/adb2c.service.ts b/dictation_server/src/gateways/adb2c/adb2c.service.ts index 7d8c17b..d1000c5 100644 --- a/dictation_server/src/gateways/adb2c/adb2c.service.ts +++ b/dictation_server/src/gateways/adb2c/adb2c.service.ts @@ -7,6 +7,7 @@ import axios from 'axios'; import { Aadb2cUser, B2cMetadata, JwkSignKey } from '../../common/token'; import { AdB2cResponse, AdB2cUser } from './types/types'; import { Context } from '../../common/log'; +import { ADB2C_SIGN_IN_TYPE } from '../../constants'; export type ConflictError = { reason: 'email'; @@ -74,7 +75,7 @@ export class AdB2cService { }, identities: [ { - signinType: 'emailAddress', + signinType: ADB2C_SIGN_IN_TYPE.EAMILADDRESS, issuer: `${this.tenantName}.onmicrosoft.com`, issuerAssignedId: email, }, diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index cdd882c..eb6523a 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -27,6 +27,7 @@ import { import { LicenseSummaryInfo, PartnerLicenseInfoForRepository, + PartnerInfoFromDb, } from '../../features/accounts/types/types'; import { AccountNotFoundError } from './errors/types'; import { @@ -685,4 +686,83 @@ export class AccountsRepositoryService { await licenseRepo.delete({ order_id: targetOrder.id }); }); } + + /** + * アカウントIDをもとに、パートナー一覧を取得する + * @param id + * @param limit + * @param offset + * @returns total: 総件数 + * @returns partners: DBから取得できるパートナー一覧情報 + */ + async getPartners( + id: number, + limit: number, + offset: number, + ): Promise<{ + total: number; + partnersInfo: PartnerInfoFromDb[]; + }> { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + + // limit/offsetによらない総件数を取得する + const total = await accountRepo.count({ + where: { + parent_account_id: id, + }, + }); + + const partnerAccounts = await accountRepo.find({ + where: { + parent_account_id: id, + }, + order: { + company_name: 'ASC', + }, + take: limit, + skip: offset, + }); + + // ADB2Cから情報を取得するための外部ユーザIDを取得する(念のためプライマリ管理者IDが存在しない場合を考慮) + const primaryUserIds = partnerAccounts.map((x) => { + if (x.primary_admin_user_id) { + return x.primary_admin_user_id; + } else if (x.secondary_admin_user_id) { + return x.secondary_admin_user_id; + } + }); + const userRepo = entityManager.getRepository(User); + const primaryUsers = await userRepo.find({ + where: { + id: In(primaryUserIds), + }, + }); + + // アカウント情報とプライマリ管理者の外部ユーザIDをマージ + const partners = partnerAccounts.map((account) => { + const primaryUser = primaryUsers.find( + (user) => + user.id === account.primary_admin_user_id || + user.id === account.secondary_admin_user_id, + ); + const primaryAccountExternalId = primaryUser + ? primaryUser.external_id + : undefined; + return { + name: account.company_name, + tier: account.tier, + accountId: account.id, + country: account.country, + primaryAccountExternalId: primaryAccountExternalId, + dealerManagement: account.delegation_permission, + }; + }); + + return { + total: total, + partnersInfo: partners, + }; + }); + } }