Merged PR 821: 画面実装(パートナーライセンス一覧画面&階層構造変更ポップアップ)

## 概要
[Task3854: 画面実装(パートナーライセンス一覧画面&階層構造変更ポップアップ)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3854)

- パートナーライセンス一覧に「Change Owner」ボタンを配置し、表示制御およびクリック時にポップアップ表示する処理の実装
- アカウント階層構造変更ポップアップの処理全体実装
- サーバー側のエラーコード定義

## レビューポイント
- 「一括」を表現するためのドロップダウンの構築や処理周りで改善点ないか(to:斎藤くん)
- コンポーネントでの状態管理でお作法に違反しているところないか(to:斎藤くん)
- 修正箇所がほかの機能に影響していないか
    - パートナーライセンス一覧の画面表示に何らか悪影響ないか?(to:ガンさん)

## UIの変更
- 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/Task3854?csf=1&web=1&e=jBGQrR

## クエリの変更
- なし

## 動作確認状況
- ローカルで確認しました
    - 第一階層でログインしてかつ第三または第四視点での一覧を確認しているときにChangeOwnerボタンが表示される
    - ボタン押下すると、仕様通りにポップアップの表示が行われる
    - ポップアップにて入力項目に入力できる&バリデーション効いている
    - ポップアップにて実行ボタン押下するとAPI実行できる&処理結果に応じて仕様通りの挙動をすること
- 行った修正がデグレを発生させていないことを確認できるか
  - 具体的にどのような確認をしたか
    - パートナーライセンス画面に新規ボタンを配置した&新規ポップアップの実装のみのため、
ポップアップでの処理が正常終了/失敗/何もせず閉じた場合に元の画面の表示が今まで通り動くことを確認済み。
This commit is contained in:
Kentaro Fukunaga 2024-03-13 07:41:25 +00:00
parent 9f5ccabb0c
commit 83e297cc9b
16 changed files with 594 additions and 5 deletions

View File

