From e6d6e477d95f32aa428d6f06b730e0379b07dfe0 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Mon, 8 Apr 2024 07:41:05 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20862:=20=E3=83=91=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=83=8A=E3=83=BC=E4=B8=80=E8=A6=A7=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=EF=BC=86=E3=83=91=E3=83=BC=E3=83=88=E3=83=8A=E3=83=BC=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E3=83=9D=E3=83=83=E3=83=97=E3=82=A2=E3=83=83=E3=83=97?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3935: パートナー一覧画面&パートナー編集ポップアップ実装](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3935) - パートナー一覧画面からパートナー編集ポップアップを表示して情報を変更できる画面実装をしています。 ## レビューポイント - エラーの表示は適切でしょうか? - 画面イメージは認識通りでしょうか? ## UIの変更 - [Task3935](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3935?csf=1&web=1&e=FdaUMT) ## クエリの変更 - なし ## 動作確認状況 - ローカルで確認 - 行った修正がデグレを発生させていないことを確認できるか - 新規機能なので問題なし --- dictation_client/src/api/api.ts | 232 ++++++++++++++++++ dictation_client/src/common/errors/code.ts | 2 + dictation_client/src/common/errors/types.ts | 2 +- .../src/features/dictation/constants.ts | 8 +- .../src/features/partner/operations.ts | 109 ++++++++ .../src/features/partner/partnerSlice.ts | 66 +++++ .../src/features/partner/selectors.ts | 14 ++ .../src/features/partner/state.ts | 8 + dictation_client/src/features/user/state.ts | 6 +- dictation_client/src/features/user/types.ts | 4 +- .../src/features/workflow/worktype/types.ts | 2 +- .../PartnerPage/addPartnerAccountPopup.tsx | 1 + .../PartnerPage/editPartnerAccountPopup.tsx | 178 ++++++++++++++ .../src/pages/PartnerPage/index.tsx | 39 +++ dictation_client/src/translation/de.json | 12 +- dictation_client/src/translation/en.json | 14 +- dictation_client/src/translation/es.json | 12 +- dictation_client/src/translation/fr.json | 12 +- .../features/accounts/accounts.controller.ts | 8 +- 19 files changed, 701 insertions(+), 28 deletions(-) create mode 100644 dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index c363b52..993f4c0 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -1035,6 +1035,32 @@ export interface GetPartnerLicensesResponse { */ 'childrenPartnerLicenses': Array; } +/** + * + * @export + * @interface GetPartnerUsersRequest + */ +export interface GetPartnerUsersRequest { + /** + * 取得対象のアカウントID + * @type {number} + * @memberof GetPartnerUsersRequest + */ + 'targetAccountId': number; +} +/** + * + * @export + * @interface GetPartnerUsersResponse + */ +export interface GetPartnerUsersResponse { + /** + * + * @type {Array} + * @memberof GetPartnerUsersResponse + */ + 'users': Array; +} /** * * @export @@ -1585,6 +1611,37 @@ export interface PartnerLicenseInfo { */ 'issueRequesting': number; } +/** + * + * @export + * @interface PartnerUser + */ +export interface PartnerUser { + /** + * ユーザーID + * @type {number} + * @memberof PartnerUser + */ + 'id': number; + /** + * ユーザー名 + * @type {string} + * @memberof PartnerUser + */ + 'name': string; + /** + * メールアドレス + * @type {string} + * @memberof PartnerUser + */ + 'email': string; + /** + * プライマリ管理者かどうか + * @type {boolean} + * @memberof PartnerUser + */ + 'isPrimaryAdmin': boolean; +} /** * * @export @@ -2289,6 +2346,31 @@ export interface UpdateOptionItemsRequest { */ 'optionItems': Array; } +/** + * + * @export + * @interface UpdatePartnerInfoRequest + */ +export interface UpdatePartnerInfoRequest { + /** + * 変更対象アカウントID + * @type {number} + * @memberof UpdatePartnerInfoRequest + */ + 'targetAccountId': number; + /** + * プライマリ管理者ID + * @type {number} + * @memberof UpdatePartnerInfoRequest + */ + 'primaryAdminUserId': number; + /** + * 会社名 + * @type {string} + * @memberof UpdatePartnerInfoRequest + */ + 'companyName': string; +} /** * * @export @@ -3318,6 +3400,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartnerUsers: async (getPartnerUsersRequest: GetPartnerUsersRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getPartnerUsersRequest' is not null or undefined + assertParamExists('getPartnerUsers', 'getPartnerUsersRequest', getPartnerUsersRequest) + const localVarPath = `/accounts/partner/users`; + // 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(getPartnerUsersRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -3710,6 +3832,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartnerInfo: async (updatePartnerInfoRequest: UpdatePartnerInfoRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updatePartnerInfoRequest' is not null or undefined + assertParamExists('updatePartnerInfo', 'updatePartnerInfoRequest', updatePartnerInfoRequest) + const localVarPath = `/accounts/partner/update`; + // 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(updatePartnerInfoRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -4092,6 +4254,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.getPartnerLicenses']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPartnerUsers(getPartnerUsersRequest: GetPartnerUsersRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPartnerUsers(getPartnerUsersRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getPartnerUsers']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -4221,6 +4396,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.updateOptionItems']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updatePartnerInfo(updatePartnerInfoRequest: UpdatePartnerInfoRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updatePartnerInfo(updatePartnerInfoRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updatePartnerInfo']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -4459,6 +4647,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: any): AxiosPromise { return localVarFp.getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(axios, basePath)); }, + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartnerUsers(getPartnerUsersRequest: GetPartnerUsersRequest, options?: any): AxiosPromise { + return localVarFp.getPartnerUsers(getPartnerUsersRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4558,6 +4756,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: any): AxiosPromise { return localVarFp.updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(axios, basePath)); }, + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartnerInfo(updatePartnerInfoRequest: UpdatePartnerInfoRequest, options?: any): AxiosPromise { + return localVarFp.updatePartnerInfo(updatePartnerInfoRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4825,6 +5033,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getPartnerUsers(getPartnerUsersRequest: GetPartnerUsersRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getPartnerUsers(getPartnerUsersRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary @@ -4944,6 +5164,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updatePartnerInfo(updatePartnerInfoRequest: UpdatePartnerInfoRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updatePartnerInfo(updatePartnerInfoRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index ed1570d..477282c 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -82,4 +82,6 @@ export const errorCodes = [ "E017003", // 親アカウント変更不可エラー(リージョンが同一でない) "E017004", // 親アカウント変更不可エラー(国が同一でない) "E018001", // パートナーアカウント削除エラー(削除条件を満たしていない) + "E019001", // パートナーアカウント取得不可エラー(階層構造が不正) + "E020001", // パートナーアカウント変更エラー(変更条件を満たしていない) ] as const; diff --git a/dictation_client/src/common/errors/types.ts b/dictation_client/src/common/errors/types.ts index 8cc801e..f4a42b4 100644 --- a/dictation_client/src/common/errors/types.ts +++ b/dictation_client/src/common/errors/types.ts @@ -6,4 +6,4 @@ export type ErrorObject = { statusCode?: number; }; -export type ErrorCodeType = typeof errorCodes[number]; +export type ErrorCodeType = (typeof errorCodes)[number]; diff --git a/dictation_client/src/features/dictation/constants.ts b/dictation_client/src/features/dictation/constants.ts index f7e22d1..ac273ce 100644 --- a/dictation_client/src/features/dictation/constants.ts +++ b/dictation_client/src/features/dictation/constants.ts @@ -6,7 +6,7 @@ export const STATUS = { BACKUP: "Backup", } as const; -export type StatusType = typeof STATUS[keyof typeof STATUS]; +export type StatusType = (typeof STATUS)[keyof typeof STATUS]; export const LIMIT_TASK_NUM = 100; @@ -26,7 +26,7 @@ export const SORTABLE_COLUMN = { TranscriptionFinishedDate: "TRANSCRIPTION_FINISHED_DATE", } as const; export type SortableColumnType = - typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; + (typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN]; export const isSortableColumnType = ( value: string @@ -36,14 +36,14 @@ export const isSortableColumnType = ( }; export type SortableColumnList = - typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; + (typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN]; export const DIRECTION = { ASC: "ASC", DESC: "DESC", } as const; -export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION]; +export type DirectionType = (typeof DIRECTION)[keyof typeof DIRECTION]; // DirectionTypeの型チェック関数 export const isDirectionType = (arg: string): arg is DirectionType => diff --git a/dictation_client/src/features/partner/operations.ts b/dictation_client/src/features/partner/operations.ts index 2dac369..8c832f6 100644 --- a/dictation_client/src/features/partner/operations.ts +++ b/dictation_client/src/features/partner/operations.ts @@ -9,6 +9,7 @@ import { CreatePartnerAccountRequest, GetPartnersResponse, DeletePartnerAccountRequest, + GetPartnerUsersResponse, } from "../../api/api"; import { Configuration } from "../../api/configuration"; @@ -176,3 +177,111 @@ export const deletePartnerAccountAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +// パートナーアカウントユーザー取得 +export const getPartnerUsersAsync = createAsyncThunk< + GetPartnerUsersResponse, + { + // パラメータ + accountId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("partner/getPartnerUsersAsync", async (args, thunkApi) => { + const { accountId } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + try { + const res = await accountApi.getPartnerUsers( + { targetAccountId: accountId }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + + return res.data; + } catch (e) { + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); + +// パートナーアカウントユーザー編集 +export const editPartnerInfoAsync = createAsyncThunk< + { + /* Empty Object */ + }, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("partner/editPartnerInfoAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + const { id, companyName, selectedAdminId } = state.partner.apps.editPartner; + + try { + await accountApi.updatePartnerInfo( + { + targetAccountId: id, + primaryAdminUserId: selectedAdminId, + companyName, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + + return {}; + } catch (e) { + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + if (error.code === "E010502" || error.code === "E020001") { + errorMessage = getTranslationID("partnerPage.message.editFailedError"); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/partner/partnerSlice.ts b/dictation_client/src/features/partner/partnerSlice.ts index cf09f4a..5edf14b 100644 --- a/dictation_client/src/features/partner/partnerSlice.ts +++ b/dictation_client/src/features/partner/partnerSlice.ts @@ -4,6 +4,8 @@ import { createPartnerAccountAsync, getPartnerInfoAsync, deletePartnerAccountAsync, + getPartnerUsersAsync, + editPartnerInfoAsync, } from "./operations"; import { LIMIT_PARTNER_VIEW_NUM } from "./constants"; @@ -21,6 +23,13 @@ const initialState: PartnerState = { adminName: "", email: "", }, + editPartner: { + users: [], + id: 0, + companyName: "", + country: "", + selectedAdminId: 0, + }, limit: LIMIT_PARTNER_VIEW_NUM, offset: 0, isLoading: false, @@ -79,6 +88,37 @@ export const partnerSlice = createSlice({ state.apps.delegatedAccountId = undefined; state.apps.delegatedCompanyName = undefined; }, + changeEditPartner: ( + state, + action: PayloadAction<{ + id: number; + companyName: string; + country: string; + }> + ) => { + const { id, companyName, country } = action.payload; + + state.apps.editPartner.id = id; + state.apps.editPartner.companyName = companyName; + state.apps.editPartner.country = country; + }, + changeEditCompanyName: ( + state, + action: PayloadAction<{ companyName: string }> + ) => { + const { companyName } = action.payload; + state.apps.editPartner.companyName = companyName; + }, + changeSelectedAdminId: ( + state, + action: PayloadAction<{ adminId: number }> + ) => { + const { adminId } = action.payload; + state.apps.editPartner.selectedAdminId = adminId; + }, + cleanupPartnerAccount: (state) => { + state.apps.editPartner = initialState.apps.editPartner; + }, }, extraReducers: (builder) => { builder.addCase(createPartnerAccountAsync.pending, (state) => { @@ -110,6 +150,28 @@ export const partnerSlice = createSlice({ builder.addCase(deletePartnerAccountAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(getPartnerUsersAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(getPartnerUsersAsync.fulfilled, (state, action) => { + const { users } = action.payload; + state.apps.editPartner.users = users; + state.apps.editPartner.selectedAdminId = + users.find((user) => user.isPrimaryAdmin)?.id ?? 0; + state.apps.isLoading = false; + }); + builder.addCase(getPartnerUsersAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(editPartnerInfoAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(editPartnerInfoAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(editPartnerInfoAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); export const { @@ -121,5 +183,9 @@ export const { savePageInfo, changeDelegateAccount, cleanupDelegateAccount, + changeEditPartner, + changeEditCompanyName, + changeSelectedAdminId, + cleanupPartnerAccount, } = partnerSlice.actions; export default partnerSlice.reducer; diff --git a/dictation_client/src/features/partner/selectors.ts b/dictation_client/src/features/partner/selectors.ts index 061f8b1..0bd2ac4 100644 --- a/dictation_client/src/features/partner/selectors.ts +++ b/dictation_client/src/features/partner/selectors.ts @@ -62,3 +62,17 @@ export const selectDelegatedAccountId = (state: RootState) => state.partner.apps.delegatedAccountId; export const selectDelegatedCompanyName = (state: RootState) => state.partner.apps.delegatedCompanyName; + +// edit +export const selectEditPartnerId = (state: RootState) => + state.partner.apps.editPartner.id; +export const selectEditPartnerCompanyName = (state: RootState) => + state.partner.apps.editPartner.companyName; +export const selectEditPartnerCountry = (state: RootState) => + state.partner.apps.editPartner.country; + +export const selectEditPartnerUsers = (state: RootState) => + state.partner.apps.editPartner.users; + +export const selectSelectedAdminId = (state: RootState) => + state.partner.apps.editPartner.selectedAdminId; diff --git a/dictation_client/src/features/partner/state.ts b/dictation_client/src/features/partner/state.ts index 18a88dd..0ef2c2c 100644 --- a/dictation_client/src/features/partner/state.ts +++ b/dictation_client/src/features/partner/state.ts @@ -1,6 +1,7 @@ import { CreatePartnerAccountRequest, GetPartnersResponse, + PartnerUser, } from "../../api/api"; export interface PartnerState { @@ -19,4 +20,11 @@ export interface Apps { isLoading: boolean; delegatedAccountId?: number; delegatedCompanyName?: string; + editPartner: { + users: PartnerUser[]; + id: number; + companyName: string; + country: string; + selectedAdminId: number; + }; } diff --git a/dictation_client/src/features/user/state.ts b/dictation_client/src/features/user/state.ts index 5d619e1..87e197d 100644 --- a/dictation_client/src/features/user/state.ts +++ b/dictation_client/src/features/user/state.ts @@ -1,9 +1,5 @@ import { CSVType } from "common/parser"; -import { - User, - AllocatableLicenseInfo, - MultipleImportUser, -} from "../../api/api"; +import { User, AllocatableLicenseInfo } from "../../api/api"; import { AddUser, UpdateUser, LicenseAllocateUser } from "./types"; export interface UsersState { diff --git a/dictation_client/src/features/user/types.ts b/dictation_client/src/features/user/types.ts index a96e236..2fa06e7 100644 --- a/dictation_client/src/features/user/types.ts +++ b/dictation_client/src/features/user/types.ts @@ -54,14 +54,14 @@ export interface LicenseAllocateUser { remaining?: number; } -export type RoleType = typeof USER_ROLES[keyof typeof USER_ROLES]; +export type RoleType = (typeof USER_ROLES)[keyof typeof USER_ROLES]; // 受け取った値がUSER_ROLESの型であるかどうかを判定する export const isRoleType = (role: string): role is RoleType => Object.values(USER_ROLES).includes(role as RoleType); export type LicenseStatusType = - typeof LICENSE_STATUS[keyof typeof LICENSE_STATUS]; + (typeof LICENSE_STATUS)[keyof typeof LICENSE_STATUS]; // 受け取った値がLicenseStatusTypeの型であるかどうかを判定する export const isLicenseStatusType = ( diff --git a/dictation_client/src/features/workflow/worktype/types.ts b/dictation_client/src/features/workflow/worktype/types.ts index 239cb26..c1c8348 100644 --- a/dictation_client/src/features/workflow/worktype/types.ts +++ b/dictation_client/src/features/workflow/worktype/types.ts @@ -2,7 +2,7 @@ import { OPTION_ITEMS_DEFAULT_VALUE_TYPE } from "./constants"; // OPTION_ITEMS_DEFAULT_VALUE_TYPEからOptionItemDefaultValueTypeを作成する export type OptionItemsDefaultValueType = - typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE]; + (typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE)[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE]; // 受け取った値がOptionItemDefaultValueType型かどうかを判定する export const isOptionItemDefaultValueType = ( diff --git a/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx b/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx index 48b36a2..2ea40a4 100644 --- a/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx +++ b/dictation_client/src/pages/PartnerPage/addPartnerAccountPopup.tsx @@ -110,6 +110,7 @@ export const AddPartnerAccountPopup: React.FC = ( country, adminName, email, + offset, ]); return ( diff --git a/dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx b/dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx new file mode 100644 index 0000000..5736ac5 --- /dev/null +++ b/dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx @@ -0,0 +1,178 @@ +import { AppDispatch } from "app/store"; +import React, { useCallback, useEffect } from "react"; +import styles from "styles/app.module.scss"; +import { useDispatch, useSelector } from "react-redux"; +import { getTranslationID } from "translation"; +import { useTranslation } from "react-i18next"; +import { + changeEditCompanyName, + changeSelectedAdminId, + cleanupPartnerAccount, + getPartnerUsersAsync, + editPartnerInfoAsync, + selectEditPartnerCompanyName, + selectEditPartnerCountry, + selectEditPartnerId, + selectEditPartnerUsers, + selectIsLoading, + selectSelectedAdminId, + selectOffset, + getPartnerInfoAsync, + LIMIT_PARTNER_VIEW_NUM, +} from "features/partner"; +import close from "../../assets/images/close.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; +import { COUNTRY_LIST } from "../SignupPage/constants"; + +interface EditPartnerAccountPopup { + isOpen: boolean; + onClose: () => void; +} + +export const EditPartnerAccountPopup: React.FC = ( + props +) => { + const { isOpen, onClose } = props; + const dispatch: AppDispatch = useDispatch(); + const { t } = useTranslation(); + const isLoading = useSelector(selectIsLoading); + const offset = useSelector(selectOffset); + + const partnerId = useSelector(selectEditPartnerId); + const companyName = useSelector(selectEditPartnerCompanyName); + const country = useSelector(selectEditPartnerCountry); + + const users = useSelector(selectEditPartnerUsers); + const adminUser = users.find((user) => user.isPrimaryAdmin); + + const selectedAdminId = useSelector(selectSelectedAdminId); + + // ポップアップを閉じる処理 + const closePopup = useCallback(() => { + if (isLoading) { + return; + } + dispatch(cleanupPartnerAccount()); + onClose(); + }, [isLoading, onClose, dispatch]); + + useEffect(() => { + if (isOpen) { + dispatch(getPartnerUsersAsync({ accountId: partnerId })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); + + const onEditPartner = useCallback(async () => { + // eslint-disable-next-line no-alert + if (!window.confirm(t(getTranslationID("common.message.dialogConfirm")))) { + return; + } + + const { meta } = await dispatch(editPartnerInfoAsync()); + if (meta.requestStatus === "fulfilled") { + dispatch( + getPartnerInfoAsync({ + limit: LIMIT_PARTNER_VIEW_NUM, + offset, + }) + ); + closePopup(); + } + }, [dispatch, closePopup, t, offset]); + + return ( +
+
+

