Merge branch 'develop' into main

This commit is contained in:
makabe 2023-09-11 15:52:17 +09:00
commit 6fd646f753
30 changed files with 1326 additions and 115 deletions

View File

@ -757,6 +757,25 @@ export interface GetPartnerLicensesResponse {
*/
'childrenPartnerLicenses': Array<PartnerLicenseInfo>;
}
/**
*
* @export
* @interface GetPartnersResponse
*/
export interface GetPartnersResponse {
/**
*
* @type {number}
* @memberof GetPartnersResponse
*/
'total': number;
/**
*
* @type {Array<Partner>}
* @memberof GetPartnersResponse
*/
'partners': Array<Partner>;
}
/**
*
* @export
@ -1028,6 +1047,55 @@ export interface OptionItemList {
*/
'optionItemList': Array<OptionItem>;
}
/**
*
* @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<number>;
}
/**
*
* @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<RequestArgs> => {
// 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<RequestArgs> => {
// 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<GetPartnersResponse>> {
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<object>> {
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<GetPartnerLicensesResponse> {
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<GetPartnersResponse> {
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<object> {
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<object> {
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));
}
}

View File

@ -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 });
}
});

View File

@ -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;

View File

@ -7,6 +7,9 @@ export interface WorktypeState {
export interface Apps {
isLoading: boolean;
isAddLoading: boolean;
isEditLoading: boolean;
selectedId: number;
worktypeId: string;
description?: string;
}

View File

@ -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;

View File

@ -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を実行することで、ポップアップのアンマウント時に初期化を行う

View File

@ -31,7 +31,7 @@ export const CardLicenseIssuePopup: React.FC<CardLicenseIssuePopupProps> = (
// ブラウザのウィンドウが閉じられようとしている場合に発火するイベントハンドラ
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
// isLoadingがtrueの場合は確認ダイアログを表示す
// 後続の処理で、ブラウザのCSVダウンロードを行うため、ダイアログを表示させ
if (isLoading) {
e.preventDefault();
// ChromeではreturnValueが必要

View File

@ -291,8 +291,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
? styles.isActive
: ""
}`}
onClick={(event) => {
event.preventDefault();
onClick={() => {
issueLicense(x.poNumber);
}}
>
@ -312,8 +311,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
? styles.isActive
: ""
}`}
onClick={(event) => {
event.preventDefault();
onClick={() => {
onCancelIssue(
selectedRow.accountId,
x.poNumber

View File

@ -51,24 +51,7 @@ export const AddPartnerAccountPopup: React.FC<AddPartnerAccountPopup> = (
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;

View File

@ -144,7 +144,7 @@ const SignupInput: React.FC = (): JSX.Element => {
{t(getTranslationID("signupPage.text.pageExplanation"))}
</p>
</div>
<section>
<section className={styles.form}>
<form>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={` ${styles.formTitle} ${styles.marginBtm0}`}>

View File

@ -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<AddWorktypeIdPopupProps> = (
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<AddWorktypeIdPopupProps> = (
// ×ボタンを押した時の処理
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<AddWorktypeIdPopupProps> = (
onClick={closePopup}
/>
</p>
<form action="" name="" method="" className={styles.form}>
<form className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>{t(getTranslationID("worktypeIdSetting.label.worktypeId"))}</dt>
@ -119,9 +126,12 @@ export const AddWorktypeIdPopup: React.FC<AddWorktypeIdPopupProps> = (
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,
})
);
}}
/>
</dd>
@ -132,9 +142,18 @@ export const AddWorktypeIdPopup: React.FC<AddWorktypeIdPopupProps> = (
value={t(
getTranslationID("worktypeIdSetting.label.addWorktype")
)}
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isAddLoading ? styles.isActive : ""
}`}
onClick={addWorktypeId}
/>
{isAddLoading && (
<img
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</dd>
</dl>
</form>

View File

@ -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<EditWorktypeIdPopupProps> = (
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<boolean>(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 (
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
<div className={styles.modalBox}>
<p className={styles.modalTitle}>
{t(getTranslationID("worktypeIdSetting.label.editWorktypeId"))}
{/* 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 className={styles.form}>
<dl className={`${styles.formList} ${styles.hasbg}`}>
<dt className={styles.formTitle} />
<dt>{t(getTranslationID("worktypeIdSetting.label.worktypeId"))}</dt>
<dd>
<input
type="text"
size={40}
maxLength={255}
value={worktypeId ?? ""}
className={styles.formInput}
onChange={(e) => {
dispatch(changeWorktypeId({ worktypeId: e.target.value }));
}}
/>
{isPushSaveButton && isEmptyWorktypeId && (
<span className={styles.formError}>
{t(getTranslationID("common.message.inputEmptyError"))}
</span>
)}
{isPushSaveButton && hasIncorrectPatternWorktypeId && (
<span className={styles.formError}>
{t(
getTranslationID(
"worktypeIdSetting.message.worktypeIdIncorrectError"
)
)}
</span>
)}
<span
style={{ whiteSpace: "pre-line" }}
className={styles.formComment}
>
{t(getTranslationID("worktypeIdSetting.label.worktypeIdTerms"))}
</span>
</dd>
<dt className={styles.overLine}>
{t(
getTranslationID("worktypeIdSetting.label.descriptionOptional")
)}
</dt>
<dd className={styles.last}>
<input
type="text"
size={40}
maxLength={255}
value={description ?? ""}
className={styles.formInput}
onChange={(e) => {
dispatch(
changeDescription({
description:
e.target.value === "" ? undefined : e.target.value,
})
);
}}
/>
</dd>
<dd className={`${styles.full} ${styles.alignCenter}`}>
<input
type="button"
name="Save Changes"
value={t(
getTranslationID("worktypeIdSetting.label.saveChange")
)}
className={`${styles.formSubmit} ${styles.marginBtm1} ${
!isEditLoading ? styles.isActive : ""
}`}
onClick={saveWorktypeId}
/>
{isEditLoading && (
<img
src={progress_activit}
className={styles.icLoading}
alt="Loading"
/>
)}
</dd>
</dl>
</form>
</div>
</div>
);
};

View File

@ -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<number>(NaN);
// 追加Popupの表示制御
const [isShowAddPopup, setIsShowAddPopup] = useState<boolean>(false);
// 編集Popupの表示制御
const [isShowEditPopup, setIsShowEditPopup] = useState<boolean>(false);
useEffect(() => {
dispatch(listWorktypesAsync());
}, [dispatch]);
@ -37,6 +43,12 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
}}
isOpen={isShowAddPopup}
/>
<EditWorktypeIdPopup
onClose={() => {
setIsShowEditPopup(false);
}}
isOpen={isShowEditPopup}
/>
<div className={styles.wrap}>
<Header userName="XXXXXXX" />
<UpdateTokenTimer />
@ -131,9 +143,23 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
className={`${styles.menuAction} ${styles.inTable}`}
>
<li>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<a
className={`${styles.menuLink} ${styles.isActive}`}
// onClick={}
onClick={() => {
dispatch(changeSelectedId({ id: worktype.id }));
dispatch(
changeWorktypeId({
worktypeId: worktype.worktypeId,
})
);
dispatch(
changeDescription({
description: worktype.description,
})
);
setIsShowEditPopup(true);
}}
>
{t(getTranslationID("common.label.edit"))}
</a>

View File

@ -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 {

View File

@ -391,7 +391,9 @@
"descriptionOptional": "(de)Description (Optional)",
"optionItem": "(de)Option Item",
"worktypeIdTerms": "(de)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .",
"addWorktype": "(de)Add Worktype"
"addWorktype": "(de)Add Worktype",
"editWorktypeId": "(de)Edit Worktype ID",
"saveChange": "(de)Save Changes"
},
"message": {
"worktypeIdIncorrectError": "(de)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください",

View File

@ -391,7 +391,9 @@
"descriptionOptional": "Description (Optional)",
"optionItem": "Option Item",
"worktypeIdTerms": "WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .",
"addWorktype": "Add Worktype"
"addWorktype": "Add Worktype",
"editWorktypeId": "Edit Worktype ID",
"saveChange": "Save Changes"
},
"message": {
"worktypeIdIncorrectError": "入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください",

View File

@ -391,7 +391,9 @@
"descriptionOptional": "(es)Description (Optional)",
"optionItem": "(es)Option Item",
"worktypeIdTerms": "(es)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .",
"addWorktype": "(es)Add Worktype"
"addWorktype": "(es)Add Worktype",
"editWorktypeId": "(es)Edit Worktype ID",
"saveChange": "(es)Save Changes"
},
"message": {
"worktypeIdIncorrectError": "(es)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください",

View File

@ -391,7 +391,9 @@
"descriptionOptional": "(fr)Description (Optional)",
"optionItem": "(fr)Option Item",
"worktypeIdTerms": "(fr)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .",
"addWorktype": "(fr)Add Worktype"
"addWorktype": "(fr)Add Worktype",
"editWorktypeId": "(fr)Edit Worktype ID",
"saveChange": "(fr)Save Changes"
},
"message": {
"worktypeIdIncorrectError": "(fr)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください",

View File

@ -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": {

View File

@ -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 = <TService>(
username: string,
) => Promise<{ sub: string } | ConflictError>;
deleteUser?: (externalId: string, context: Context) => Promise<void>;
getUsers?: (
context: Context,
externalIds: string[],
) => Promise<AdB2cUser[]>;
},
): void => {
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
@ -44,6 +49,12 @@ export const overrideAdB2cService = <TService>(
writable: true,
});
}
if (overrides.getUsers) {
Object.defineProperty(obj, obj.getUsers.name, {
value: overrides.getUsers,
writable: true,
});
}
};
/**

View File

@ -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,
},
);

View File

@ -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;

View File

@ -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<GetOptionItemsResponse> {
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<UpdateOptionItemsResponse> {
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;
}
}

View File

@ -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>(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>(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);
});
});

View File

@ -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<GetPartnersResponse> {
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}`);
}
}
}

View File

@ -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;
};

View File

@ -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',
},

View File

@ -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;

View File

@ -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,
},

View File

@ -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,
};
});
}
}