@ -1840,6 +1840,25 @@ export interface SignupRequest {
*/ */
'prompt'?: boolean; 'prompt'?: boolean;
} }
/**
*
* @export
* @interface SwitchParentRequest
*/
export interface SwitchParentRequest {
/**
* ID
* @type {number}
* @memberof SwitchParentRequest
*/
'to': number;
/**
* IDのリスト
* @type {Array<number>}
* @memberof SwitchParentRequest
*/
'children': Array<number>;
}
/** /**
* *
* @export * @export
@ -3474,6 +3493,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @summary
* @param {SwitchParentRequest} switchParentRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
switchParent: async (switchParentRequest: SwitchParentRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'switchParentRequest' is not null or undefined
assertParamExists('switchParent', 'switchParentRequest', switchParentRequest)
const localVarPath = `/accounts/parent/switch`;
// 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(switchParentRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @summary * @summary
@ -4043,6 +4102,19 @@ export const AccountsApiFp = function(configuration?: Configuration) {
const operationBasePath = operationServerMap['AccountsApi.issueLicense']?.[index]?.url; const operationBasePath = operationServerMap['AccountsApi.issueLicense']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
}, },
/**
*
* @summary
* @param {SwitchParentRequest} switchParentRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async switchParent(switchParentRequest: SwitchParentRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.switchParent(switchParentRequest, options);
const index = configuration?.serverIndex ?? 0;
const operationBasePath = operationServerMap['AccountsApi.switchParent']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
/** /**
* *
* @summary * @summary
@ -4369,6 +4441,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: any): AxiosPromise<object> { issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: any): AxiosPromise<object> {
return localVarFp.issueLicense(issueLicenseRequest, options).then((request) => request(axios, basePath)); return localVarFp.issueLicense(issueLicenseRequest, options).then((request) => request(axios, basePath));
}, },
/**
*
* @summary
* @param {SwitchParentRequest} switchParentRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
switchParent(switchParentRequest: SwitchParentRequest, options?: any): AxiosPromise<object> {
return localVarFp.switchParent(switchParentRequest, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @summary * @summary
@ -4725,6 +4807,18 @@ export class AccountsApi extends BaseAPI {
return AccountsApiFp(this.configuration).issueLicense(issueLicenseRequest, options).then((request) => request(this.axios, this.basePath)); return AccountsApiFp(this.configuration).issueLicense(issueLicenseRequest, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @summary
* @param {SwitchParentRequest} switchParentRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AccountsApi
*/
public switchParent(switchParentRequest: SwitchParentRequest, options?: AxiosRequestConfig) {
return AccountsApiFp(this.configuration).switchParent(switchParentRequest, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @summary * @summary

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
<style type="text/css">
.st0{fill:#282828;}
</style>
<path class="st0" d="M24.1,38l5.7-5.7l-5.7-5.6L22,28.8l2.1,2.1c-0.9,0-1.8-0.1-2.7-0.4c-0.9-0.3-1.7-0.9-2.4-1.6
c-0.7-0.7-1.2-1.4-1.5-2.3C17.2,25.8,17,24.9,17,24c0-0.6,0.1-1.1,0.2-1.7s0.4-1.1,0.6-1.7l-2.2-2.2c-0.6,0.8-1,1.7-1.2,2.6
C14.1,22.1,14,23,14,24c0,1.3,0.2,2.5,0.8,3.8C15.2,29,16,30.1,17,31s2,1.7,3.2,2.2c1.2,0.5,2.4,0.7,3.7,0.8L22,35.9L24.1,38z
M32.4,29.5c0.6-0.8,1-1.7,1.2-2.6C33.9,25.9,34,25,34,24c0-1.3-0.2-2.5-0.7-3.8s-1.2-2.4-2.2-3.3s-2.1-1.7-3.3-2.2
c-1.2-0.5-2.5-0.7-3.7-0.7l1.9-1.9L23.9,10l-5.7,5.7l5.7,5.6l2.1-2.1L23.8,17c0.9,0,1.8,0.2,2.8,0.5s1.7,0.9,2.4,1.5
s1.2,1.4,1.5,2.3c0.4,0.9,0.5,1.7,0.5,2.6c0,0.6-0.1,1.1-0.2,1.7c-0.1,0.6-0.4,1.1-0.6,1.6L32.4,29.5z M24,44
c-2.7,0-5.3-0.5-7.8-1.6s-4.6-2.5-6.4-4.3s-3.2-3.9-4.3-6.4S4,26.7,4,24c0-2.8,0.5-5.4,1.6-7.8s2.5-4.5,4.3-6.3s3.9-3.2,6.4-4.3
S21.3,4,24,4c2.8,0,5.4,0.5,7.8,1.6s4.6,2.5,6.4,4.3s3.2,3.9,4.3,6.3c1.1,2.4,1.6,5,1.6,7.8c0,2.7-0.5,5.3-1.6,7.8
c-1,2.4-2.5,4.6-4.3,6.4s-3.9,3.2-6.4,4.3S26.8,44,24,44z M24,41c4.7,0,8.8-1.7,12-5c3.3-3.3,5-7.3,5-12c0-4.7-1.6-8.8-5-12.1
c-3.3-3.3-7.3-5-12-5c-4.7,0-8.7,1.7-12,5S7,19.3,7,24c0,4.7,1.7,8.7,5,12C15.3,39.3,19.3,41,24,41z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="_レイヤー_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 37.17"><defs><style>.cls-1{fill:#282828;}.cls-1,.cls-2{stroke-width:0px;}.cls-2{fill:#e6e6e6;}</style></defs><path class="cls-2" d="M42.13,35.07l-2.15,2.1L3,5.1v6.1H0V0h11.15v3h-6l36.98,32.07Z"/><path class="cls-1" d="M39.98,37.17l-2.1-2.15L74.9,3h-6.1V0h11.2v11.15h-3v-6l-37.03,32.02Z"/></svg>

After

Width:  |  Height:  |  Size: 409 B

View File

@ -77,4 +77,8 @@ export const errorCodes = [
"E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) "E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった)
"E016002", // テンプレートファイル削除エラー削除しようとしたテンプレートファイルがWorkflowに指定されていた "E016002", // テンプレートファイル削除エラー削除しようとしたテンプレートファイルがWorkflowに指定されていた
"E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) "E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた)
"E017001", // 親アカウント変更不可エラー(指定したアカウントが存在しない)
"E017002", // 親アカウント変更不可エラー(階層関係が不正)
"E017003", // 親アカウント変更不可エラー(リージョンが同一でない)
"E017004", // 親アカウント変更不可エラー(国が同一でない)
] as const; ] as const;

View File

@ -105,3 +105,88 @@ export const getPartnerLicenseAsync = createAsyncThunk<
return thunkApi.rejectWithValue({ error }); return thunkApi.rejectWithValue({ error });
} }
}); });
export const switchParentAsync = createAsyncThunk<
{
/* Empty Object */
},
{
// パラメータ
to: number;
children: number[];
},
{
// rejectした時の返却値の型
rejectValue: {
error: ErrorObject;
};
}
>("accounts/switchParentAsync", 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 accountsApi = new AccountsApi(config);
const { to, children } = args;
try {
await accountsApi.switchParent(
{
to,
children,
},
{
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");
// TODO:エラー処理
if (error.code === "E017001") {
errorMessage = getTranslationID(
"changeOwnerPopup.message.accountNotFoundError"
);
}
if (error.code === "E017002") {
errorMessage = getTranslationID(
"changeOwnerPopup.message.hierarchyMismatchError"
);
}
if (error.code === "E017003") {
errorMessage = getTranslationID(
"changeOwnerPopup.message.regionMismatchError"
);
}
if (error.code === "E017004") {
errorMessage = getTranslationID(
"changeOwnerPopup.message.countryMismatchError"
);
}
thunkApi.dispatch(
openSnackbar({
level: "error",
message: errorMessage,
})
);
return thunkApi.rejectWithValue({ error });
}
});

View File

@ -1,7 +1,11 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { PartnerLicenseInfo } from "api"; import { PartnerLicenseInfo } from "api";
import { PartnerLicensesState, HierarchicalElement } from "./state"; import { PartnerLicensesState, HierarchicalElement } from "./state";
import { getMyAccountAsync, getPartnerLicenseAsync } from "./operations"; import {
getMyAccountAsync,
getPartnerLicenseAsync,
switchParentAsync,
} from "./operations";
import { ACCOUNTS_VIEW_LIMIT } from "./constants"; import { ACCOUNTS_VIEW_LIMIT } from "./constants";
const initialState: PartnerLicensesState = { const initialState: PartnerLicensesState = {
@ -109,6 +113,15 @@ export const partnerLicenseSlice = createSlice({
builder.addCase(getPartnerLicenseAsync.rejected, (state) => { builder.addCase(getPartnerLicenseAsync.rejected, (state) => {
state.apps.isLoading = false; state.apps.isLoading = false;
}); });
builder.addCase(switchParentAsync.pending, (state) => {
state.apps.isLoading = true;
});
builder.addCase(switchParentAsync.fulfilled, (state) => {
state.apps.isLoading = false;
});
builder.addCase(switchParentAsync.rejected, (state) => {
state.apps.isLoading = false;
});
}, },
}); });
export const { export const {

View File

@ -0,0 +1,203 @@
import React, { useState, useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
selectChildrenPartnerLicenses,
selectIsLoading,
selectOwnPartnerLicense,
} from "features/license/partnerLicense/selectors";
import {
getMyAccountAsync,
switchParentAsync,
} from "features/license/partnerLicense/operations";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import { AppDispatch } from "app/store";
import { clearHierarchicalElement } from "features/license/partnerLicense";
import styles from "../../styles/app.module.scss";
import close from "../../assets/images/close.svg";
import shuffle from "../../assets/images/shuffle.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
interface ChangeOwnerPopupProps {
onClose: () => void;
}
const ChangeOwnerPopup: React.FC<ChangeOwnerPopupProps> = (props) => {
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
const [selectedChildId, setSelectedChildId] = useState<number | null>(null);
const [selectedChildName, setSelectedChildName] = useState<string>("");
const [destinationParentId, setDestinationParentId] = useState<string>("");
const [error, setError] = useState<string>("");
const originParentLicenseInfo = useSelector(selectOwnPartnerLicense);
const childrenLicenseInfos = useSelector(selectChildrenPartnerLicenses);
const isLoading = useSelector(selectIsLoading);
const { onClose } = props;
const closePopup = useCallback(() => {
if (isLoading) return;
onClose();
}, [isLoading, onClose]);
const bulkDisplayName = "-- Bulk --";
const bulkValue = "bulk";
const onBulkChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const { value } = e.target;
const childId = value === bulkValue ? null : Number(value);
setSelectedChildId(childId);
// 一括追加のときは子アカウント名を表示しない
let childName = "";
if (childId) {
const child = childrenLicenseInfos.find((c) => c.accountId === childId);
// childがundefinedになることはないが、コード解析対応のためのチェック
if (child) {
childName = child.companyName;
}
}
setSelectedChildName(childName);
},
[childrenLicenseInfos]
);
const onSaveClick = useCallback(async () => {
const destinationParentIdNum = Number(destinationParentId);
if (
Number.isNaN(destinationParentIdNum) || // 数値でない場合
destinationParentIdNum <= 0 || // IDにならない数値の場合
destinationParentId.length > 7 // 8桁以上の場合本システムの特徴として8桁以上になることはあり得ない
) {
setError(t(getTranslationID("changeOwnerPopup.label.invalidInputError")));
return;
}
setError("");
if (
// eslint-disable-next-line no-alert
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
) {
return;
}
const children = selectedChildId
? [selectedChildId]
: childrenLicenseInfos.map((child) => child.accountId);
const { meta } = await dispatch(
switchParentAsync({ to: Number(destinationParentId), children })
);
if (meta.requestStatus === "fulfilled") {
dispatch(getMyAccountAsync());
dispatch(clearHierarchicalElement());
closePopup();
}
}, [
childrenLicenseInfos,
closePopup,
destinationParentId,
dispatch,
selectedChildId,
t,
]);
return (
<div className={`${styles.modal} ${styles.isShow}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("changeOwnerPopup.label.title"))}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions */}
<img
src={close}
className={styles.modalTitleIcon}
alt="close"
onClick={closePopup}
/>
</p>
<form action="" name="" method="" className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>
{t(getTranslationID("changeOwnerPopup.label.upperLayerId"))}
</dt>
<dd className={styles.ownerChange}>
<p className={styles.Owner}>
<input
type="text"
size={40}
name=""
value={originParentLicenseInfo.accountId}
readOnly
className={`${styles.formInput} ${styles.short}`}
/>
<span className={styles.txName}>
{originParentLicenseInfo.companyName}
</span>
</p>
<p className={styles.arrowR} />
<p className={styles.newOwner}>
<input
type="text"
size={40}
name=""
value={destinationParentId}
placeholder=" "
className={`${styles.formInput} ${styles.short}`}
onChange={(e) => setDestinationParentId(e.target.value)}
/>
<span className={styles.formError}>{error}</span>
</p>
</dd>
<dd className={styles.full}>
<img src={shuffle} className={styles.transOwner} alt="" />
</dd>
<dt>
{t(getTranslationID("changeOwnerPopup.label.lowerLayerId"))}
</dt>
<dd className={styles.lowerTrans}>
<select
name=""
className={`${styles.formInput} ${styles.short}`}
value={selectedChildId ?? bulkDisplayName}
onChange={onBulkChange}
>
<option value={bulkValue}>{bulkDisplayName}</option>
{childrenLicenseInfos.map((child) => (
<option key={child.accountId} value={child.accountId}>
{child.accountId}
</option>
))}
</select>
<span className={styles.txName}>{selectedChildName}</span>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
{/* 処理中や子アカウントが1件も存在しない場合、Saveボタンを押せないようにする */}
<input
type="button"
name="submit"
value={t(getTranslationID("common.label.save"))}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isLoading && childrenLicenseInfos.length > 0
? styles.isActive
: ""
}`}
onClick={onSaveClick}
disabled={isLoading || childrenLicenseInfos.length <= 0}
/>
</dd>
<img
style={{ display: isLoading ? "inline" : "none" }}
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
</dl>
</form>
</div>
</div>
);
};
export default ChangeOwnerPopup;