+ {t(getTranslationID("partnerPage.label.editAccount"))} + +

+
+
+
+ {t(getTranslationID("partnerPage.label.accountInformation"))} +
+
{t(getTranslationID("partnerPage.label.name"))}
+
+ { + dispatch( + changeEditCompanyName({ companyName: e.target.value }) + ); + }} + /> +
+
{t(getTranslationID("partnerPage.label.country"))}
+
+ c.value === country)?.label} + className={styles.formInput} + readOnly + /> +
+
+ {t(getTranslationID("partnerPage.label.primaryAdminInfo"))} +
+
{t(getTranslationID("partnerPage.label.adminName"))}
+
+ +
+
{t(getTranslationID("partnerPage.label.email"))}
+
+ +
+
+ + Loading +
+
+
+
+
+ ); +}; diff --git a/dictation_client/src/pages/PartnerPage/index.tsx b/dictation_client/src/pages/PartnerPage/index.tsx index d1006e3..dac23a2 100644 --- a/dictation_client/src/pages/PartnerPage/index.tsx +++ b/dictation_client/src/pages/PartnerPage/index.tsx @@ -20,20 +20,24 @@ import { } from "features/partner/index"; import { changeDelegateAccount, + changeEditPartner, savePageInfo, } from "features/partner/partnerSlice"; import { getTranslationID } from "translation"; import { useTranslation } from "react-i18next"; import { getDelegationTokenAsync } from "features/auth/operations"; import { useNavigate } from "react-router-dom"; +import { Partner } from "api"; import personAdd from "../../assets/images/person_add.svg"; import { TIERS } from "../../components/auth/constants"; import { AddPartnerAccountPopup } from "./addPartnerAccountPopup"; +import { EditPartnerAccountPopup } from "./editPartnerAccountPopup"; import checkFill from "../../assets/images/check_fill.svg"; const PartnerPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); const [isPopupOpen, setIsPopupOpen] = useState(false); + const [isEditPopupOpen, setIsEditPopupOpen] = useState(false); const [t] = useTranslation(); const navigate = useNavigate(); const total = useSelector(selectTotal); @@ -72,6 +76,19 @@ const PartnerPage: React.FC = (): JSX.Element => { const onOpen = useCallback(() => { setIsPopupOpen(true); }, [setIsPopupOpen]); + const onOpenEditPopup = useCallback( + (editPartner: Partner) => { + dispatch( + changeEditPartner({ + id: editPartner.accountId, + companyName: editPartner.name, + country: editPartner.country, + }) + ); + setIsEditPopupOpen(true); + }, + [setIsEditPopupOpen, dispatch] + ); // パートナー取得APIを呼び出す useEffect(() => { @@ -144,6 +161,12 @@ const PartnerPage: React.FC = (): JSX.Element => { setIsPopupOpen(false); }} /> + { + setIsEditPopupOpen(false); + }} + />
@@ -211,6 +234,22 @@ const PartnerPage: React.FC = (): JSX.Element => {
    + {isVisibleButton && ( +
  • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + { + onOpenEditPopup(x); + }} + > + {t( + getTranslationID( + "partnerPage.label.editAccount" + ) + )} + +
  • + )} {isVisibleButton && (
  • {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index a3b0e67..e163161 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -532,14 +532,20 @@ "email": "Email", "dealerManagement": "Erlauben Sie dem Händler, Änderungen vorzunehmen", "partners": "Partner", - "deleteAccount": "Konto löschen" + "deleteAccount": "Konto löschen", + "editAccount": "(de)Edit Account", + "accountInformation": "(de)Account information", + "primaryAdminInfo": "(de)Primary administrator's information", + "adminName": "(de)Admin Name", + "saveChanges": "(de)Save Changes" }, "message": { "delegateNotAllowedError": "Aktionen im Namen des Partners sind nicht zulässig. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", "deleteFailedError": "Der Delegierungsvorgang ist fehlgeschlagen. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.", "delegateCancelError": "Der delegierte Vorgang wurde beendet, da die Berechtigung für den delegierten Vorgang widerrufen wurde.", "partnerDeleteConfirm": "(de)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(de)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" + "partnerDeleteFailedError": "(de)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", + "editFailedError": "(de)パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" } }, "accountPage": { @@ -638,4 +644,4 @@ "lowerLayerId": "(de)Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index a02319e..df0214b 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -532,14 +532,20 @@ "email": "Email", "dealerManagement": "Dealer Management", "partners": "Partners", - "deleteAccount": "Delete Account" + "deleteAccount": "Delete Account", + "editAccount": "Edit Account", + "accountInformation": "Account information", + "primaryAdminInfo": "Primary administrator's information", + "adminName": "Admin Name", + "saveChanges": "Save Changes" }, "message": { "delegateNotAllowedError": "Actions on behalf of partner are not allowed. Please refresh the screen and check again.", "deleteFailedError": "Delegate operation failed. Please refresh the screen and check again.", "delegateCancelError": "The delegated operation has been terminated because permission for the delegated operation has been revoked.", - "partnerDeleteConfirm": "(en)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(en)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" + "partnerDeleteConfirm": "選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", + "partnerDeleteFailedError": "削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", + "editFailedError": "パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" } }, "accountPage": { @@ -638,4 +644,4 @@ "lowerLayerId": "Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index fb4578b..e526a1d 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -532,14 +532,20 @@ "email": "Email", "dealerManagement": "Permitir que el distribuidor realice los cambios", "partners": "Socios", - "deleteAccount": "Borrar cuenta" + "deleteAccount": "Borrar cuenta", + "editAccount": "(es)Edit Account", + "accountInformation": "(es)Account information", + "primaryAdminInfo": "(es)Primary administrator's information", + "adminName": "(es)Admin Name", + "saveChanges": "(es)Save Changes" }, "message": { "delegateNotAllowedError": "No se permiten acciones en nombre del socio. Actualice la pantalla y verifique nuevamente.", "deleteFailedError": "La operación del delegado falló. Actualice la pantalla y verifique nuevamente.", "delegateCancelError": "La operación delegada finalizó porque se revocó el permiso para la operación delegada.", "partnerDeleteConfirm": "(es)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(es削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" + "partnerDeleteFailedError": "(es)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", + "editFailedError": "(es)パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" } }, "accountPage": { @@ -638,4 +644,4 @@ "lowerLayerId": "(es)Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 597d578..393e5d9 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -532,14 +532,20 @@ "email": "Email", "dealerManagement": "Autoriser le revendeur à modifier les paramètres", "partners": "Partenaires", - "deleteAccount": "Supprimer le compte" + "deleteAccount": "Supprimer le compte", + "editAccount": "(fr)Edit Account", + "accountInformation": "(fr)Account information", + "primaryAdminInfo": "(fr)Primary administrator's information", + "adminName": "(fr)Admin Name", + "saveChanges": "(fr)Save Changes" }, "message": { "delegateNotAllowedError": "Les actions au nom du partenaire ne sont pas autorisées. Veuillez actualiser l'écran et vérifier à nouveau.", "deleteFailedError": "L’opération de délégation a échoué. Veuillez actualiser l'écran et vérifier à nouveau.", "delegateCancelError": "L'opération déléguée a été interrompue car l'autorisation pour l'opération déléguée a été révoquée.", "partnerDeleteConfirm": "(fr)選択したアカウントを削除します。削除したアカウントは復元できませんが本当によろしいですか?対象アカウント:", - "partnerDeleteFailedError": "(fr)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。" + "partnerDeleteFailedError": "(fr)削除対象アカウントにLower layerアカウントが存在するため削除できません。Lower layerアカウントに対して削除、または現地法人以上のアカウントに階層構造の変更を依頼してください。", + "editFailedError": "(fr)パートナーアカウントの編集に失敗しました。画面を更新し、再度ご確認ください。" } }, "accountPage": { @@ -638,4 +644,4 @@ "lowerLayerId": "(fr)Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 3523dc9..9985180 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -2559,9 +2559,13 @@ export class AccountsController { const context = makeContext(userId, requestId); this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); - await this.accountService.getPartnerUsers(context, userId, targetAccountId); + const users = await this.accountService.getPartnerUsers( + context, + userId, + targetAccountId, + ); - return { users: [] }; + return { users }; } @Post('partner/update')