View File

@ -12,6 +12,7 @@ import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup";
import postAdd from "../../assets/images/post_add.svg"; import postAdd from "../../assets/images/post_add.svg";
import history from "../../assets/images/history.svg"; import history from "../../assets/images/history.svg";
import returnLabel from "../../assets/images/undo.svg"; import returnLabel from "../../assets/images/undo.svg";
import changeOwnerIcon from "../../assets/images/change_circle.svg";
import { isApproveTier } from "../../features/auth/utils"; import { isApproveTier } from "../../features/auth/utils";
import { TIERS } from "../../components/auth/constants"; import { TIERS } from "../../components/auth/constants";
import { import {
@ -37,6 +38,7 @@ import { LicenseOrderPopup } from "./licenseOrderPopup";
import { LicenseOrderHistory } from "./licenseOrderHistory"; import { LicenseOrderHistory } from "./licenseOrderHistory";
import { LicenseSummary } from "./licenseSummary"; import { LicenseSummary } from "./licenseSummary";
import progress_activit from "../../assets/images/progress_activit.svg"; import progress_activit from "../../assets/images/progress_activit.svg";
import ChangeOwnerPopup from "./changeOwnerPopup";
const PartnerLicense: React.FC = (): JSX.Element => { const PartnerLicense: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch(); const dispatch: AppDispatch = useDispatch();
@ -49,6 +51,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] = const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] =
useState(false); useState(false);
const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false); const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false);
const [isChangeOwnerPopupOpen, setIsChangeOwnerPopupOpen] = useState(false);
// 階層表示用 // 階層表示用
const tierNames: { [key: number]: string } = { const tierNames: { [key: number]: string } = {
@ -148,6 +151,11 @@ const PartnerLicense: React.FC = (): JSX.Element => {
[dispatch, setIslicenseOrderHistoryOpen] [dispatch, setIslicenseOrderHistoryOpen]
); );
// changeOwnerボタン押下時
const onClickChangeOwner = useCallback(() => {
setIsChangeOwnerPopupOpen(true);
}, [setIsChangeOwnerPopupOpen]);
// マウント時のみ実行 // マウント時のみ実行
useEffect(() => { useEffect(() => {
dispatch(getMyAccountAsync()); dispatch(getMyAccountAsync());
@ -245,6 +253,13 @@ const PartnerLicense: React.FC = (): JSX.Element => {
}} }}
/> />
)} )}
{isChangeOwnerPopupOpen && (
<ChangeOwnerPopup
onClose={() => {
setIsChangeOwnerPopupOpen(false);
}}
/>
)}
{!islicenseOrderHistoryOpen && !isViewDetailsOpen && ( {!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
<div className={styles.wrap}> <div className={styles.wrap}>
<Header /> <Header />
@ -329,6 +344,26 @@ const PartnerLicense: React.FC = (): JSX.Element => {
</a> </a>
)} )}
</li> </li>
<li>
{isVisibleChangeOwner(ownPartnerLicenseInfo.tier) && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<a
className={`${styles.menuLink} ${styles.isActive}`}
onClick={onClickChangeOwner}
>
<img
src={changeOwnerIcon}
alt=""
className={styles.menuIcon}
/>
{t(
getTranslationID(
"partnerLicense.label.changeOwnerButton"
)
)}
</a>
)}
</li>
</ul> </ul>
<ul className={styles.brCrumbLicense}> <ul className={styles.brCrumbLicense}>
{hierarchicalElements.map((value) => ( {hierarchicalElements.map((value) => (
@ -545,4 +580,10 @@ const PartnerLicense: React.FC = (): JSX.Element => {
); );
}; };
const isVisibleChangeOwner = (partnerTier: number) =>
// 自身が第一階層または第二階層で、表示しているパートナーが第三階層または第四階層の場合のみ表示
isApproveTier([TIERS.TIER1, TIERS.TIER2]) &&
(partnerTier.toString() === TIERS.TIER3 ||
partnerTier.toString() === TIERS.TIER4);
export default PartnerLicense; export default PartnerLicense;

View File

@ -1999,6 +1999,61 @@ tr.isSelected .menuInTable li a.isDisable {
text-align: right; text-align: right;
} }
.formList dd.ownerChange {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 0;
}
.formList dd.ownerChange p.Owner,
.formList dd.ownerChange p.newOwner {
width: 150px;
}
.formList dd.ownerChange .arrowR {
width: 8%;
height: 20px;
margin-top: 10px;
margin-right: 2%;
background: #e6e6e6;
position: relative;
}
.formList dd.ownerChange .arrowR::after {
content: "";
border-top: 20px transparent solid;
border-bottom: 20px transparent solid;
border-left: 20px #e6e6e6 solid;
position: absolute;
top: 50%;
right: -15px;
transform: translateY(-50%);
}
.formList dd.ownerChange + .full {
width: 66%;
margin-left: 30%;
margin-bottom: -10px;
text-align: center;
}
.formList dd.ownerChange + .full .transOwner {
width: 100px;
}
.formList dd.lowerTrans {
margin-bottom: 1.5rem;
position: relative;
text-align: center;
}
.formList dd.lowerTrans select,
.formList dd.lowerTrans span {
margin: 0 auto;
}
.formList dd .txName {
display: block;
width: 150px;
padding: 0.2rem 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dictation .menuAction { .dictation .menuAction {
margin-top: -1rem; margin-top: -1rem;
height: 34px; height: 34px;

View File

@ -129,6 +129,13 @@ declare const classNames: {
readonly cardHistory: "cardHistory"; readonly cardHistory: "cardHistory";
readonly partner: "partner"; readonly partner: "partner";
readonly isOpen: "isOpen"; readonly isOpen: "isOpen";
readonly ownerChange: "ownerChange";
readonly Owner: "Owner";
readonly newOwner: "newOwner";
readonly arrowR: "arrowR";
readonly transOwner: "transOwner";
readonly lowerTrans: "lowerTrans";
readonly txName: "txName";
readonly alignLeft: "alignLeft"; readonly alignLeft: "alignLeft";
readonly displayOptions: "displayOptions"; readonly displayOptions: "displayOptions";
readonly tableFilter: "tableFilter"; readonly tableFilter: "tableFilter";

View File

@ -368,7 +368,8 @@
"shortage": "Lizenzmangel", "shortage": "Lizenzmangel",
"issueRequesting": "Lizenzen auf Bestellung", "issueRequesting": "Lizenzen auf Bestellung",
"viewDetails": "Details anzeigen", "viewDetails": "Details anzeigen",
"accounts": "konten" "accounts": "konten",
"changeOwnerButton": "(de)Change Owner"
} }
}, },
"orderHistoriesPage": { "orderHistoriesPage": {
@ -619,5 +620,19 @@
"saveButton": "(de)Save Settings", "saveButton": "(de)Save Settings",
"daysValidationError": "(de)Daysには1999の数字を入力してください。" "daysValidationError": "(de)Daysには1999の数字を入力してください。"
} }
},
"changeOwnerPopup": {
"message": {
"accountNotFoundError": "(de)変更先のアカウントIDは存在しません。",
"hierarchyMismatchError": "(de)パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。",
"regionMismatchError": "(de)パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。",
"countryMismatchError": "(de)パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。"
},
"label": {
"invalidInputError": "(de)変更先アカウントIDには19999999の数字を入力してください。",
"title": "(de)Change Owner",
"upperLayerId": "(de)Upper Layer ID",
"lowerLayerId": "(de)Lower Layer ID"
}
} }
} }

View File

@ -368,7 +368,8 @@
"shortage": "License Shortage", "shortage": "License Shortage",
"issueRequesting": "Licenses on Order", "issueRequesting": "Licenses on Order",
"viewDetails": "View Details", "viewDetails": "View Details",
"accounts": "accounts" "accounts": "accounts",
"changeOwnerButton": "Change Owner"
} }
}, },
"orderHistoriesPage": { "orderHistoriesPage": {
@ -619,5 +620,19 @@
"saveButton": "Save Settings", "saveButton": "Save Settings",
"daysValidationError": "Daysには1999の数字を入力してください。" "daysValidationError": "Daysには1999の数字を入力してください。"
} }
},
"changeOwnerPopup": {
"message": {
"accountNotFoundError": "変更先のアカウントIDは存在しません。",
"hierarchyMismatchError": "パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。",
"regionMismatchError": "パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。",
"countryMismatchError": "パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。"
},
"label": {
"invalidInputError": "変更先アカウントIDには19999999の数字を入力してください。",
"title": "Change Owner",
"upperLayerId": "Upper Layer ID",
"lowerLayerId": "Lower Layer ID"
}
} }
} }

View File

@ -368,7 +368,8 @@
"shortage": "Escasez de licencias", "shortage": "Escasez de licencias",
"issueRequesting": "Licencias en Pedido", "issueRequesting": "Licencias en Pedido",
"viewDetails": "Ver detalles", "viewDetails": "Ver detalles",
"accounts": "cuentas" "accounts": "cuentas",
"changeOwnerButton": "(es)Change Owner"
} }
}, },
"orderHistoriesPage": { "orderHistoriesPage": {
@ -619,5 +620,19 @@
"saveButton": "(es)Save Settings", "saveButton": "(es)Save Settings",
"daysValidationError": "(es)Daysには1999の数字を入力してください。" "daysValidationError": "(es)Daysには1999の数字を入力してください。"
} }
},
"changeOwnerPopup": {
"message": {
"accountNotFoundError": "(es)変更先のアカウントIDは存在しません。",
"hierarchyMismatchError": "(es)パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。",
"regionMismatchError": "(es)パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。",
"countryMismatchError": "(es)パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。"
},
"label": {
"invalidInputError": "(es)変更先アカウントIDには19999999の数字を入力してください。",
"title": "(es)Change Owner",
"upperLayerId": "(es)Upper Layer ID",
"lowerLayerId": "(es)Lower Layer ID"
}
} }
} }

View File

@ -368,7 +368,8 @@
"shortage": "Pénurie de licences", "shortage": "Pénurie de licences",
"issueRequesting": "Licences en commande", "issueRequesting": "Licences en commande",
"viewDetails": "Voir les détails", "viewDetails": "Voir les détails",
"accounts": "comptes" "accounts": "comptes",
"changeOwnerButton": "(fr)Change Owner"
} }
}, },
"orderHistoriesPage": { "orderHistoriesPage": {
@ -619,5 +620,19 @@
"saveButton": "(fr)Save Settings", "saveButton": "(fr)Save Settings",
"daysValidationError": "(fr)Daysには1999の数字を入力してください。" "daysValidationError": "(fr)Daysには1999の数字を入力してください。"
} }
},
"changeOwnerPopup": {
"message": {
"accountNotFoundError": "(fr)変更先のアカウントIDは存在しません。",
"hierarchyMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\n子アカウントの1階層上のアカウントを切り替え先に指定してください。",
"regionMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\n子アカウントと同じリージョンのアカウントを切り替え先に指定してください。",
"countryMismatchError": "(fr)パートナーアカウントの変更に失敗しました。\n子アカウントと同じ国のアカウントを切り替え先に指定してください。"
},
"label": {
"invalidInputError": "(fr)変更先アカウントIDには19999999の数字を入力してください。",
"title": "(fr)Change Owner",
"upperLayerId": "(fr)Upper Layer ID",
"lowerLayerId": "(fr)Lower Layer ID"
}
} }
} }

View File

@ -82,4 +82,8 @@ export const ErrorCodes = [
'E016001', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) 'E016001', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった)
'E016002', // テンプレートファイル削除エラー削除しようとしたテンプレートファイルがWorkflowに指定されていた 'E016002', // テンプレートファイル削除エラー削除しようとしたテンプレートファイルがWorkflowに指定されていた
'E016003', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) 'E016003', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた)
'E017001', // 親アカウント変更不可エラー(指定したアカウントが存在しない)
'E017002', // 親アカウント変更不可エラー(階層関係が不正)
'E017003', // 親アカウント変更不可エラー(リージョンが同一でない)
'E017004', // 親アカウント変更不可エラー(国が同一でない)
] as const; ] as const;

View File

@ -72,4 +72,8 @@ export const errors: Errors = {
E016002: 'Template file delete failed Error: workflow assigned', E016002: 'Template file delete failed Error: workflow assigned',
E016003: E016003:
'Template file delete failed Error: not finished task has template file', 'Template file delete failed Error: not finished task has template file',
E017001: 'Parent account switch failed Error: account not found',
E017002: 'Parent account switch failed Error: hierarchy mismatch',
E017003: 'Parent account switch failed Error: region mismatch',
E017004: 'Parent account switch failed Error: country mismatch',
}; };