Merge branch 'develop'
@ -21,6 +21,7 @@ import TypistGroupSettingPage from "pages/TypistGroupSettingPage";
|
||||
import WorktypeIdSettingPage from "pages/WorkTypeIdSettingPage";
|
||||
import AccountPage from "pages/AccountPage";
|
||||
import { TemplateFilePage } from "pages/TemplateFilePage";
|
||||
import { AccountDeleteSuccess } from "pages/AccountPage/accountDeleteSuccess";
|
||||
|
||||
const AppRouter: React.FC = () => (
|
||||
<Routes>
|
||||
@ -81,6 +82,8 @@ const AppRouter: React.FC = () => (
|
||||
path="/partners"
|
||||
element={<RouteAuthGuard component={<PartnerPage />} />}
|
||||
/>
|
||||
<Route path="/accountDeleteSuccess" element={<AccountDeleteSuccess />} />
|
||||
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@ -335,6 +335,25 @@ export interface AudioUploadLocationResponse {
|
||||
*/
|
||||
'url': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface Author
|
||||
*/
|
||||
export interface Author {
|
||||
/**
|
||||
* Authorユーザーの内部ID
|
||||
* @type {number}
|
||||
* @memberof Author
|
||||
*/
|
||||
'id': number;
|
||||
/**
|
||||
* AuthorID
|
||||
* @type {string}
|
||||
* @memberof Author
|
||||
*/
|
||||
'authorId': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -504,6 +523,37 @@ export interface CreateTypistGroupRequest {
|
||||
*/
|
||||
'typistIds': Array<number>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface CreateWorkflowsRequest
|
||||
*/
|
||||
export interface CreateWorkflowsRequest {
|
||||
/**
|
||||
* Authornの内部ID
|
||||
* @type {number}
|
||||
* @memberof CreateWorkflowsRequest
|
||||
*/
|
||||
'authorId': number;
|
||||
/**
|
||||
* Worktypeの内部ID
|
||||
* @type {number}
|
||||
* @memberof CreateWorkflowsRequest
|
||||
*/
|
||||
'worktypeId'?: number;
|
||||
/**
|
||||
* テンプレートの内部ID
|
||||
* @type {number}
|
||||
* @memberof CreateWorkflowsRequest
|
||||
*/
|
||||
'templateId'?: number;
|
||||
/**
|
||||
* ルーティング候補のタイピストユーザー/タイピストグループ
|
||||
* @type {Array<WorkflowTypist>}
|
||||
* @memberof CreateWorkflowsRequest
|
||||
*/
|
||||
'typists': Array<WorkflowTypist>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -606,6 +656,19 @@ export interface GetAllocatableLicensesResponse {
|
||||
*/
|
||||
'allocatableLicenses': Array<AllocatableLicenseInfo>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface GetAuthorsResponse
|
||||
*/
|
||||
export interface GetAuthorsResponse {
|
||||
/**
|
||||
*
|
||||
* @type {Array<Author>}
|
||||
* @memberof GetAuthorsResponse
|
||||
*/
|
||||
'authors': Array<Author>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -989,6 +1052,19 @@ export interface GetUsersResponse {
|
||||
*/
|
||||
'users': Array<User>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface GetWorkflowsResponse
|
||||
*/
|
||||
export interface GetWorkflowsResponse {
|
||||
/**
|
||||
* ワークフローの一覧
|
||||
* @type {Array<Workflow>}
|
||||
* @memberof GetWorkflowsResponse
|
||||
*/
|
||||
'workflows': Array<Workflow>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -1963,6 +2039,100 @@ export interface User {
|
||||
*/
|
||||
'licenseStatus': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface Workflow
|
||||
*/
|
||||
export interface Workflow {
|
||||
/**
|
||||
* ワークフローの内部ID
|
||||
* @type {number}
|
||||
* @memberof Workflow
|
||||
*/
|
||||
'id': number;
|
||||
/**
|
||||
*
|
||||
* @type {Author}
|
||||
* @memberof Workflow
|
||||
*/
|
||||
'author': Author;
|
||||
/**
|
||||
*
|
||||
* @type {WorkflowWorktype}
|
||||
* @memberof Workflow
|
||||
*/
|
||||
'worktype'?: WorkflowWorktype;
|
||||
/**
|
||||
*
|
||||
* @type {WorkflowTemplate}
|
||||
* @memberof Workflow
|
||||
*/
|
||||
'template'?: WorkflowTemplate;
|
||||
/**
|
||||
* ルーティング候補のタイピストユーザー/タイピストグループ
|
||||
* @type {Array<Assignee>}
|
||||
* @memberof Workflow
|
||||
*/
|
||||
'typists': Array<Assignee>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface WorkflowTemplate
|
||||
*/
|
||||
export interface WorkflowTemplate {
|
||||
/**
|
||||
* テンプレートの内部ID
|
||||
* @type {number}
|
||||
* @memberof WorkflowTemplate
|
||||
*/
|
||||
'id': number;
|
||||
/**
|
||||
* テンプレートのファイル名
|
||||
* @type {string}
|
||||
* @memberof WorkflowTemplate
|
||||
*/
|
||||
'fileName': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface WorkflowTypist
|
||||
*/
|
||||
export interface WorkflowTypist {
|
||||
/**
|
||||
* タイピストユーザーの内部ID
|
||||
* @type {number}
|
||||
* @memberof WorkflowTypist
|
||||
*/
|
||||
'typistId'?: number;
|
||||
/**
|
||||
* タイピストグループの内部ID
|
||||
* @type {number}
|
||||
* @memberof WorkflowTypist
|
||||
*/
|
||||
'typistGroupId'?: number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface WorkflowWorktype
|
||||
*/
|
||||
export interface WorkflowWorktype {
|
||||
/**
|
||||
* Worktypeの内部ID
|
||||
* @type {number}
|
||||
* @memberof WorkflowWorktype
|
||||
*/
|
||||
'id': number;
|
||||
/**
|
||||
* WorktypeID
|
||||
* @type {string}
|
||||
* @memberof WorkflowWorktype
|
||||
*/
|
||||
'worktypeId': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -2271,6 +2441,40 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* ログインしているユーザーのアカウント配下のAuthor一覧を取得します
|
||||
* @summary
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAuthors: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/accounts/authors`;
|
||||
// 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)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary
|
||||
@ -2980,6 +3184,16 @@ export const AccountsApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAccount(deleteAccountRequest, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
* ログインしているユーザーのアカウント配下のAuthor一覧を取得します
|
||||
* @summary
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getAuthors(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetAuthorsResponse>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAuthors(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary
|
||||
@ -3235,6 +3449,15 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP
|
||||
deleteAccount(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise<object> {
|
||||
return localVarFp.deleteAccount(deleteAccountRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* ログインしているユーザーのアカウント配下のAuthor一覧を取得します
|
||||
* @summary
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAuthors(options?: any): AxiosPromise<GetAuthorsResponse> {
|
||||
return localVarFp.getAuthors(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @summary
|
||||
@ -3488,6 +3711,17 @@ export class AccountsApi extends BaseAPI {
|
||||
return AccountsApiFp(this.configuration).deleteAccount(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* ログインしているユーザーのアカウント配下のAuthor一覧を取得します
|
||||
* @summary
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AccountsApi
|
||||
*/
|
||||
public getAuthors(options?: AxiosRequestConfig) {
|
||||
return AccountsApiFp(this.configuration).getAuthors(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary
|
||||
@ -6481,3 +6715,179 @@ export class UsersApi extends BaseAPI {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* WorkflowsApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const WorkflowsApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
* アカウント内にワークフローを新規作成します
|
||||
* @summary
|
||||
* @param {CreateWorkflowsRequest} createWorkflowsRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createWorkflows: async (createWorkflowsRequest: CreateWorkflowsRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'createWorkflowsRequest' is not null or undefined
|
||||
assertParamExists('createWorkflows', 'createWorkflowsRequest', createWorkflowsRequest)
|
||||
const localVarPath = `/workflows`;
|
||||
// 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(createWorkflowsRequest, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* アカウント内のワークフローの一覧を取得します
|
||||
* @summary
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getWorkflows: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/workflows`;
|
||||
// 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)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* WorkflowsApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const WorkflowsApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = WorkflowsApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
* アカウント内にワークフローを新規作成します
|
||||
* @summary
|
||||
* @param {CreateWorkflowsRequest} createWorkflowsRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createWorkflows(createWorkflowsRequest, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
* アカウント内のワークフローの一覧を取得します
|
||||
* @summary
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getWorkflows(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GetWorkflowsResponse>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getWorkflows(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* WorkflowsApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const WorkflowsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = WorkflowsApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
* アカウント内にワークフローを新規作成します
|
||||
* @summary
|
||||
* @param {CreateWorkflowsRequest} createWorkflowsRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: any): AxiosPromise<object> {
|
||||
return localVarFp.createWorkflows(createWorkflowsRequest, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* アカウント内のワークフローの一覧を取得します
|
||||
* @summary
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getWorkflows(options?: any): AxiosPromise<GetWorkflowsResponse> {
|
||||
return localVarFp.getWorkflows(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* WorkflowsApi - object-oriented interface
|
||||
* @export
|
||||
* @class WorkflowsApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class WorkflowsApi extends BaseAPI {
|
||||
/**
|
||||
* アカウント内にワークフローを新規作成します
|
||||
* @summary
|
||||
* @param {CreateWorkflowsRequest} createWorkflowsRequest
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof WorkflowsApi
|
||||
*/
|
||||
public createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: AxiosRequestConfig) {
|
||||
return WorkflowsApiFp(this.configuration).createWorkflows(createWorkflowsRequest, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウント内のワークフローの一覧を取得します
|
||||
* @summary
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof WorkflowsApi
|
||||
*/
|
||||
public getWorkflows(options?: AxiosRequestConfig) {
|
||||
return WorkflowsApiFp(this.configuration).getWorkflows(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ import typistGroup from "features/workflow/typistGroup/typistGroupSlice";
|
||||
import worktype from "features/workflow/worktype/worktypeSlice";
|
||||
import account from "features/account/accountSlice";
|
||||
import template from "features/workflow/template/templateSlice";
|
||||
import workflow from "features/workflow/workflowSlice";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
@ -38,6 +39,7 @@ export const store = configureStore({
|
||||
worktype,
|
||||
account,
|
||||
template,
|
||||
workflow,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
10
dictation_client/src/assets/images/download.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.8.1, 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,32.3l-9.6-9.6l2.1-2.1l6,6V8h3v18.6l6-6l2.2,2.1L24,32.3z M11,40c-0.8,0-1.5-0.3-2.1-0.9
|
||||
C8.3,38.5,8,37.8,8,37v-7.1h3V37h26v-7.1h3V37c0,0.8-0.3,1.5-0.9,2.1C38.5,39.7,37.8,40,37,40H11z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 623 B |
11
dictation_client/src/assets/images/exit.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.8.1, 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="M9,42c-0.8,0-1.5-0.3-2.1-0.9C6.3,40.5,6,39.8,6,39V28.5h3V39h30V9H9v10.5H6V9c0-0.8,0.3-1.5,0.9-2.1S8.2,6,9,6
|
||||
h30c0.8,0,1.5,0.3,2.1,0.9C41.7,7.5,42,8.2,42,9v30c0,0.8-0.3,1.5-0.9,2.1C40.5,41.7,39.8,42,39,42H9z M20.6,33.6l-2.2-2.3l5.9-5.9
|
||||
H6v-3h18.3l-5.9-5.9l2.2-2.2l9.7,9.6L20.6,33.6z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 721 B |
25
dictation_client/src/assets/images/group_setting.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.7.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>
|
||||
<g>
|
||||
<path class="st0" d="M25.7,23.7c2.5,0.6,4.8,0.2,6.7-1.2s2.9-3.4,2.9-6.1s-1-4.8-2.9-6.1c-1.9-1.3-4.1-1.7-6.7-1.1
|
||||
c0.9,1.1,1.5,2.2,1.9,3.3c0.4,1.1,0.6,2.5,0.6,4s-0.2,2.8-0.6,3.9C27.2,21.5,26.6,22.6,25.7,23.7z"/>
|
||||
<path class="st0" d="M17.8,24c2.2,0,4-0.7,5.4-2.1c1.4-1.4,2.1-3.2,2.1-5.4c0-2.2-0.7-4-2.1-5.4C21.8,9.6,20,9,17.8,9
|
||||
s-4,0.7-5.4,2.1c-1.4,1.4-2.1,3.2-2.1,5.4c0,2.2,0.7,4,2.1,5.4C13.8,23.2,15.5,24,17.8,24z M14.5,13.2c0.8-0.8,1.9-1.3,3.2-1.3
|
||||
s2.4,0.4,3.2,1.3c0.9,0.9,1.3,1.9,1.3,3.2c0,1.3-0.4,2.4-1.3,3.2C20.1,20.5,19,21,17.8,21s-2.4-0.4-3.2-1.3
|
||||
c-0.9-0.8-1.3-1.9-1.3-3.2C13.2,15.1,13.7,14.1,14.5,13.2z"/>
|
||||
<path class="st0" d="M44,36.5c0-0.6-0.1-1.3-0.2-2.1l1.9-1.5l-1.8-2.8l-2.1,1c-0.4-0.4-1-0.8-1.7-1.2c-0.7-0.4-1.4-0.7-2-0.9
|
||||
l-0.3-2.5h-3l-0.2,2.5c-0.7,0.2-1.4,0.5-2.1,0.9c-0.7,0.4-1.3,0.8-1.7,1.2l-2.1-1l-1.8,2.8l1.9,1.5c-0.2,0.8-0.2,1.5-0.2,2.1
|
||||
c0,0.6,0.1,1.3,0.2,2.1l-1.9,1.5l1.8,2.7l2.1-1c0.4,0.5,1,0.9,1.7,1.3c0.7,0.4,1.4,0.7,2.1,0.9l0.2,2.4h3l0.3-2.4
|
||||
c0.7-0.2,1.3-0.5,2-0.9c0.7-0.4,1.3-0.8,1.7-1.3l2.1,1l1.8-2.7l-1.9-1.5C43.9,37.8,44,37.1,44,36.5z M39.9,40.1
|
||||
c-1,1-2.2,1.5-3.6,1.5c-1.5,0-2.7-0.5-3.7-1.5s-1.5-2.2-1.5-3.6c0-1.5,0.5-2.7,1.5-3.7s2.2-1.5,3.7-1.5c1.5,0,2.7,0.5,3.6,1.5
|
||||
s1.5,2.2,1.5,3.7C41.3,38,40.8,39.2,39.9,40.1z"/>
|
||||
<path class="st0" d="M5,37v-1.7c0-0.5,0.1-1,0.4-1.5s0.7-0.8,1.2-1.1c2.4-1.1,4.3-1.8,5.9-2.2c1.6-0.4,3.3-0.5,5.2-0.5
|
||||
s3.7,0.2,5.2,0.5c0.4,0.1,0.9,0.2,1.4,0.4c0.7-0.8,1.5-1.6,2.4-2.3c-1.1-0.4-2.2-0.7-3.1-1c-1.9-0.5-3.8-0.7-5.9-0.7
|
||||
s-4,0.2-5.9,0.7c-1.9,0.5-4,1.2-6.4,2.3c-1,0.5-1.9,1.2-2.5,2.1c-0.6,1-0.9,2-0.9,3.2V40h19.1c0-1,0.1-2,0.3-3H5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
10
dictation_client/src/assets/images/logout.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.9.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="M9,42c-0.8,0-1.5-0.3-2.1-0.9C6.3,40.5,6,39.8,6,39V9c0-0.8,0.3-1.5,0.9-2.1S8.2,6,9,6h15v3H9v30h15v3H9z
|
||||
M33.3,32.7l-2.1-2.1l5.1-5.1H18v-3h18.2l-5.1-5.1l2.1-2.1L42,24L33.3,32.7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 614 B |
12
dictation_client/src/assets/images/rule_add.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.7.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="M6.6,40v-3h6.2l-0.7-0.7c-2-2-3.4-4-4.3-6.1s-1.3-4.1-1.3-6c0-3.6,1-6.9,3.1-9.8s4.8-4.8,8.2-5.9v3.1
|
||||
c-2.5,1-4.6,2.6-6.1,4.9s-2.3,4.9-2.3,7.7c0,1.7,0.3,3.4,0.9,4.9c0.6,1.6,1.6,3,3,4.2l1.5,1.3v-5.9h3V40H6.6z M41.5,23.3h-3
|
||||
c-0.1-1.7-0.4-3.3-1-4.7c-0.6-1.5-1.5-2.8-2.8-3.9l-1.5-1.4v5.8h-3V8h11.2v3h-6.2l0.8,0.7c1.9,1.8,3.3,3.8,4.2,5.8
|
||||
S41.5,21.4,41.5,23.3z M31.9,42.6v-6.5h-6.5v-3.9h6.5v-6.5h3.9v6.5h6.5v3.9h-6.5v6.5H31.9z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 857 B |
14
dictation_client/src/assets/images/template_setting.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.7.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="M11,44c-0.8,0-1.5-0.3-2.1-0.9C8.3,42.5,8,41.8,8,41V7c0-0.8,0.3-1.5,0.9-2.1C9.5,4.3,10.2,4,11,4h17l12,12v5.8
|
||||
h-3V18H26V7H11v34h13v3H11z M11,41V7V41z M34.9,45.7l-0.2-2.4c-0.7-0.2-1.4-0.5-2.1-0.9c-0.7-0.4-1.3-0.8-1.7-1.3l-2.1,1L27,39.5
|
||||
l1.9-1.5c-0.2-0.8-0.2-1.5-0.2-2.1s0.1-1.3,0.2-2.1L27,32.3l1.8-2.8l2.1,1c0.4-0.4,1-0.8,1.7-1.2c0.7-0.4,1.4-0.7,2.1-0.9l0.2-2.5h3
|
||||
l0.3,2.5c0.7,0.2,1.3,0.5,2,0.9s1.3,0.8,1.7,1.2l2.1-1l1.8,2.8l-1.9,1.5c0.2,0.8,0.2,1.5,0.2,2.1S44,37.2,43.9,38l1.9,1.5L44,42.2
|
||||
l-2.1-1c-0.4,0.5-1,0.9-1.7,1.3c-0.7,0.4-1.4,0.7-2,0.9l-0.3,2.4H34.9z M36.4,41c1.5,0,2.7-0.5,3.6-1.5c1-1,1.5-2.2,1.5-3.7
|
||||
S41,33.2,40,32.2c-1-1-2.2-1.5-3.6-1.5c-1.5,0-2.7,0.5-3.7,1.5c-1,1-1.5,2.2-1.5,3.6s0.5,2.7,1.5,3.7C33.7,40.5,34.9,41,36.4,41z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
17
dictation_client/src/assets/images/worktype_setting.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.7.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="M9,40c-0.8,0-1.5-0.3-2.1-0.9C6.3,38.5,6,37.8,6,37V7c0-0.8,0.3-1.5,0.9-2.1S8.2,4,9,4h30
|
||||
c0.8,0,1.5,0.3,2.1,0.9C41.7,5.5,42,6.2,42,7v15.1c-0.3,0-0.5-0.1-0.7-0.1c-0.2,0-0.5,0-0.7,0H39V7H9v30h6.9c0.2,0.5,0.3,1,0.5,1.5
|
||||
s0.4,1,0.7,1.5H9z M9,34v3V7V34z M14.5,31.5H16c0.4-1.2,1-2.5,1.8-3.7c0.8-1.2,1.8-2.3,2.8-3.3h-6.1V31.5z M14.5,19.5h7v-7h-7V19.5z
|
||||
M26.5,19.5h7v-7h-7V19.5z M31.5,45.8l-0.2-2.4c-0.7-0.2-1.4-0.5-2-0.9c-0.7-0.4-1.3-0.8-1.7-1.3l-2.1,1l-1.8-2.7l1.9-1.5
|
||||
c-0.2-0.8-0.2-1.5-0.2-2.1c0-0.6,0.1-1.3,0.2-2.1l-1.9-1.5l1.8-2.7l2.1,1c0.4-0.4,1-0.8,1.7-1.2c0.7-0.4,1.4-0.7,2-0.9l0.2-2.5h3
|
||||
l0.3,2.5c0.7,0.2,1.3,0.5,2,0.9s1.3,0.8,1.7,1.2l2.1-1l1.8,2.7l-1.9,1.5c0.2,0.8,0.2,1.5,0.2,2.1c0,0.6-0.1,1.3-0.2,2.1l1.9,1.5
|
||||
l-1.8,2.7l-2.1-1c-0.4,0.5-1,0.9-1.7,1.3c-0.7,0.4-1.4,0.7-2,0.9l-0.3,2.4H31.5z M33,41.1c1.5,0,2.7-0.5,3.6-1.5
|
||||
c1-1,1.5-2.2,1.5-3.6c0-1.5-0.5-2.7-1.5-3.7c-1-1-2.2-1.5-3.6-1.5c-1.5,0-2.7,0.5-3.7,1.5s-1.5,2.2-1.5,3.7c0,1.5,0.5,2.7,1.5,3.6
|
||||
S31.5,41.1,33,41.1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@ -55,4 +55,5 @@ export const errorCodes = [
|
||||
"E011001", // ワークタイプ重複エラー
|
||||
"E011002", // ワークタイプ登録上限超過エラー
|
||||
"E011003", // ワークタイプ不在エラー
|
||||
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
|
||||
] as const;
|
||||
|
||||
@ -117,6 +117,10 @@ export const dictationSlice = createSlice({
|
||||
state.apps.direction = action.payload.direction;
|
||||
state.apps.paramName = action.payload.paramName;
|
||||
});
|
||||
// 画面起動時にgetSortColumnAsyncがrejectedするとisLoadingがtrueのままになるため
|
||||
builder.addCase(getSortColumnAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(listTypistsAsync.fulfilled, (state, action) => {
|
||||
state.domain.typists = action.payload.typists;
|
||||
});
|
||||
|
||||
@ -41,6 +41,13 @@ export const listUsersAsync = createAsyncThunk<
|
||||
// e ⇒ errorObjectに変換
|
||||
const error = createErrorObject(e);
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
4
dictation_client/src/features/workflow/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./workflowSlice";
|
||||
export * from "./state";
|
||||
export * from "./selectors";
|
||||
export * from "./operations";
|
||||
213
dictation_client/src/features/workflow/operations.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import {
|
||||
AccountsApi,
|
||||
Author,
|
||||
Configuration,
|
||||
GetWorkflowsResponse,
|
||||
TemplateFile,
|
||||
TemplatesApi,
|
||||
Typist,
|
||||
TypistGroup,
|
||||
WorkflowTypist,
|
||||
WorkflowsApi,
|
||||
Worktype,
|
||||
} from "api";
|
||||
import type { RootState } from "app/store";
|
||||
import { ErrorObject, createErrorObject } from "common/errors";
|
||||
import { openSnackbar } from "features/ui/uiSlice";
|
||||
import { getTranslationID } from "translation";
|
||||
import { WorkflowRelations } from "./state";
|
||||
|
||||
export const listWorkflowAsync = createAsyncThunk<
|
||||
GetWorkflowsResponse,
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("workflow/listWorkflowAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration, accessToken } = state.auth;
|
||||
const config = new Configuration(configuration);
|
||||
const workflowsApi = new WorkflowsApi(config);
|
||||
|
||||
try {
|
||||
const { data } = await workflowsApi.getWorkflows({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const createWorkflowAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("workflow/createWorkflowAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration, accessToken } = state.auth;
|
||||
const config = new Configuration(configuration);
|
||||
const workflowsApi = new WorkflowsApi(config);
|
||||
const { selectedAssignees, authorId, templateId, worktypeId } =
|
||||
state.workflow.apps;
|
||||
|
||||
try {
|
||||
if (authorId === undefined) {
|
||||
throw new Error("authorId is not found");
|
||||
}
|
||||
// 選択されたタイピストを取得し、リクエスト用の型に変換する
|
||||
const typists = selectedAssignees.map(
|
||||
(item): WorkflowTypist => ({
|
||||
typistId: item.typistUserId,
|
||||
typistGroupId: item.typistGroupId,
|
||||
})
|
||||
);
|
||||
await workflowsApi.createWorkflows(
|
||||
{
|
||||
authorId,
|
||||
typists,
|
||||
templateId,
|
||||
worktypeId,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
const { code, statusCode } = error;
|
||||
// AuthorIDとWorktypeIDが一致するものが既に存在する場合
|
||||
if (code === "E013001") {
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID(
|
||||
"workflowPage.message.workflowConflictError"
|
||||
),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
// パラメータが存在しない場合
|
||||
if (statusCode === 400) {
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("workflowPage.message.saveFailedError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
// その他のエラー
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const getworkflowRelationsAsync = createAsyncThunk<
|
||||
{
|
||||
authors: Author[];
|
||||
typists: Typist[];
|
||||
typistGroups: TypistGroup[];
|
||||
templates: TemplateFile[];
|
||||
worktypes: Worktype[];
|
||||
},
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("workflow/getworkflowRelationsAsync", 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);
|
||||
const templatesApi = new TemplatesApi(config);
|
||||
|
||||
try {
|
||||
const { authors } = (
|
||||
await accountsApi.getAuthors({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
).data;
|
||||
const { typists } = (
|
||||
await accountsApi.getTypists({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
).data;
|
||||
const { typistGroups } = (
|
||||
await accountsApi.getTypistGroups({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
).data;
|
||||
const { templates } = (
|
||||
await templatesApi.getTemplates({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
).data;
|
||||
const { worktypes } = (
|
||||
await accountsApi.getWorktypes({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
).data;
|
||||
|
||||
return {
|
||||
authors,
|
||||
typists,
|
||||
typistGroups,
|
||||
templates,
|
||||
worktypes,
|
||||
};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
52
dictation_client/src/features/workflow/selectors.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Assignee } from "api";
|
||||
import { RootState } from "app/store";
|
||||
|
||||
export const selectWorkflows = (state: RootState) =>
|
||||
state.workflow.domain.workflows;
|
||||
|
||||
export const selectIsLoading = (state: RootState) =>
|
||||
state.workflow.apps.isLoading;
|
||||
|
||||
export const selectWorkflowRelations = (state: RootState) =>
|
||||
state.workflow.domain.workflowRelations;
|
||||
|
||||
export const selectWorkflowAssinee = (state: RootState) => {
|
||||
// 選択されたassigneeを取得
|
||||
const { selectedAssignees } = state.workflow.apps;
|
||||
// すべてのassigneeを取得
|
||||
const assignees = state.workflow.domain.workflowRelations?.assignees ?? [];
|
||||
// assigneeが選択されているかどうかを判定する
|
||||
const isAssigneeSelected = (assignee: Assignee) =>
|
||||
selectedAssignees.some(
|
||||
(sa) =>
|
||||
sa.typistUserId === assignee.typistUserId &&
|
||||
sa.typistGroupId === assignee.typistGroupId
|
||||
);
|
||||
// 未選択のassigneeを取得する
|
||||
const poolAssignees = assignees.filter(
|
||||
(assignee) => !isAssigneeSelected(assignee)
|
||||
);
|
||||
// selectedAssigneesとpoolAssigneesをtypistNameでソートして返す
|
||||
return {
|
||||
selectedAssignees: [...selectedAssignees].sort((a, b) =>
|
||||
a.typistName.localeCompare(b.typistName)
|
||||
),
|
||||
poolAssignees: poolAssignees.sort((a, b) =>
|
||||
a.typistName.localeCompare(b.typistName)
|
||||
),
|
||||
};
|
||||
};
|
||||
export const selectIsAddLoading = (state: RootState) =>
|
||||
state.workflow.apps.isAddLoading;
|
||||
|
||||
export const selectWorkflowError = (state: RootState) => {
|
||||
// authorIdがundefinedの場合はエラーを返す
|
||||
const hasAuthorIdEmptyError = state.workflow.apps.authorId === undefined;
|
||||
// workflowAssineeのselectedが空の場合はエラーを返す
|
||||
const hasSelectedWorkflowAssineeEmptyError =
|
||||
state.workflow.apps.selectedAssignees.length === 0;
|
||||
return {
|
||||
hasAuthorIdEmptyError,
|
||||
hasSelectedWorkflowAssineeEmptyError,
|
||||
};
|
||||
};
|
||||
27
dictation_client/src/features/workflow/state.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Assignee, Author, TemplateFile, Workflow, Worktype } from "api";
|
||||
|
||||
export interface WorkflowState {
|
||||
apps: Apps;
|
||||
domain: Domain;
|
||||
}
|
||||
|
||||
export interface Apps {
|
||||
isLoading: boolean;
|
||||
isAddLoading: boolean;
|
||||
selectedAssignees: Assignee[];
|
||||
authorId?: number;
|
||||
worktypeId?: number;
|
||||
templateId?: number;
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
workflows?: Workflow[];
|
||||
workflowRelations?: WorkflowRelations;
|
||||
}
|
||||
|
||||
export interface WorkflowRelations {
|
||||
authors: Author[];
|
||||
assignees: Assignee[];
|
||||
templates: TemplateFile[];
|
||||
worktypes: Worktype[];
|
||||
}
|
||||
142
dictation_client/src/features/workflow/workflowSlice.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { Assignee } from "api";
|
||||
import {
|
||||
createWorkflowAsync,
|
||||
getworkflowRelationsAsync,
|
||||
listWorkflowAsync,
|
||||
} from "./operations";
|
||||
import { WorkflowState } from "./state";
|
||||
|
||||
const initialState: WorkflowState = {
|
||||
apps: {
|
||||
isLoading: false,
|
||||
isAddLoading: false,
|
||||
selectedAssignees: [],
|
||||
},
|
||||
domain: {},
|
||||
};
|
||||
|
||||
export const workflowSlice = createSlice({
|
||||
name: "workflow",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearWorkflow: (state) => {
|
||||
state.apps.selectedAssignees = [];
|
||||
state.apps.authorId = undefined;
|
||||
state.apps.worktypeId = undefined;
|
||||
state.apps.templateId = undefined;
|
||||
state.domain.workflowRelations = undefined;
|
||||
},
|
||||
addAssignee: (state, action: PayloadAction<{ assignee: Assignee }>) => {
|
||||
const { assignee } = action.payload;
|
||||
const { selectedAssignees } = state.apps;
|
||||
|
||||
// assigneeがselectedAssigneesに存在するか確認する
|
||||
const isDuplicate = selectedAssignees.some(
|
||||
(x) =>
|
||||
x.typistUserId === assignee.typistUserId &&
|
||||
x.typistGroupId === assignee.typistGroupId
|
||||
);
|
||||
|
||||
// 重複していなければ追加する
|
||||
if (!isDuplicate) {
|
||||
const newSelectedAssignees = [...selectedAssignees, assignee];
|
||||
// stateに保存する
|
||||
state.apps.selectedAssignees = newSelectedAssignees;
|
||||
}
|
||||
},
|
||||
removeAssignee: (state, action: PayloadAction<{ assignee: Assignee }>) => {
|
||||
const { assignee } = action.payload;
|
||||
const { selectedAssignees } = state.apps;
|
||||
// selectedAssigneeの要素からassigneeを削除する
|
||||
state.apps.selectedAssignees = selectedAssignees.filter(
|
||||
(x) =>
|
||||
x.typistUserId !== assignee.typistUserId ||
|
||||
x.typistGroupId !== assignee.typistGroupId
|
||||
);
|
||||
},
|
||||
changeAuthor: (state, action: PayloadAction<{ authorId: number }>) => {
|
||||
const { authorId } = action.payload;
|
||||
state.apps.authorId = authorId;
|
||||
},
|
||||
changeWorktype: (state, action: PayloadAction<{ worktypeId?: number }>) => {
|
||||
const { worktypeId } = action.payload;
|
||||
state.apps.worktypeId = worktypeId;
|
||||
},
|
||||
changeTemplate: (state, action: PayloadAction<{ templateId?: number }>) => {
|
||||
const { templateId } = action.payload;
|
||||
state.apps.templateId = templateId;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(listWorkflowAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(listWorkflowAsync.fulfilled, (state, action) => {
|
||||
const { workflows } = action.payload;
|
||||
|
||||
state.domain.workflows = workflows;
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(listWorkflowAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getworkflowRelationsAsync.pending, (state) => {
|
||||
state.apps.isAddLoading = true;
|
||||
});
|
||||
builder.addCase(getworkflowRelationsAsync.fulfilled, (state, action) => {
|
||||
const { authors, typistGroups, typists, templates, worktypes } =
|
||||
action.payload;
|
||||
|
||||
// 取得したtypistsとtypistGroupsを型変換
|
||||
const assineeTypists = typists.map(
|
||||
(typist): Assignee => ({
|
||||
typistUserId: typist.id,
|
||||
typistGroupId: undefined,
|
||||
typistName: typist.name,
|
||||
})
|
||||
);
|
||||
const assineeTypistGroups = typistGroups.map(
|
||||
(typistGroup): Assignee => ({
|
||||
typistUserId: undefined,
|
||||
typistGroupId: typistGroup.id,
|
||||
typistName: typistGroup.name,
|
||||
})
|
||||
);
|
||||
// 取得したtypistsとtypistGroupsを結合
|
||||
const assinees = [...assineeTypists, ...assineeTypistGroups];
|
||||
// storeに保存
|
||||
state.domain.workflowRelations = {
|
||||
authors,
|
||||
assignees: assinees,
|
||||
templates,
|
||||
worktypes,
|
||||
};
|
||||
|
||||
state.apps.isAddLoading = false;
|
||||
});
|
||||
builder.addCase(getworkflowRelationsAsync.rejected, (state) => {
|
||||
state.apps.isAddLoading = false;
|
||||
});
|
||||
builder.addCase(createWorkflowAsync.pending, (state) => {
|
||||
state.apps.isAddLoading = true;
|
||||
});
|
||||
builder.addCase(createWorkflowAsync.fulfilled, (state) => {
|
||||
state.apps.isAddLoading = false;
|
||||
});
|
||||
builder.addCase(createWorkflowAsync.rejected, (state) => {
|
||||
state.apps.isAddLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
addAssignee,
|
||||
removeAssignee,
|
||||
changeAuthor,
|
||||
changeWorktype,
|
||||
changeTemplate,
|
||||
clearWorkflow,
|
||||
} = workflowSlice.actions;
|
||||
|
||||
export default workflowSlice.reducer;
|
||||
@ -0,0 +1,54 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
import Header from "components/header";
|
||||
import Footer from "components/footer";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { Link } from "react-router-dom";
|
||||
import { clearToken } from "features/auth";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
export const AccountDeleteSuccess: React.FC = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
|
||||
// アカウントの削除完了時に遷移するページなので、遷移と同時にログアウト状態とする
|
||||
useEffect(() => {
|
||||
dispatch(clearToken());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<Header />
|
||||
<main className={styles.main}>
|
||||
<div className={styles.mainSmall}>
|
||||
<div>
|
||||
<h1 className={styles.marginBtm1}>
|
||||
{t(getTranslationID("accountDeleteSuccess.label.title"))}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<dl className={`${styles.formList} ${styles.hasbg}`}>
|
||||
<dt className={styles.formTitle} />
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
{t(getTranslationID("accountDeleteSuccess.label.message"))}
|
||||
</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<Link to="/">
|
||||
{t(
|
||||
getTranslationID(
|
||||
"accountDeleteSuccess.label.backToTopPageLink"
|
||||
)
|
||||
)}
|
||||
</Link>
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -2,18 +2,19 @@ import React, { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { selectAccountInfo, selectIsLoading } from "features/account";
|
||||
import { deleteAccountAsync } from "features/account/operations";
|
||||
import { useMsal } from "@azure/msal-react";
|
||||
import styles from "../../styles/app.module.scss";
|
||||
import { getTranslationID } from "../../translation";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import deleteButton from "../../assets/images/delete.svg";
|
||||
import { selectAccountInfo, selectIsLoading } from "features/account";
|
||||
import { deleteAccountAsync } from "features/account/operations";
|
||||
|
||||
interface deleteAccountPopupProps {
|
||||
interface DeleteAccountPopupProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const DeleteAccountPopup: React.FC<deleteAccountPopupProps> = (
|
||||
export const DeleteAccountPopup: React.FC<DeleteAccountPopupProps> = (
|
||||
props
|
||||
) => {
|
||||
const { onClose } = props;
|
||||
@ -21,6 +22,8 @@ export const DeleteAccountPopup: React.FC<deleteAccountPopupProps> = (
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
|
||||
const { instance } = useMsal();
|
||||
|
||||
const accountInfo = useSelector(selectAccountInfo);
|
||||
|
||||
// ポップアップを閉じる処理
|
||||
@ -31,13 +34,21 @@ export const DeleteAccountPopup: React.FC<deleteAccountPopupProps> = (
|
||||
onClose();
|
||||
}, [isLoading, onClose]);
|
||||
|
||||
const onDeleteAccount = useCallback(() => {
|
||||
dispatch(
|
||||
const onDeleteAccount = useCallback(async () => {
|
||||
const { meta } = await dispatch(
|
||||
deleteAccountAsync({
|
||||
accountId: accountInfo.account.accountId,
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
// 削除成功後にAccountDeleteSuccess ページに遷移
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
instance.logoutRedirect({
|
||||
postLogoutRedirectUri: "/accountDeleteSuccess",
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [instance]);
|
||||
|
||||
// HTML
|
||||
return (
|
||||
@ -74,7 +85,7 @@ export const DeleteAccountPopup: React.FC<deleteAccountPopupProps> = (
|
||||
</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<input
|
||||
type="submit"
|
||||
type="button"
|
||||
name="submit"
|
||||
value={t(
|
||||
getTranslationID("deleteAccountPopup.label.deleteButton")
|
||||
|
||||
@ -331,6 +331,16 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
{!isLoading && licenseOrderHistory.length === 0 && (
|
||||
<p
|
||||
style={{
|
||||
textAlign: "center",
|
||||
width: "1000px",
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{isLoading && (
|
||||
<img
|
||||
|
||||
@ -452,6 +452,16 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
{!isLoading && childrenPartnerLicensesInfo.length === 0 && (
|
||||
<p
|
||||
style={{
|
||||
margin: "10px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
{/* pagenation */}
|
||||
<div className={styles.pagenation}>
|
||||
<nav className={styles.pagenationNav}>
|
||||
|
||||
@ -189,6 +189,16 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
{!isLoading && partnerInfo.partners.length === 0 && (
|
||||
<p
|
||||
style={{
|
||||
textAlign: "center",
|
||||
width: "1420px",
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
{/** pagenation */}
|
||||
<div className={styles.pagenation}>
|
||||
<nav className={styles.pagenationNav}>
|
||||
|
||||
@ -101,16 +101,17 @@ export const TemplateFilePage: React.FC = () => {
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!isLoading && templates?.length === 0 && (
|
||||
<p
|
||||
style={{
|
||||
margin: "10px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
{!isLoading &&
|
||||
(templates === undefined || templates.length === 0) && (
|
||||
<p
|
||||
style={{
|
||||
margin: "10px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
{isLoading && (
|
||||
<img
|
||||
src={progress_activit}
|
||||
|
||||
@ -290,6 +290,16 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{!isLoading && users.length === 0 && (
|
||||
<p
|
||||
style={{
|
||||
margin: "10px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
{isLoading && (
|
||||
<img
|
||||
src={progress_activit}
|
||||
|
||||
@ -265,17 +265,18 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
{!isLoading && worktypes?.length === 0 && (
|
||||
<p
|
||||
style={{
|
||||
margin: "10px",
|
||||
textAlign: "center",
|
||||
width: "1000px",
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
{!isLoading &&
|
||||
(worktypes === undefined || worktypes.length === 0) && (
|
||||
<p
|
||||
style={{
|
||||
margin: "10px",
|
||||
textAlign: "center",
|
||||
width: "1000px",
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
{isLoading && (
|
||||
<img
|
||||
src={progress_activit}
|
||||
|
||||
293
dictation_client/src/pages/WorkflowPage/addworkflowPopup.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { AppDispatch } from "app/store";
|
||||
import progress_activit from "assets/images/progress_activit.svg";
|
||||
import {
|
||||
addAssignee,
|
||||
removeAssignee,
|
||||
changeAuthor,
|
||||
changeTemplate,
|
||||
changeWorktype,
|
||||
clearWorkflow,
|
||||
selectIsAddLoading,
|
||||
selectWorkflowAssinee,
|
||||
selectWorkflowError,
|
||||
selectWorkflowRelations,
|
||||
} from "features/workflow";
|
||||
import {
|
||||
createWorkflowAsync,
|
||||
getworkflowRelationsAsync,
|
||||
listWorkflowAsync,
|
||||
} from "features/workflow/operations";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { getTranslationID } from "translation";
|
||||
import close from "../../assets/images/close.svg";
|
||||
|
||||
interface AddWorkflowPopupProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const AddWorkflowPopup: React.FC<AddWorkflowPopupProps> = (
|
||||
props
|
||||
): JSX.Element => {
|
||||
const { onClose } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
// 保存ボタンを押したかどうか
|
||||
const [isPushAddButton, setIsPushAddButton] = useState<boolean>(false);
|
||||
|
||||
const workflowRelations = useSelector(selectWorkflowRelations);
|
||||
const { poolAssignees, selectedAssignees } = useSelector(
|
||||
selectWorkflowAssinee
|
||||
);
|
||||
const isLoading = useSelector(selectIsAddLoading);
|
||||
const { hasAuthorIdEmptyError, hasSelectedWorkflowAssineeEmptyError } =
|
||||
useSelector(selectWorkflowError);
|
||||
useEffect(() => {
|
||||
dispatch(getworkflowRelationsAsync());
|
||||
// ポップアップのアンマウント時に初期化を行う
|
||||
return () => {
|
||||
dispatch(clearWorkflow());
|
||||
setIsPushAddButton(false);
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const changeWorktypeId = useCallback(
|
||||
(target: string) => {
|
||||
// 空文字の場合はundefinedをdispatchする
|
||||
if (target === "") {
|
||||
dispatch(changeWorktype({ worktypeId: undefined }));
|
||||
} else if (!Number.isNaN(Number(target))) {
|
||||
dispatch(changeWorktype({ worktypeId: Number(target) }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const changeTemplateId = useCallback(
|
||||
(target: string) => {
|
||||
// 空文字の場合はundefinedをdispatchする
|
||||
if (target === "") {
|
||||
dispatch(changeTemplate({ templateId: undefined }));
|
||||
} else if (!Number.isNaN(Number(target))) {
|
||||
dispatch(changeTemplate({ templateId: Number(target) }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const changeAuthorId = useCallback(
|
||||
(target: string) => {
|
||||
if (!Number.isNaN(target)) {
|
||||
dispatch(changeAuthor({ authorId: Number(target) }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// 追加ボタン押下時の処理
|
||||
const handleAdd = useCallback(async () => {
|
||||
setIsPushAddButton(true);
|
||||
// エラーチェック
|
||||
if (hasAuthorIdEmptyError || hasSelectedWorkflowAssineeEmptyError) {
|
||||
return;
|
||||
}
|
||||
const { meta } = await dispatch(createWorkflowAsync());
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
onClose();
|
||||
dispatch(listWorkflowAsync());
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
hasAuthorIdEmptyError,
|
||||
hasSelectedWorkflowAssineeEmptyError,
|
||||
onClose,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.modal} ${styles.isShow}`}>
|
||||
<div className={styles.modalBox}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("workflowPage.label.addRoutingRule"))}
|
||||
{/* 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={onClose}
|
||||
/>
|
||||
</p>
|
||||
<form className={styles.form}>
|
||||
<dl className={`${styles.formList} ${styles.hasbg}`}>
|
||||
<dt className={styles.formTitle} />
|
||||
<dt>{t(getTranslationID("workflowPage.label.authorID"))}</dt>
|
||||
<dd>
|
||||
<select
|
||||
className={styles.formInput}
|
||||
onChange={(e) => {
|
||||
changeAuthorId(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="" hidden>
|
||||
{`-- ${t(
|
||||
getTranslationID("workflowPage.label.selectAuthor")
|
||||
)} --`}
|
||||
</option>
|
||||
{workflowRelations?.authors.map((author) => (
|
||||
<option key={author.authorId} value={author.id}>
|
||||
{author.authorId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isPushAddButton && hasAuthorIdEmptyError && (
|
||||
<span className={styles.formError}>
|
||||
{t(getTranslationID("workflowPage.message.inputEmptyError"))}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
<dt className={styles.overLine}>
|
||||
{t(getTranslationID("workflowPage.label.worktypeOptional"))}
|
||||
</dt>
|
||||
<dd>
|
||||
<select
|
||||
className={styles.formInput}
|
||||
onChange={(e) => {
|
||||
changeWorktypeId(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="" hidden>
|
||||
{`-- ${t(
|
||||
getTranslationID("workflowPage.label.selectWorktypeId")
|
||||
)} --`}
|
||||
</option>
|
||||
<option value="">
|
||||
{`-- ${t(getTranslationID("common.label.notSelected"))} --`}
|
||||
</option>
|
||||
{workflowRelations?.worktypes.map((worktype) => (
|
||||
<option key={worktype.id} value={worktype.id}>
|
||||
{worktype.worktypeId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</dd>
|
||||
<dt className={styles.formTitle}>
|
||||
{t(getTranslationID("typistGroupSetting.label.transcriptionist"))}
|
||||
</dt>
|
||||
<dd className={`${styles.formChange} ${styles.last}`}>
|
||||
<ul className={styles.chooseMember}>
|
||||
<li className={styles.changeTitle}>
|
||||
{t(getTranslationID("workflowPage.label.selected"))}
|
||||
</li>
|
||||
{selectedAssignees?.map((x) => {
|
||||
const key = `${x.typistName}_${
|
||||
x.typistUserId ?? x.typistGroupId
|
||||
}`;
|
||||
return (
|
||||
<li key={key}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.formCheck}
|
||||
value={x.typistName}
|
||||
id={key}
|
||||
checked
|
||||
onClick={() => {
|
||||
dispatch(removeAssignee({ assignee: x }));
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={key} title="Remove">
|
||||
{x.typistName}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<p />
|
||||
<ul className={styles.holdMember}>
|
||||
<li className={styles.changeTitle}>
|
||||
{t(getTranslationID("workflowPage.label.pool"))}
|
||||
</li>
|
||||
{poolAssignees?.map((x) => {
|
||||
const key = `${x.typistName}_${
|
||||
x.typistUserId ?? x.typistGroupId
|
||||
}`;
|
||||
return (
|
||||
<li key={key}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.formCheck}
|
||||
value={x.typistName}
|
||||
id={key}
|
||||
onClick={() => dispatch(addAssignee({ assignee: x }))}
|
||||
/>
|
||||
<label htmlFor={key} title="Add">
|
||||
{x.typistName}
|
||||
</label>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{isPushAddButton && hasSelectedWorkflowAssineeEmptyError && (
|
||||
<span
|
||||
className={styles.formError}
|
||||
style={{ margin: "0px 30px 0px 30px" }}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"workflowPage.message.selectedTypistEmptyError"
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
<dt className={styles.overLine}>
|
||||
{t(getTranslationID("workflowPage.label.templateOptional"))}
|
||||
</dt>
|
||||
<dd className={styles.last}>
|
||||
<select
|
||||
className={styles.formInput}
|
||||
onChange={(e) => {
|
||||
changeTemplateId(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="" hidden>
|
||||
{`-- ${t(
|
||||
getTranslationID("workflowPage.label.selectTemplate")
|
||||
)} --`}
|
||||
</option>
|
||||
<option value="">
|
||||
{`-- ${t(getTranslationID("common.label.notSelected"))} --`}
|
||||
</option>
|
||||
{workflowRelations?.templates.map((template) => (
|
||||
<option
|
||||
key={`${template.name}_${template.id}`}
|
||||
value={template.id}
|
||||
>
|
||||
{template.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<input
|
||||
type="button"
|
||||
value={t(getTranslationID("common.label.save"))}
|
||||
className={`${styles.formSubmit} ${styles.marginBtm1} ${
|
||||
!isLoading ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={handleAdd}
|
||||
/>
|
||||
{isLoading && (
|
||||
<img
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,34 +1,195 @@
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Header from "components/header";
|
||||
import Footer from "components/footer";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { UpdateTokenTimer } from "components/auth/updateTokenTimer";
|
||||
import ruleAddImg from "assets/images/rule_add.svg";
|
||||
import templateSettingImg from "assets/images/template_setting.svg";
|
||||
import worktypeSettingImg from "assets/images/worktype_setting.svg";
|
||||
import groupSettingImg from "assets/images/group_setting.svg";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { listWorkflowAsync } from "features/workflow/operations";
|
||||
import { selectIsLoading, selectWorkflows } from "features/workflow";
|
||||
import progress_activit from "assets/images/progress_activit.svg";
|
||||
import { getTranslationID } from "translation";
|
||||
import { AddWorkflowPopup } from "./addworkflowPopup";
|
||||
|
||||
const WorkflowPage: React.FC = (): JSX.Element => (
|
||||
<div className={styles.wrap}>
|
||||
<Header userName="XXXXXX" />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div className="">
|
||||
<span>
|
||||
<a style={{ margin: 20 }} href="/workflow/typist-group">
|
||||
Transcriptionist Group Setting
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
<a style={{ margin: 20 }} href="/workflow/worktype-id">
|
||||
Worktype ID Setting
|
||||
</a>
|
||||
</span>
|
||||
<span>
|
||||
<a style={{ margin: 20 }} href="/workflow/template">
|
||||
Template File
|
||||
</a>
|
||||
</span>
|
||||
const WorkflowPage: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
// 追加Popupの表示制御
|
||||
const [isShowAddPopup, setIsShowAddPopup] = useState<boolean>(false);
|
||||
const workflows = useSelector(selectWorkflows);
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(listWorkflowAsync());
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<>
|
||||
{isShowAddPopup && (
|
||||
<AddWorkflowPopup
|
||||
onClose={() => {
|
||||
setIsShowAddPopup(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.wrap}>
|
||||
<Header userName="XXXXXX" />
|
||||
<UpdateTokenTimer />
|
||||
<main className={styles.main}>
|
||||
<div className="">
|
||||
<div className={styles.pageHeader}>
|
||||
<h1 className={styles.pageTitle}>
|
||||
{t(getTranslationID("workflowPage.label.title"))}
|
||||
</h1>
|
||||
</div>
|
||||
<section className={styles.workflow}>
|
||||
<div>
|
||||
<ul className={`${styles.menuAction} ${styles.alignRight}`}>
|
||||
<li className={styles.floatLeft}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={() => {
|
||||
setIsShowAddPopup(true);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={ruleAddImg}
|
||||
alt="addRoutingRule"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(getTranslationID("workflowPage.label.addRoutingRule"))}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/workflow/template"
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
>
|
||||
<img
|
||||
src={templateSettingImg}
|
||||
alt="templateSetting"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(
|
||||
getTranslationID("workflowPage.label.templateSetting")
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/workflow/worktype-id"
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
>
|
||||
<img
|
||||
src={worktypeSettingImg}
|
||||
alt="worktypeIdSetting"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(
|
||||
getTranslationID("workflowPage.label.worktypeIdSetting")
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/workflow/typist-group"
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
>
|
||||
<img
|
||||
src={groupSettingImg}
|
||||
alt="typistGroupSetting"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"workflowPage.label.typistGroupSetting"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<table className={`${styles.table} ${styles.workflow}`}>
|
||||
<tr className={styles.tableHeader}>
|
||||
<th className={styles.clm0}>{/** empty th */}</th>
|
||||
<th>
|
||||
{t(getTranslationID("workflowPage.label.authorID"))}
|
||||
</th>
|
||||
<th>
|
||||
{t(getTranslationID("workflowPage.label.worktype"))}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID("workflowPage.label.transcriptionist")
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
{t(getTranslationID("workflowPage.label.template"))}
|
||||
</th>
|
||||
</tr>
|
||||
{workflows?.map((workflow) => (
|
||||
<tr key={workflow.id}>
|
||||
<td className={styles.clm0}>
|
||||
<ul className={styles.menuInTable}>
|
||||
<li>
|
||||
<a href="">
|
||||
{t(
|
||||
getTranslationID("workflowPage.label.editRule")
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="">
|
||||
{t(getTranslationID("common.label.delete"))}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>{workflow.author.authorId}</td>
|
||||
<td>{workflow.worktype?.worktypeId ?? "-"}</td>
|
||||
|
||||
<td className={styles.txWsline}>
|
||||
{workflow.typists.map((typist, i) => (
|
||||
<>
|
||||
{typist.typistName}
|
||||
{i !== workflow.typists.length - 1 && <br />}
|
||||
</>
|
||||
))}
|
||||
</td>
|
||||
<td>{workflow.template?.fileName ?? "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
{!isLoading &&
|
||||
(workflows === undefined || workflows.length === 0) && (
|
||||
<p
|
||||
style={{
|
||||
margin: "10px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
{isLoading && (
|
||||
<img
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowPage;
|
||||
|
||||
@ -356,7 +356,29 @@
|
||||
},
|
||||
"workflowPage": {
|
||||
"label": {
|
||||
"title": "Arbeitsablauf"
|
||||
"title": "Arbeitsablauf",
|
||||
"addRoutingRule": "(de)Add Routing Rule",
|
||||
"templateSetting": "(de)Template Setting",
|
||||
"worktypeIdSetting": "(de)WorktypeID Setting",
|
||||
"typistGroupSetting": "(de)Transcriptionist Group Setting",
|
||||
"authorID": "Autoren-ID",
|
||||
"worktype": "Aufgabentypkennung",
|
||||
"worktypeOptional": "(de)Worktype ID (Optional)",
|
||||
"transcriptionist": "Transkriptionist",
|
||||
"template": "(de)Template",
|
||||
"templateOptional": "(de)Template (Optional)",
|
||||
"editRule": "(de)Edit Rule",
|
||||
"selected": "Ausgewählter transkriptionist",
|
||||
"pool": "Transkriptionsliste",
|
||||
"selectAuthor": "(de)Select Author ID",
|
||||
"selectWorktypeId": "(de)Select Worktype ID",
|
||||
"selectTemplate": "(de)Select Template"
|
||||
},
|
||||
"message": {
|
||||
"selectedTypistEmptyError": "(de)Transcriptionist,TranscriptionistGroupがいないルーティングルールは保存できません。ルーティング先を1つ以上選択してください。",
|
||||
"workflowConflictError": "(de)指定したAuthorIDとWorktypeIDの組み合わせで既にルーティングルールが登録されています。他の組み合わせで登録してください。",
|
||||
"inputEmptyError": "Pflichtfeld",
|
||||
"saveFailedError": "(de)ルーティングルールの保存に失敗しました。画面を更新し、再度実行してください"
|
||||
}
|
||||
},
|
||||
"typistGroupSetting": {
|
||||
@ -468,5 +490,12 @@
|
||||
"deleteButton": "(de)Delete account",
|
||||
"cancelButton": "(de)Cancel"
|
||||
}
|
||||
},
|
||||
"accountDeleteSuccess": {
|
||||
"label": {
|
||||
"title": "(de)Account Delete Success",
|
||||
"message": "(de)Your account has been deleted. Thank you for using our services.",
|
||||
"backToTopPageLink": "(de)Back to TOP Page"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,7 +356,29 @@
|
||||
},
|
||||
"workflowPage": {
|
||||
"label": {
|
||||
"title": "Workflow"
|
||||
"title": "Workflow",
|
||||
"addRoutingRule": "Add Routing Rule",
|
||||
"templateSetting": "Template Setting",
|
||||
"worktypeIdSetting": "WorktypeID Setting",
|
||||
"typistGroupSetting": "Transcriptionist Group Setting",
|
||||
"authorID": "Author ID",
|
||||
"worktype": "Worktype ID",
|
||||
"worktypeOptional": "Worktype ID (Optional)",
|
||||
"transcriptionist": "Transcriptionist",
|
||||
"template": "Template",
|
||||
"templateOptional": "Template (Optional)",
|
||||
"editRule": "Edit Rule",
|
||||
"selected": "Selected Transcriptionist",
|
||||
"pool": "Transcription List",
|
||||
"selectAuthor": "Select Author ID",
|
||||
"selectWorktypeId": "Select Worktype ID",
|
||||
"selectTemplate": "Select Template"
|
||||
},
|
||||
"message": {
|
||||
"selectedTypistEmptyError": "Transcriptionist,TranscriptionistGroupがいないルーティングルールは保存できません。ルーティング先を1つ以上選択してください。",
|
||||
"workflowConflictError": "指定したAuthorIDとWorktypeIDの組み合わせで既にルーティングルールが登録されています。他の組み合わせで登録してください。",
|
||||
"inputEmptyError": "Mandatory Field",
|
||||
"saveFailedError": "ルーティングルールの保存に失敗しました。画面を更新し、再度実行してください"
|
||||
}
|
||||
},
|
||||
"typistGroupSetting": {
|
||||
@ -468,5 +490,12 @@
|
||||
"deleteButton": "Delete account",
|
||||
"cancelButton": "Cancel"
|
||||
}
|
||||
},
|
||||
"accountDeleteSuccess": {
|
||||
"label": {
|
||||
"title": "Account Delete Success",
|
||||
"message": "Your account has been deleted. Thank you for using our services.",
|
||||
"backToTopPageLink": "Back to TOP Page"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,7 +356,29 @@
|
||||
},
|
||||
"workflowPage": {
|
||||
"label": {
|
||||
"title": "flujo de trabajo"
|
||||
"title": "flujo de trabajo",
|
||||
"addRoutingRule": "(es)Add Routing Rule",
|
||||
"templateSetting": "(es)Template Setting",
|
||||
"worktypeIdSetting": "(es)WorktypeID Setting",
|
||||
"typistGroupSetting": "(es)Transcriptionist Group Setting",
|
||||
"authorID": "ID de autor",
|
||||
"worktype": "ID de tipo de trabajo",
|
||||
"worktypeOptional": "(es)Worktype ID (Optional)",
|
||||
"transcriptionist": "Transcriptor",
|
||||
"template": "(es)Template",
|
||||
"templateOptional": "(es)Template (Optional)",
|
||||
"editRule": "(es)Edit Rule",
|
||||
"selected": "Transcriptor seleccionado",
|
||||
"pool": "Lista de transcriptor",
|
||||
"selectAuthor": "(es)Select Author ID",
|
||||
"selectWorktypeId": "(es)Select Worktype ID",
|
||||
"selectTemplate": "(es)Select Template"
|
||||
},
|
||||
"message": {
|
||||
"selectedTypistEmptyError": "(es)Transcriptionist,TranscriptionistGroupがいないルーティングルールは保存できません。ルーティング先を1つ以上選択してください。",
|
||||
"workflowConflictError": "(es)指定したAuthorIDとWorktypeIDの組み合わせで既にルーティングルールが登録されています。他の組み合わせで登録してください。",
|
||||
"inputEmptyError": "Campo obligatorio",
|
||||
"saveFailedError": "(es)ルーティングルールの保存に失敗しました。画面を更新し、再度実行してください"
|
||||
}
|
||||
},
|
||||
"typistGroupSetting": {
|
||||
@ -468,5 +490,12 @@
|
||||
"deleteButton": "(es)Delete account",
|
||||
"cancelButton": "(es)Cancel"
|
||||
}
|
||||
},
|
||||
"accountDeleteSuccess": {
|
||||
"label": {
|
||||
"title": "(es)Account Delete Success",
|
||||
"message": "(es)Your account has been deleted. Thank you for using our services.",
|
||||
"backToTopPageLink": "(es)Back to TOP Page"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,7 +356,29 @@
|
||||
},
|
||||
"workflowPage": {
|
||||
"label": {
|
||||
"title": "Flux de travail"
|
||||
"title": "Flux de travail",
|
||||
"addRoutingRule": "(fr)Add Routing Rule",
|
||||
"templateSetting": "(fr)Template Setting",
|
||||
"worktypeIdSetting": "(fr)WorktypeID Setting",
|
||||
"typistGroupSetting": "(fr)Transcriptionist Group Setting",
|
||||
"authorID": "Identifiant Auteur",
|
||||
"worktype": "Identifiant du Type de travail",
|
||||
"worktypeOptional": "(fr)Worktype ID (Optional)",
|
||||
"transcriptionist": "Transcriptionniste",
|
||||
"template": "(fr)Template",
|
||||
"templateOptional": "(fr)Template (Optional)",
|
||||
"editRule": "(fr)Edit Rule",
|
||||
"selected": "Transcriptionniste sélectionné",
|
||||
"pool": "Liste de transcriptionniste",
|
||||
"selectAuthor": "(fr)Select Author ID",
|
||||
"selectWorktypeId": "(fr)Select Worktype ID",
|
||||
"selectTemplate": "(fr)Select Template"
|
||||
},
|
||||
"message": {
|
||||
"selectedTypistEmptyError": "(fr)Transcriptionist,TranscriptionistGroupがいないルーティングルールは保存できません。ルーティング先を1つ以上選択してください。",
|
||||
"workflowConflictError": "(fr)指定したAuthorIDとWorktypeIDの組み合わせで既にルーティングルールが登録されています。他の組み合わせで登録してください。",
|
||||
"inputEmptyError": "Champ obligatoire",
|
||||
"saveFailedError": "(fr)ルーティングルールの保存に失敗しました。画面を更新し、再度実行してください"
|
||||
}
|
||||
},
|
||||
"typistGroupSetting": {
|
||||
@ -468,5 +490,12 @@
|
||||
"deleteButton": "(fr)Delete account",
|
||||
"cancelButton": "(fr)Cancel"
|
||||
}
|
||||
},
|
||||
"accountDeleteSuccess": {
|
||||
"label": {
|
||||
"title": "(fr)Account Delete Success",
|
||||
"message": "(fr)Your account has been deleted. Thank you for using our services.",
|
||||
"backToTopPageLink": "(fr)Back to TOP Page"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
dictation_server:
|
||||
env_file: ../.env
|
||||
build: .
|
||||
working_dir: /app/dictation_server
|
||||
ports:
|
||||
|
||||
@ -1,16 +1,5 @@
|
||||
DB_HOST=omds-mysql
|
||||
DB_PORT=3306
|
||||
DB_EXTERNAL_PORT=3306
|
||||
DB_NAME=omds
|
||||
DB_ROOT_PASS=omdsdbpass
|
||||
DB_USERNAME=omdsdbuser
|
||||
DB_PASSWORD=omdsdbpass
|
||||
NO_COLOR=TRUE
|
||||
ACCESS_TOKEN_LIFETIME_WEB=7200000
|
||||
REFRESH_TOKEN_LIFETIME_WEB=86400000
|
||||
REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000
|
||||
TENANT_NAME=adb2codmsdev
|
||||
SIGNIN_FLOW_NAME=b2c_1_signin_dev
|
||||
EMAIL_CONFIRM_LIFETIME=86400000
|
||||
APP_DOMAIN=https://10.1.0.10:4443/
|
||||
STORAGE_TOKEN_EXPIRE_TIME=2
|
||||
@ -1,15 +1,14 @@
|
||||
STAGE=local
|
||||
NO_COLOR=TRUE
|
||||
CORS=TRUE
|
||||
PORT=8081
|
||||
AZURE_TENANT_ID=xxxxxxxx
|
||||
AZURE_CLIENT_ID=xxxxxxxx
|
||||
AZURE_CLIENT_SECRET=xxxxxxxx
|
||||
# 開発環境ではADB2Cが別テナントになる都合上、環境変数を分けている
|
||||
TENANT_NAME=adb2codmsdev
|
||||
SIGNIN_FLOW_NAME=b2c_1_signin_dev
|
||||
ADB2C_TENANT_ID=xxxxxxxx
|
||||
ADB2C_CLIENT_ID=xxxxxxxx
|
||||
ADB2C_CLIENT_SECRET=xxxxxxxx
|
||||
ADB2C_ORIGIN=https://zzzzzzzzzz
|
||||
KEY_VAULT_NAME=kv-odms-secret-dev
|
||||
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n"
|
||||
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
SENDGRID_API_KEY=xxxxxxxxxxxxxxxx
|
||||
@ -17,6 +16,7 @@ MAIL_FROM=xxxxx@xxxxx.xxxx
|
||||
NOTIFICATION_HUB_NAME=ntf-odms-dev
|
||||
NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX
|
||||
APP_DOMAIN=http://localhost:8081/
|
||||
STORAGE_TOKEN_EXPIRE_TIME=30
|
||||
STORAGE_ACCOUNT_NAME_US=saodmsusdev
|
||||
STORAGE_ACCOUNT_NAME_AU=saodmsaudev
|
||||
STORAGE_ACCOUNT_NAME_EU=saodmseudev
|
||||
@ -25,4 +25,8 @@ STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX
|
||||
STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX
|
||||
STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA
|
||||
STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA
|
||||
STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA
|
||||
STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA
|
||||
ACCESS_TOKEN_LIFETIME_WEB=7200000
|
||||
REFRESH_TOKEN_LIFETIME_WEB=86400000
|
||||
REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000
|
||||
EMAIL_CONFIRM_LIFETIME=86400000
|
||||
44
dictation_server/db/migrations/041_create_workflow.sql
Normal file
@ -0,0 +1,44 @@
|
||||
-- +migrate Up
|
||||
CREATE TABLE IF NOT EXISTS `workflows` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'workflowの内部ID',
|
||||
`account_id` BIGINT UNSIGNED NOT NULL COMMENT 'アカウントID',
|
||||
`author_id` BIGINT UNSIGNED NOT NULL COMMENT 'authorユーザーの内部ID',
|
||||
`worktype_id`BIGINT UNSIGNED COMMENT 'Worktypeの内部ID',
|
||||
`template_id` BIGINT UNSIGNED COMMENT 'テンプレートファイルの内部ID',
|
||||
`created_by` VARCHAR(255) COMMENT '作成者',
|
||||
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
|
||||
`updated_by` VARCHAR(255) COMMENT '更新者',
|
||||
`updated_at` TIMESTAMP DEFAULT now() on UPDATE now() COMMENT '更新時刻',
|
||||
UNIQUE worktype_id_index (account_id, author_id, worktype_id),
|
||||
CONSTRAINT `workflows_fk_account_id` FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
CONSTRAINT `workflows_fk_author_id` FOREIGN KEY (author_id) REFERENCES users(id),
|
||||
CONSTRAINT `workflows_fk_worktype_id` FOREIGN KEY (worktype_id) REFERENCES worktypes(id),
|
||||
CONSTRAINT `workflows_fk_template_id` FOREIGN KEY (template_id) REFERENCES template_files(id)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `workflow_typists` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'worktypeの内部ID',
|
||||
`workflow_id` BIGINT UNSIGNED NOT NULL COMMENT 'workflowの内部ID',
|
||||
`typist_id` BIGINT UNSIGNED COMMENT 'タイピストユーザーの内部ID',
|
||||
`typist_group_id` BIGINT UNSIGNED COMMENT 'タイピストグループの内部ID',
|
||||
`created_by` VARCHAR(255) COMMENT '作成者',
|
||||
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
|
||||
`updated_by` VARCHAR(255) COMMENT '更新者',
|
||||
`updated_at` TIMESTAMP DEFAULT now() on UPDATE now() COMMENT '更新時刻',
|
||||
CONSTRAINT `workflow_typists_fk_workflow_id` FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE,
|
||||
CONSTRAINT `workflow_typists_fk_typist_id` FOREIGN KEY (typist_id) REFERENCES users(id),
|
||||
CONSTRAINT `workflow_typists_fk_typist_group_id` FOREIGN KEY (typist_group_id) REFERENCES user_group(id)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
||||
|
||||
|
||||
|
||||
-- +migrate Down
|
||||
ALTER TABLE `workflows` DROP FOREIGN KEY `workflows_fk_account_id`;
|
||||
ALTER TABLE `workflows` DROP FOREIGN KEY `workflows_fk_author_id`;
|
||||
ALTER TABLE `workflows` DROP FOREIGN KEY `workflows_fk_worktype_id`;
|
||||
ALTER TABLE `workflows` DROP FOREIGN KEY `workflows_fk_template_id`;
|
||||
ALTER TABLE `workflow_typists` DROP FOREIGN KEY `workflow_typists_fk_workflow_id`;
|
||||
ALTER TABLE `workflow_typists` DROP FOREIGN KEY `workflow_typists_fk_typist_id`;
|
||||
ALTER TABLE `workflow_typists` DROP FOREIGN KEY `workflow_typists_fk_typist_group_id`;
|
||||
DROP TABLE IF EXISTS `workflows`;
|
||||
DROP TABLE IF EXISTS `workflow_typists`;
|
||||
@ -0,0 +1,33 @@
|
||||
-- +migrate Up
|
||||
ALTER TABLE `users` ADD CONSTRAINT `users_fk_account_id` FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `sort_criteria` ADD CONSTRAINT `sort_criteria_fk_user_id` FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `license_orders` ADD CONSTRAINT `license_orders_fk_from_account_id` FOREIGN KEY(from_account_id) REFERENCES accounts(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `licenses` ADD CONSTRAINT `licenses_fk_account_id` FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `card_licenses` ADD CONSTRAINT `card_licenses_fk_license_id` FOREIGN KEY(license_id) REFERENCES licenses(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `license_allocation_history` ADD CONSTRAINT `license_allocation_history_fk_account_id` FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `user_group` ADD CONSTRAINT `user_group_fk_account_id` FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `user_group_member` ADD CONSTRAINT `user_group_member_fk_user_group_id` FOREIGN KEY(user_group_id) REFERENCES user_group(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `audio_files` ADD CONSTRAINT `audio_files_fk_account_id` FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `audio_option_items` ADD CONSTRAINT `audio_option_items_fk_audio_file_id` FOREIGN KEY(audio_file_id) REFERENCES audio_files(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `worktypes` ADD CONSTRAINT `worktypes_fk_account_id` FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `option_items` ADD CONSTRAINT `option_items_fk_worktype_id` FOREIGN KEY(worktype_id) REFERENCES worktypes(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `template_files` ADD CONSTRAINT `template_files_fk_account_id` FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `tasks` ADD CONSTRAINT `tasks_fk_account_id` FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE;
|
||||
ALTER TABLE `checkout_permission` ADD CONSTRAINT `checkout_permission_fk_task_id` FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE;
|
||||
|
||||
-- +migrate Down
|
||||
ALTER TABLE `users` DROP FOREIGN KEY `users_fk_account_id`;
|
||||
ALTER TABLE `sort_criteria` DROP FOREIGN KEY `sort_criteria_fk_user_id`;
|
||||
ALTER TABLE `license_orders` DROP FOREIGN KEY `license_orders_fk_from_account_id`;
|
||||
ALTER TABLE `licenses` DROP FOREIGN KEY `licenses_fk_account_id`;
|
||||
ALTER TABLE `card_licenses` DROP FOREIGN KEY `card_licenses_fk_license_id`;
|
||||
ALTER TABLE `license_allocation_history` DROP FOREIGN KEY `license_allocation_history_fk_account_id`;
|
||||
ALTER TABLE `user_group` DROP FOREIGN KEY `user_group_fk_account_id`;
|
||||
ALTER TABLE `user_group_member` DROP FOREIGN KEY `user_group_member_fk_user_group_id`;
|
||||
ALTER TABLE `audio_files` DROP FOREIGN KEY `audio_files_fk_account_id`;
|
||||
ALTER TABLE `audio_option_items` DROP FOREIGN KEY `audio_option_items_fk_audio_file_id`;
|
||||
ALTER TABLE `worktypes` DROP FOREIGN KEY `worktypes_fk_account_id`;
|
||||
ALTER TABLE `option_items` DROP FOREIGN KEY `option_items_fk_worktype_id`;
|
||||
ALTER TABLE `template_files` DROP FOREIGN KEY `template_files_fk_account_id`;
|
||||
ALTER TABLE `tasks` DROP FOREIGN KEY `tasks_fk_account_id`;
|
||||
ALTER TABLE `checkout_permission` DROP FOREIGN KEY `checkout_permission_fk_task_id`;
|
||||
13
dictation_server/db/migrations/043-create_terms.sql
Normal file
@ -0,0 +1,13 @@
|
||||
-- +migrate Up
|
||||
CREATE TABLE IF NOT EXISTS `terms` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT '通番',
|
||||
`document_type` VARCHAR(255) NOT NULL COMMENT '規約種別(EULA/DPA)',
|
||||
`version` VARCHAR(255) NOT NULL COMMENT 'バージョン',
|
||||
`created_by` VARCHAR(255) COMMENT '作成者',
|
||||
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
|
||||
`updated_by` VARCHAR(255) COMMENT '更新者',
|
||||
`updated_at` TIMESTAMP DEFAULT now() on UPDATE now() COMMENT '更新時刻'
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE IF EXISTS `terms`;
|
||||
@ -0,0 +1,13 @@
|
||||
-- +migrate Up
|
||||
ALTER TABLE `users` CHANGE COLUMN `accepted_terms_version` `accepted_eula_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(EULA)',
|
||||
ADD COLUMN `accepted_dpa_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(DPA)' AFTER `accepted_eula_version`;
|
||||
|
||||
ALTER TABLE `users_archive` CHANGE COLUMN `accepted_terms_version` `accepted_eula_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(EULA)',
|
||||
ADD COLUMN `accepted_dpa_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(DPA)' AFTER `accepted_eula_version`;
|
||||
|
||||
-- +migrate Down
|
||||
ALTER TABLE `users` CHANGE COLUMN `accepted_eula_version` `accepted_terms_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(EULA)',
|
||||
DROP COLUMN `accepted_dpa_version`;
|
||||
|
||||
ALTER TABLE `users_archive` CHANGE COLUMN `accepted_eula_version` `accepted_terms_version` VARCHAR(255) COMMENT '同意済み利用規約バージョン(EULA)',
|
||||
DROP COLUMN `accepted_dpa_version`;
|
||||
@ -33,7 +33,7 @@
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"description": "認証エラー/同意済み利用規約が最新でない場合",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
@ -1219,7 +1219,7 @@
|
||||
},
|
||||
"/accounts/delete": {
|
||||
"post": {
|
||||
"operationId": "deleteAccount",
|
||||
"operationId": "deleteAccountAndData",
|
||||
"summary": "",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
@ -1262,6 +1262,52 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/accounts/minimal-access": {
|
||||
"post": {
|
||||
"operationId": "getAccountInfoMinimalAccess",
|
||||
"summary": "",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetAccountInfoMinimalAccessRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetAccountInfoMinimalAccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "対象のユーザーIDが存在しない場合",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["accounts"]
|
||||
}
|
||||
},
|
||||
"/users/confirm": {
|
||||
"post": {
|
||||
"operationId": "confirmUser",
|
||||
@ -1728,6 +1774,53 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users/accepted-version": {
|
||||
"post": {
|
||||
"operationId": "updateAcceptedVersion",
|
||||
"summary": "",
|
||||
"description": "利用規約同意バージョンを更新",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateAcceptedVersionRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateAcceptedVersionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "パラメータ不正/対象のユーザidが存在しない場合",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"]
|
||||
}
|
||||
},
|
||||
"/files/audio/upload-finished": {
|
||||
"post": {
|
||||
"operationId": "uploadFinished",
|
||||
@ -2976,6 +3069,114 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/workflows/{workflowId}": {
|
||||
"post": {
|
||||
"operationId": "updateWorkflow",
|
||||
"summary": "",
|
||||
"description": "アカウント内のワークフローを編集します",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "workflowId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "ワークフローの内部ID",
|
||||
"schema": { "type": "number" }
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/UpdateWorkflowRequest" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateWorkflowResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "パラメータ不正エラー",
|
||||
"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": ["workflows"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/workflows/{workflowId}/delete": {
|
||||
"post": {
|
||||
"operationId": "deleteWorkflow",
|
||||
"summary": "",
|
||||
"description": "アカウント内のワークフローを削除します",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "workflowId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "ワークフローの内部ID",
|
||||
"schema": { "type": "number" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteWorkflowResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["workflows"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/notification/register": {
|
||||
"post": {
|
||||
"operationId": "register",
|
||||
@ -3026,6 +3227,34 @@
|
||||
"tags": ["notification"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/terms": {
|
||||
"post": {
|
||||
"operationId": "getTermsInfo",
|
||||
"summary": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetTermsInfoResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["terms"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
@ -3087,9 +3316,13 @@
|
||||
"adminName": { "type": "string" },
|
||||
"adminMail": { "type": "string" },
|
||||
"adminPassword": { "type": "string" },
|
||||
"acceptedTermsVersion": {
|
||||
"acceptedEulaVersion": {
|
||||
"type": "string",
|
||||
"description": "同意済み利用規約のバージョン"
|
||||
"description": "同意済み利用規約のバージョン(EULA)"
|
||||
},
|
||||
"acceptedDpaVersion": {
|
||||
"type": "string",
|
||||
"description": "同意済み利用規約のバージョン(DPA)"
|
||||
},
|
||||
"token": { "type": "string", "description": "reCAPTCHA Token" }
|
||||
},
|
||||
@ -3099,7 +3332,8 @@
|
||||
"adminName",
|
||||
"adminMail",
|
||||
"adminPassword",
|
||||
"acceptedTermsVersion",
|
||||
"acceptedEulaVersion",
|
||||
"acceptedDpaVersion",
|
||||
"token"
|
||||
]
|
||||
},
|
||||
@ -3597,6 +3831,18 @@
|
||||
},
|
||||
"required": ["accountId"]
|
||||
},
|
||||
"GetAccountInfoMinimalAccessRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"idToken": { "type": "string", "description": "idトークン" }
|
||||
},
|
||||
"required": ["idToken"]
|
||||
},
|
||||
"GetAccountInfoMinimalAccessResponse": {
|
||||
"type": "object",
|
||||
"properties": { "tier": { "type": "number", "description": "階層" } },
|
||||
"required": ["tier"]
|
||||
},
|
||||
"ConfirmRequest": {
|
||||
"type": "object",
|
||||
"properties": { "token": { "type": "string" } },
|
||||
@ -3817,6 +4063,22 @@
|
||||
"required": ["userId"]
|
||||
},
|
||||
"DeallocateLicenseResponse": { "type": "object", "properties": {} },
|
||||
"UpdateAcceptedVersionRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"idToken": { "type": "string", "description": "IDトークン" },
|
||||
"acceptedEULAVersion": {
|
||||
"type": "string",
|
||||
"description": "更新バージョン(EULA)"
|
||||
},
|
||||
"acceptedDPAVersion": {
|
||||
"type": "string",
|
||||
"description": "更新バージョン(DPA)"
|
||||
}
|
||||
},
|
||||
"required": ["idToken", "acceptedEULAVersion"]
|
||||
},
|
||||
"UpdateAcceptedVersionResponse": { "type": "object", "properties": {} },
|
||||
"AudioOptionItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -4251,7 +4513,7 @@
|
||||
"CreateWorkflowsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authorId": { "type": "number", "description": "Authornの内部ID" },
|
||||
"authorId": { "type": "number", "description": "Authorの内部ID" },
|
||||
"worktypeId": { "type": "number", "description": "Worktypeの内部ID" },
|
||||
"templateId": {
|
||||
"type": "number",
|
||||
@ -4267,6 +4529,26 @@
|
||||
"required": ["authorId", "typists"]
|
||||
},
|
||||
"CreateWorkflowsResponse": { "type": "object", "properties": {} },
|
||||
"UpdateWorkflowRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authorId": { "type": "number", "description": "Authorの内部ID" },
|
||||
"worktypeId": { "type": "number", "description": "Worktypeの内部ID" },
|
||||
"templateId": {
|
||||
"type": "number",
|
||||
"description": "テンプレートの内部ID"
|
||||
},
|
||||
"typists": {
|
||||
"description": "ルーティング候補のタイピストユーザー/タイピストグループ",
|
||||
"minItems": 1,
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/WorkflowTypist" }
|
||||
}
|
||||
},
|
||||
"required": ["authorId", "typists"]
|
||||
},
|
||||
"UpdateWorkflowResponse": { "type": "object", "properties": {} },
|
||||
"DeleteWorkflowResponse": { "type": "object", "properties": {} },
|
||||
"RegisterRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -4278,7 +4560,8 @@
|
||||
},
|
||||
"required": ["pns", "handler"]
|
||||
},
|
||||
"RegisterResponse": { "type": "object", "properties": {} }
|
||||
"RegisterResponse": { "type": "object", "properties": {} },
|
||||
"GetTermsInfoResponse": { "type": "object", "properties": {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +48,8 @@ import { WorkflowsModule } from './features/workflows/workflows.module';
|
||||
import { WorkflowsController } from './features/workflows/workflows.controller';
|
||||
import { WorkflowsService } from './features/workflows/workflows.service';
|
||||
import { validate } from './common/validators/env.validator';
|
||||
import { WorkflowsRepositoryModule } from './repositories/workflows/workflows.repository.module';
|
||||
import { TermsModule } from './features/terms/terms.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -86,6 +88,7 @@ import { validate } from './common/validators/env.validator';
|
||||
CheckoutPermissionsRepositoryModule,
|
||||
UserGroupsRepositoryModule,
|
||||
TemplateFilesRepositoryModule,
|
||||
WorkflowsRepositoryModule,
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
@ -106,6 +109,7 @@ import { validate } from './common/validators/env.validator';
|
||||
AuthGuardsModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
WorktypesRepositoryModule,
|
||||
TermsModule,
|
||||
],
|
||||
controllers: [
|
||||
HealthController,
|
||||
|
||||
@ -56,4 +56,7 @@ export const ErrorCodes = [
|
||||
'E011001', // ワークタイプ重複エラー
|
||||
'E011002', // ワークタイプ登録上限超過エラー
|
||||
'E011003', // ワークタイプ不在エラー
|
||||
'E012001', // テンプレートファイル不在エラー
|
||||
'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
|
||||
'E013002', // ワークフロー不在エラー
|
||||
] as const;
|
||||
|
||||
@ -45,4 +45,7 @@ export const errors: Errors = {
|
||||
E011001: 'This WorkTypeID already used Error',
|
||||
E011002: 'WorkTypeID create limit exceeded Error',
|
||||
E011003: 'WorkTypeID not found Error',
|
||||
E012001: 'Template file not found Error',
|
||||
E013001: 'AuthorId and WorktypeId pair already exists Error',
|
||||
E013002: 'Workflow not found Error',
|
||||
};
|
||||
|
||||
@ -29,6 +29,7 @@ export const overrideAdB2cService = <TService>(
|
||||
username: string,
|
||||
) => Promise<{ sub: string } | ConflictError>;
|
||||
deleteUser?: (externalId: string, context: Context) => Promise<void>;
|
||||
deleteUsers?: (externalIds: string[], context: Context) => Promise<void>;
|
||||
getUsers?: (
|
||||
context: Context,
|
||||
externalIds: string[],
|
||||
@ -49,6 +50,12 @@ export const overrideAdB2cService = <TService>(
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.deleteUsers) {
|
||||
Object.defineProperty(obj, obj.deleteUsers.name, {
|
||||
value: overrides.deleteUsers,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.getUsers) {
|
||||
Object.defineProperty(obj, obj.getUsers.name, {
|
||||
value: overrides.getUsers,
|
||||
@ -229,9 +236,11 @@ export const overrideAccountsRepositoryService = <TService>(
|
||||
tier: number,
|
||||
adminExternalUserId: string,
|
||||
adminUserRole: string,
|
||||
adminUserAcceptedTermsVersion: string,
|
||||
adminUserAcceptedEulaVersion: string,
|
||||
adminUserAcceptedDpaVersion: string,
|
||||
) => Promise<{ newAccount: Account; adminUser: User }>;
|
||||
deleteAccount?: (accountId: number, userId: number) => Promise<void>;
|
||||
deleteAccountAndInsertArchives?: (accountId: number) => Promise<User[]>;
|
||||
},
|
||||
): void => {
|
||||
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
|
||||
@ -248,4 +257,10 @@ export const overrideAccountsRepositoryService = <TService>(
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
if (overrides.deleteAccountAndInsertArchives) {
|
||||
Object.defineProperty(obj, obj.deleteAccountAndInsertArchives.name, {
|
||||
value: overrides.deleteAccountAndInsertArchives,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
import { User, UserArchive } from '../../repositories/users/entity/user.entity';
|
||||
import { Account } from '../../repositories/accounts/entity/account.entity';
|
||||
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
|
||||
|
||||
@ -180,7 +180,8 @@ export const makeTestAccount = async (
|
||||
account_id: accountId,
|
||||
role: d?.role ?? 'admin none',
|
||||
author_id: d?.author_id ?? undefined,
|
||||
accepted_terms_version: d?.accepted_terms_version ?? '1.0',
|
||||
accepted_eula_version: d?.accepted_eula_version ?? '1.0',
|
||||
accepted_dpa_version: d?.accepted_dpa_version ?? '1.0',
|
||||
email_verified: d?.email_verified ?? true,
|
||||
auto_renew: d?.auto_renew ?? true,
|
||||
license_alert: d?.license_alert ?? true,
|
||||
@ -282,7 +283,8 @@ export const makeTestUser = async (
|
||||
external_id: d?.external_id ?? uuidv4(),
|
||||
role: d?.role ?? `${ADMIN_ROLES.STANDARD} ${USER_ROLES.NONE}`,
|
||||
author_id: d?.author_id,
|
||||
accepted_terms_version: d?.accepted_terms_version,
|
||||
accepted_eula_version: d?.accepted_eula_version ?? '1.0',
|
||||
accepted_dpa_version: d?.accepted_dpa_version ?? '1.0',
|
||||
email_verified: d?.email_verified ?? true,
|
||||
auto_renew: d?.auto_renew ?? true,
|
||||
license_alert: d?.license_alert ?? true,
|
||||
@ -368,3 +370,14 @@ export const getUser = async (
|
||||
export const getUsers = async (dataSource: DataSource): Promise<User[]> => {
|
||||
return await dataSource.getRepository(User).find();
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: ユーザー退避テーブルの内容を取得する
|
||||
* @param dataSource データソース
|
||||
* @returns ユーザー退避テーブルの内容
|
||||
*/
|
||||
export const getUserArchive = async (
|
||||
dataSource: DataSource,
|
||||
): Promise<UserArchive[]> => {
|
||||
return await dataSource.getRepository(UserArchive).find();
|
||||
};
|
||||
|
||||
@ -20,18 +20,10 @@ export class EnvValidator {
|
||||
@IsNumber()
|
||||
DB_PORT: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
DB_EXTERNAL_PORT: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
DB_NAME: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
DB_ROOT_PASS: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
DB_USERNAME: string;
|
||||
@ -40,21 +32,22 @@ export class EnvValidator {
|
||||
@IsString()
|
||||
DB_PASSWORD: string;
|
||||
|
||||
// .env.local
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
STAGE: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
NO_COLOR: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
ACCESS_TOKEN_LIFETIME_WEB: number;
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
CORS: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
REFRESH_TOKEN_LIFETIME_WEB: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
REFRESH_TOKEN_LIFETIME_DEFAULT: number;
|
||||
PORT: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ -64,43 +57,6 @@ export class EnvValidator {
|
||||
@IsString()
|
||||
SIGNIN_FLOW_NAME: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
EMAIL_CONFIRM_LIFETIME: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
APP_DOMAIN: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
STORAGE_TOKEN_EXPIRE_TIME: number;
|
||||
|
||||
// .env.local
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
STAGE: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
CORS: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
PORT: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
AZURE_TENANT_ID: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
AZURE_CLIENT_ID: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
AZURE_CLIENT_SECRET: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
ADB2C_TENANT_ID: string;
|
||||
@ -113,14 +69,10 @@ export class EnvValidator {
|
||||
@IsString()
|
||||
ADB2C_CLIENT_SECRET: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
ADB2C_ORIGIN: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
KEY_VAULT_NAME: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
JWT_PRIVATE_KEY: string;
|
||||
@ -145,6 +97,14 @@ export class EnvValidator {
|
||||
@IsString()
|
||||
NOTIFICATION_HUB_CONNECT_STRING: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
APP_DOMAIN: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
STORAGE_TOKEN_EXPIRE_TIME: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
STORAGE_ACCOUNT_NAME_US: string;
|
||||
@ -180,6 +140,22 @@ export class EnvValidator {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
STORAGE_ACCOUNT_ENDPOINT_EU: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
ACCESS_TOKEN_LIFETIME_WEB: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
REFRESH_TOKEN_LIFETIME_WEB: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
REFRESH_TOKEN_LIFETIME_DEFAULT: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
EMAIL_CONFIRM_LIFETIME: number;
|
||||
}
|
||||
|
||||
export function validate(config: Record<string, unknown>) {
|
||||
|
||||
@ -246,3 +246,9 @@ export const OPTION_ITEM_VALUE_TYPE = {
|
||||
export const ADB2C_SIGN_IN_TYPE = {
|
||||
EAMILADDRESS: 'emailAddress',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* MANUAL_RECOVERY_REQUIRED
|
||||
* @const {string}
|
||||
*/
|
||||
export const MANUAL_RECOVERY_REQUIRED = '[MANUAL_RECOVERY_REQUIRED]';
|
||||
|
||||
@ -63,6 +63,8 @@ import {
|
||||
DeleteAccountRequest,
|
||||
DeleteAccountResponse,
|
||||
GetAuthorsResponse,
|
||||
GetAccountInfoMinimalAccessRequest,
|
||||
GetAccountInfoMinimalAccessResponse,
|
||||
} from './types/types';
|
||||
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
|
||||
import { AuthGuard } from '../../common/guards/auth/authguards';
|
||||
@ -107,7 +109,8 @@ export class AccountsController {
|
||||
adminMail,
|
||||
adminPassword,
|
||||
adminName,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
} = body;
|
||||
const role = USER_ROLES.NONE;
|
||||
|
||||
@ -122,7 +125,8 @@ export class AccountsController {
|
||||
adminPassword,
|
||||
adminName,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
|
||||
return {};
|
||||
@ -231,9 +235,9 @@ export class AccountsController {
|
||||
const { userId } = jwt.decode(accessToken, { json: true }) as AccessToken;
|
||||
const context = makeContext(userId);
|
||||
|
||||
console.log(context.trackingId);
|
||||
const authors = await this.accountService.getAuthors(context, userId);
|
||||
|
||||
return { authors: [] };
|
||||
return { authors };
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
@ -1071,7 +1075,7 @@ export class AccountsController {
|
||||
description: 'DBアクセスに失敗しログインできる状態で処理が終了した場合',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({ operationId: 'deleteAccount' })
|
||||
@ApiOperation({ operationId: 'deleteAccountAndData' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
@ -1079,7 +1083,7 @@ export class AccountsController {
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
}),
|
||||
)
|
||||
async deleteAccount(
|
||||
async deleteAccountAndData(
|
||||
@Req() req: Request,
|
||||
@Body() body: DeleteAccountRequest,
|
||||
): Promise<DeleteAccountResponse> {
|
||||
@ -1088,12 +1092,35 @@ export class AccountsController {
|
||||
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
|
||||
const context = makeContext(userId);
|
||||
|
||||
/* TODO 仮実装、別タスクで実装する
|
||||
await this.accountService.deleteAccount(
|
||||
context,
|
||||
accountId
|
||||
);
|
||||
*/
|
||||
await this.accountService.deleteAccountAndData(context, userId, accountId);
|
||||
return;
|
||||
}
|
||||
|
||||
@Post('/minimal-access')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: GetAccountInfoMinimalAccessResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: '対象のユーザーIDが存在しない場合',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({ operationId: 'getAccountInfoMinimalAccess' })
|
||||
async getAccountInfoMinimalAccess(
|
||||
@Body() body: GetAccountInfoMinimalAccessRequest,
|
||||
): Promise<GetAccountInfoMinimalAccessResponse> {
|
||||
const context = makeContext(uuidv4());
|
||||
|
||||
// TODO 仮実装。API実装タスクで本実装する。
|
||||
// const idToken = await this.authService.getVerifiedIdToken(body.idToken);
|
||||
// await this.accountService.getAccountInfoMinimalAccess(context, idToken);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,8 @@ import {
|
||||
getUsers,
|
||||
makeTestUser,
|
||||
makeHierarchicalAccounts,
|
||||
getUser,
|
||||
getUserArchive,
|
||||
} from '../../common/test/utility';
|
||||
import { AccountsService } from './accounts.service';
|
||||
import { Context, makeContext } from '../../common/log';
|
||||
@ -58,7 +60,10 @@ import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
|
||||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||||
import { UserGroupsRepositoryService } from '../../repositories/user_groups/user_groups.repository.service';
|
||||
import {
|
||||
createLicenseAllocationHistory,
|
||||
createOrder,
|
||||
getLicenseArchive,
|
||||
getLicenseAllocationHistoryArchive,
|
||||
selectLicense,
|
||||
selectOrderLicense,
|
||||
} from '../licenses/test/utility';
|
||||
@ -66,6 +71,7 @@ import { WorktypesRepositoryService } from '../../repositories/worktypes/worktyp
|
||||
import { AdB2cUser } from '../../gateways/adb2c/types/types';
|
||||
import { Worktype } from '../../repositories/worktypes/entity/worktype.entity';
|
||||
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
|
||||
describe('createAccount', () => {
|
||||
let source: DataSource = null;
|
||||
@ -97,7 +103,8 @@ describe('createAccount', () => {
|
||||
const password = 'dummy_password';
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedTermsVersion = '1.0.0';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
createUser: async () => {
|
||||
@ -128,7 +135,8 @@ describe('createAccount', () => {
|
||||
password,
|
||||
username,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
// 作成したアカウントのIDが返ってくるか確認
|
||||
expect(accountId).toBe(1);
|
||||
@ -144,7 +152,8 @@ describe('createAccount', () => {
|
||||
expect(account.tier).toBe(TIERS.TIER5);
|
||||
expect(account.primary_admin_user_id).toBe(user.id);
|
||||
expect(account.secondary_admin_user_id).toBe(null);
|
||||
expect(user.accepted_terms_version).toBe(acceptedTermsVersion);
|
||||
expect(user.accepted_eula_version).toBe(acceptedEulaVersion);
|
||||
expect(user.accepted_dpa_version).toBe(acceptedDpaVersion);
|
||||
expect(user.account_id).toBe(accountId);
|
||||
expect(user.role).toBe(role);
|
||||
});
|
||||
@ -175,7 +184,8 @@ describe('createAccount', () => {
|
||||
const password = 'dummy_password';
|
||||
const username = 'dummy_username';
|
||||
const role = 'admin none';
|
||||
const acceptedTermsVersion = '1.0.0';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
createUser: async () => {
|
||||
@ -195,7 +205,8 @@ describe('createAccount', () => {
|
||||
password,
|
||||
username,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
@ -240,7 +251,8 @@ describe('createAccount', () => {
|
||||
const password = 'dummy_password';
|
||||
const username = 'dummy_username';
|
||||
const role = 'admin none';
|
||||
const acceptedTermsVersion = '1.0.0';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
createUser: async () => {
|
||||
@ -261,7 +273,8 @@ describe('createAccount', () => {
|
||||
password,
|
||||
username,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
@ -290,7 +303,8 @@ describe('createAccount', () => {
|
||||
const password = 'dummy_password';
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedTermsVersion = '1.0.0';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
createUser: async () => {
|
||||
@ -316,7 +330,8 @@ describe('createAccount', () => {
|
||||
password,
|
||||
username,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
@ -352,7 +367,8 @@ describe('createAccount', () => {
|
||||
const password = 'dummy_password';
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedTermsVersion = '1.0.0';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
createUser: async () => {
|
||||
@ -378,7 +394,8 @@ describe('createAccount', () => {
|
||||
password,
|
||||
username,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
@ -416,7 +433,8 @@ describe('createAccount', () => {
|
||||
const password = 'dummy_password';
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedTermsVersion = '1.0.0';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
createUser: async () => {
|
||||
@ -443,7 +461,8 @@ describe('createAccount', () => {
|
||||
password,
|
||||
username,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
@ -480,7 +499,8 @@ describe('createAccount', () => {
|
||||
const password = 'dummy_password';
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedTermsVersion = '1.0.0';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
createUser: async () => {
|
||||
@ -510,7 +530,8 @@ describe('createAccount', () => {
|
||||
password,
|
||||
username,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
@ -549,7 +570,8 @@ describe('createAccount', () => {
|
||||
const password = 'dummy_password';
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedTermsVersion = '1.0.0';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
createUser: async (
|
||||
@ -596,7 +618,8 @@ describe('createAccount', () => {
|
||||
password,
|
||||
username,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
@ -641,7 +664,8 @@ describe('createAccount', () => {
|
||||
const password = 'dummy_password';
|
||||
const username = 'dummy_username';
|
||||
const role = 'none';
|
||||
const acceptedTermsVersion = '1.0.0';
|
||||
const acceptedEulaVersion = '1.0.0';
|
||||
const acceptedDpaVersion = '1.0.0';
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
createUser: async () => {
|
||||
@ -685,7 +709,8 @@ describe('createAccount', () => {
|
||||
password,
|
||||
username,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
@ -5204,3 +5229,369 @@ describe('getAccountInfo', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('getAuthors', () => {
|
||||
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('アカウント内のAuthorユーザーの一覧を取得できる', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
|
||||
const { id: userId1 } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
author_id: 'AUTHOR_ID_1',
|
||||
});
|
||||
const { id: userId2 } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
author_id: 'AUTHOR_ID_2',
|
||||
});
|
||||
const { id: userId3 } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
// 作成したデータを確認
|
||||
{
|
||||
const users = await getUsers(source);
|
||||
expect(users.length).toBe(4);
|
||||
expect(users[1].id).toBe(userId1);
|
||||
expect(users[2].id).toBe(userId2);
|
||||
expect(users[3].id).toBe(userId3);
|
||||
}
|
||||
|
||||
const service = module.get<AccountsService>(AccountsService);
|
||||
const context = makeContext(admin.external_id);
|
||||
const authors = await service.getAuthors(context, admin.external_id);
|
||||
|
||||
//実行結果を確認
|
||||
{
|
||||
expect(authors.length).toBe(2);
|
||||
expect(authors[0].id).toBe(userId1);
|
||||
expect(authors[0].authorId).toBe('AUTHOR_ID_1');
|
||||
expect(authors[1].id).toBe(userId2);
|
||||
expect(authors[1].authorId).toBe('AUTHOR_ID_2');
|
||||
}
|
||||
});
|
||||
it('アカウント内のAuthorユーザーの一覧を取得できる(0件)', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
// 第五階層のアカウント作成
|
||||
const { admin } = await makeTestAccount(source, { tier: 5 });
|
||||
|
||||
// 作成したデータを確認
|
||||
{
|
||||
const users = await getUsers(source);
|
||||
expect(users.length).toBe(1);
|
||||
}
|
||||
|
||||
const service = module.get<AccountsService>(AccountsService);
|
||||
const context = makeContext(admin.external_id);
|
||||
const authors = await service.getAuthors(context, admin.external_id);
|
||||
|
||||
//実行結果を確認
|
||||
{
|
||||
expect(authors.length).toBe(0);
|
||||
}
|
||||
});
|
||||
it('DBアクセスに失敗した場合、500エラーとなる', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
// 第五階層のアカウント作成
|
||||
const { admin } = await makeTestAccount(source, { tier: 5 });
|
||||
|
||||
const service = module.get<AccountsService>(AccountsService);
|
||||
const context = makeContext(admin.external_id);
|
||||
|
||||
//DBアクセスに失敗するようにする
|
||||
const usersService = module.get<UsersRepositoryService>(
|
||||
UsersRepositoryService,
|
||||
);
|
||||
usersService.findAuthorUsers = jest.fn().mockRejectedValue('DB failed');
|
||||
|
||||
//実行結果を確認
|
||||
try {
|
||||
await service.getAuthors(context, admin.external_id);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('deleteAccountAndData', () => {
|
||||
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 tier4Accounts = await makeHierarchicalAccounts(source);
|
||||
const { account: account1, admin: admin1 } = await makeTestAccount(source, {
|
||||
parent_account_id: tier4Accounts.tier4Accounts[0].account.id,
|
||||
});
|
||||
const account = account1;
|
||||
const admin = admin1;
|
||||
const context = makeContext(admin.external_id);
|
||||
// 第五階層のアカウント作成
|
||||
const tier5Accounts = await makeTestAccount(source, {
|
||||
parent_account_id: account.id,
|
||||
tier: 5,
|
||||
});
|
||||
|
||||
// ユーザの作成
|
||||
const user = await makeTestUser(source, {
|
||||
account_id: tier5Accounts.account.id,
|
||||
});
|
||||
// ライセンス作成
|
||||
await createLicense(
|
||||
source,
|
||||
1,
|
||||
new Date(),
|
||||
tier5Accounts.account.id,
|
||||
LICENSE_TYPE.NORMAL,
|
||||
LICENSE_ALLOCATED_STATUS.UNALLOCATED,
|
||||
null,
|
||||
user.id,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
await createLicenseAllocationHistory(
|
||||
source,
|
||||
1,
|
||||
user.id,
|
||||
1,
|
||||
tier5Accounts.account.id,
|
||||
'NONE',
|
||||
);
|
||||
|
||||
// ADB2Cユーザーの削除成功
|
||||
overrideAdB2cService(service, {
|
||||
deleteUsers: jest.fn(),
|
||||
});
|
||||
// blobstorageコンテナの削除成功
|
||||
overrideBlobstorageService(service, {
|
||||
deleteContainer: jest.fn(),
|
||||
});
|
||||
// アカウント情報の削除
|
||||
await service.deleteAccountAndData(
|
||||
context,
|
||||
tier5Accounts.admin.external_id,
|
||||
tier5Accounts.account.id,
|
||||
);
|
||||
|
||||
// DB内が想定通りになっているか確認
|
||||
const accountRecord = await getAccount(source, tier5Accounts.account.id);
|
||||
expect(accountRecord).toBe(null);
|
||||
|
||||
const userRecord = await getUser(source, user.id);
|
||||
expect(userRecord).toBe(null);
|
||||
|
||||
const UserArchive = await getUserArchive(source);
|
||||
expect(UserArchive.length).toBe(2);
|
||||
|
||||
const LicenseArchive = await getLicenseArchive(source);
|
||||
expect(LicenseArchive.length).toBe(1);
|
||||
|
||||
const LicenseAllocationHistoryArchive =
|
||||
await getLicenseAllocationHistoryArchive(source);
|
||||
expect(LicenseAllocationHistoryArchive.length).toBe(1);
|
||||
});
|
||||
it('アカウントの削除に失敗した場合はエラーを返す', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const service = module.get<AccountsService>(AccountsService);
|
||||
const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation();
|
||||
// 第五階層のアカウント作成
|
||||
const tier4Accounts = await makeHierarchicalAccounts(source);
|
||||
const { account: account1, admin: admin1 } = await makeTestAccount(source, {
|
||||
parent_account_id: tier4Accounts.tier4Accounts[0].account.id,
|
||||
});
|
||||
const account = account1;
|
||||
const admin = admin1;
|
||||
const context = makeContext(admin.external_id);
|
||||
// 第五階層のアカウント作成
|
||||
const tier5Accounts = await makeTestAccount(source, {
|
||||
parent_account_id: account.id,
|
||||
tier: 5,
|
||||
});
|
||||
|
||||
// ユーザの作成
|
||||
const user = await makeTestUser(source, {
|
||||
account_id: tier5Accounts.account.id,
|
||||
});
|
||||
|
||||
// アカウント情報の削除失敗
|
||||
overrideAccountsRepositoryService(service, {
|
||||
deleteAccountAndInsertArchives: jest.fn().mockRejectedValue(new Error()),
|
||||
});
|
||||
|
||||
// ADB2Cユーザーの削除成功
|
||||
overrideAdB2cService(service, {
|
||||
deleteUsers: jest.fn(),
|
||||
});
|
||||
|
||||
// blobstorageコンテナの削除成功
|
||||
overrideBlobstorageService(service, {
|
||||
deleteContainer: jest.fn(),
|
||||
});
|
||||
|
||||
// アカウント情報の削除に失敗することを確認
|
||||
await expect(
|
||||
service.deleteAccountAndData(
|
||||
context,
|
||||
tier5Accounts.admin.external_id,
|
||||
tier5Accounts.account.id,
|
||||
),
|
||||
).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
|
||||
// loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用)
|
||||
const logs = loggerSpy.mock.calls.map((call) => call[0]);
|
||||
console.log(logs);
|
||||
|
||||
// DB内が削除されていないことを確認
|
||||
const accountRecord = await getAccount(source, tier5Accounts.account.id);
|
||||
expect(accountRecord.id).not.toBeNull();
|
||||
const userRecord = await getUser(source, user.id);
|
||||
expect(userRecord.id).not.toBeNull();
|
||||
});
|
||||
it('ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const service = module.get<AccountsService>(AccountsService);
|
||||
const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation();
|
||||
// 第五階層のアカウント作成
|
||||
const tier4Accounts = await makeHierarchicalAccounts(source);
|
||||
const { account: account1, admin: admin1 } = await makeTestAccount(source, {
|
||||
parent_account_id: tier4Accounts.tier4Accounts[0].account.id,
|
||||
});
|
||||
const account = account1;
|
||||
const admin = admin1;
|
||||
const context = makeContext(admin.external_id);
|
||||
// 第五階層のアカウント作成
|
||||
const tier5Accounts = await makeTestAccount(source, {
|
||||
parent_account_id: account.id,
|
||||
tier: 5,
|
||||
});
|
||||
|
||||
// ユーザの作成
|
||||
const user = await makeTestUser(source, {
|
||||
account_id: tier5Accounts.account.id,
|
||||
});
|
||||
|
||||
// ADB2Cユーザーの削除失敗
|
||||
overrideAdB2cService(service, {
|
||||
deleteUsers: jest.fn().mockRejectedValue(new Error()),
|
||||
});
|
||||
|
||||
// blobstorageコンテナの削除成功
|
||||
overrideBlobstorageService(service, {
|
||||
deleteContainer: jest.fn(),
|
||||
});
|
||||
|
||||
// 処理自体は成功することを確認
|
||||
expect(
|
||||
await service.deleteAccountAndData(
|
||||
context,
|
||||
tier5Accounts.admin.external_id,
|
||||
tier5Accounts.account.id,
|
||||
),
|
||||
).toEqual(undefined);
|
||||
|
||||
// loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用)
|
||||
const logs = loggerSpy.mock.calls.map((call) => call[0]);
|
||||
console.log(logs);
|
||||
|
||||
// DB内が想定通りになっているか確認
|
||||
const accountRecord = await getAccount(source, tier5Accounts.account.id);
|
||||
expect(accountRecord).toBe(null);
|
||||
const userRecord = await getUser(source, user.id);
|
||||
expect(userRecord).toBe(null);
|
||||
});
|
||||
it('blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了', async () => {
|
||||
const module = await makeTestingModule(source);
|
||||
const service = module.get<AccountsService>(AccountsService);
|
||||
const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation();
|
||||
|
||||
// 第五階層のアカウント作成
|
||||
const tier4Accounts = await makeHierarchicalAccounts(source);
|
||||
const { account: account1, admin: admin1 } = await makeTestAccount(source, {
|
||||
parent_account_id: tier4Accounts.tier4Accounts[0].account.id,
|
||||
});
|
||||
const account = account1;
|
||||
const admin = admin1;
|
||||
const context = makeContext(admin.external_id);
|
||||
// 第五階層のアカウント作成
|
||||
const tier5Accounts = await makeTestAccount(source, {
|
||||
parent_account_id: account.id,
|
||||
tier: 5,
|
||||
});
|
||||
|
||||
// ユーザの作成
|
||||
const user = await makeTestUser(source, {
|
||||
account_id: tier5Accounts.account.id,
|
||||
});
|
||||
|
||||
// ADB2Cユーザーの削除成功
|
||||
overrideAdB2cService(service, {
|
||||
deleteUsers: jest.fn(),
|
||||
});
|
||||
|
||||
// blobstorageコンテナの削除失敗
|
||||
overrideBlobstorageService(service, {
|
||||
deleteContainer: jest.fn().mockRejectedValue(new Error()),
|
||||
});
|
||||
|
||||
// 処理自体は成功することを確認
|
||||
expect(
|
||||
await service.deleteAccountAndData(
|
||||
context,
|
||||
tier5Accounts.admin.external_id,
|
||||
tier5Accounts.account.id,
|
||||
),
|
||||
).toEqual(undefined);
|
||||
|
||||
// loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用)
|
||||
const logs = loggerSpy.mock.calls.map((call) => call[0]);
|
||||
console.log(logs);
|
||||
|
||||
// DB内が想定通りになっているか確認
|
||||
const accountRecord = await getAccount(source, tier5Accounts.account.id);
|
||||
expect(accountRecord).toBe(null);
|
||||
const userRecord = await getUser(source, user.id);
|
||||
expect(userRecord).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
USER_ROLES,
|
||||
ADB2C_SIGN_IN_TYPE,
|
||||
OPTION_ITEM_VALUE_TYPE,
|
||||
MANUAL_RECOVERY_REQUIRED,
|
||||
} from '../../constants';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import {
|
||||
@ -31,6 +32,7 @@ import {
|
||||
GetOptionItemsResponse,
|
||||
GetPartnersResponse,
|
||||
PostWorktypeOptionItem,
|
||||
Author,
|
||||
} from './types/types';
|
||||
import {
|
||||
DateWithZeroTime,
|
||||
@ -157,14 +159,16 @@ export class AccountsService {
|
||||
password: string,
|
||||
username: string,
|
||||
role: string,
|
||||
acceptedTermsVersion: string,
|
||||
acceptedEulaVersion: string,
|
||||
acceptedDpaVersion: string,
|
||||
): Promise<{ accountId: number; userId: number; externalUserId: string }> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.createAccount.name} | params: { ` +
|
||||
`country: ${country}, ` +
|
||||
`dealerAccountId: ${dealerAccountId}, ` +
|
||||
`role: ${role}, ` +
|
||||
`acceptedTermsVersion: ${acceptedTermsVersion} };`,
|
||||
`acceptedEulaVersion: ${acceptedEulaVersion} }, ` +
|
||||
`acceptedDpaVersion: ${acceptedDpaVersion} };`,
|
||||
);
|
||||
try {
|
||||
let externalUser: { sub: string } | ConflictError;
|
||||
@ -207,7 +211,8 @@ export class AccountsService {
|
||||
TIERS.TIER5,
|
||||
externalUser.sub,
|
||||
role,
|
||||
acceptedTermsVersion,
|
||||
acceptedEulaVersion,
|
||||
acceptedDpaVersion,
|
||||
);
|
||||
account = newAccount;
|
||||
user = adminUser;
|
||||
@ -319,7 +324,7 @@ export class AccountsService {
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -338,7 +343,7 @@ export class AccountsService {
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete account: ${accountId}, user: ${userId}`,
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete account: ${accountId}, user: ${userId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -361,7 +366,7 @@ export class AccountsService {
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete container: ${accountId}, country: ${country}`,
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete container: ${accountId}, country: ${country}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -553,6 +558,64 @@ export class AccountsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウント内のAuthorを取得する
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @returns authors
|
||||
*/
|
||||
async getAuthors(context: Context, externalId: string): Promise<Author[]> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.getAuthors.name} | params: { externalId: ${externalId} };`,
|
||||
);
|
||||
|
||||
try {
|
||||
const { account } = await this.usersRepository.findUserByExternalId(
|
||||
externalId,
|
||||
);
|
||||
|
||||
if (!account) {
|
||||
throw new AccountNotFoundError(
|
||||
`account not found. externalId: ${externalId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const authorUsers = await this.usersRepository.findAuthorUsers(
|
||||
account.id,
|
||||
);
|
||||
|
||||
const authors = authorUsers.map((x) => {
|
||||
return {
|
||||
id: x.id,
|
||||
authorId: x.author_id,
|
||||
};
|
||||
});
|
||||
return authors;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case AccountNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010501'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.getAuthors.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* パートナーを追加する
|
||||
* @param companyName パートナーの会社名
|
||||
@ -638,6 +701,7 @@ export class AccountsService {
|
||||
externalUser.sub,
|
||||
USER_ROLES.NONE,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
account = newAccount;
|
||||
user = adminUser;
|
||||
@ -1684,4 +1748,99 @@ export class AccountsService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウントと紐づくデータを削除する
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param accountId // 削除対象のアカウントID
|
||||
*/
|
||||
async deleteAccountAndData(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
accountId: number,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.deleteAccountAndData.name} | params: { ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`accountId: ${accountId}, };`,
|
||||
);
|
||||
let country: string;
|
||||
let dbUsers: User[];
|
||||
try {
|
||||
// パラメータとトークンから取得したアカウントIDの突き合わせ
|
||||
const { account_id: myAccountId } =
|
||||
await this.usersRepository.findUserByExternalId(externalId);
|
||||
if (myAccountId !== accountId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000108'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
// アカウント削除前に必要な情報を退避する
|
||||
const targetAccount = await this.accountRepository.findAccountById(
|
||||
accountId,
|
||||
);
|
||||
// 削除対象アカウントを削除する
|
||||
dbUsers = await this.accountRepository.deleteAccountAndInsertArchives(
|
||||
accountId,
|
||||
);
|
||||
this.logger.log(`[${context.trackingId}] delete account: ${accountId}`);
|
||||
country = targetAccount.country;
|
||||
} catch (e) {
|
||||
// アカウントの削除に失敗した場合はエラーを返す
|
||||
this.logger.log(`[${context.trackingId}] ${e}`);
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// 削除対象アカウント内のADB2Cユーザーをすべて削除する
|
||||
await this.adB2cService.deleteUsers(
|
||||
dbUsers.map((x) => x.external_id),
|
||||
context,
|
||||
);
|
||||
this.logger.log(
|
||||
`[${
|
||||
context.trackingId
|
||||
}] delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
|
||||
(x) => x.external_id,
|
||||
)}`,
|
||||
);
|
||||
} catch (e) {
|
||||
// ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行
|
||||
this.logger.log(`[${context.trackingId}] ${e}`);
|
||||
this.logger.log(
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${
|
||||
context.trackingId
|
||||
}] Failed to delete ADB2C users: ${accountId}, users_id: ${dbUsers.map(
|
||||
(x) => x.external_id,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// blobstorageコンテナを削除する
|
||||
await this.deleteBlobContainer(accountId, country, context);
|
||||
this.logger.log(
|
||||
`[${context.trackingId}] delete blob container: ${accountId}-${country}`,
|
||||
);
|
||||
} catch (e) {
|
||||
// blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了
|
||||
this.logger.log(`[${context.trackingId}] ${e}`);
|
||||
this.logger.log(
|
||||
`${MANUAL_RECOVERY_REQUIRED}[${context.trackingId}] Failed to delete blob container: ${accountId}, country: ${country}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.deleteAccountAndData.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,7 +345,8 @@ export const makeDefaultAccountsRepositoryMockValue =
|
||||
user.account_id = 1234567890123456;
|
||||
user.role = 'none admin';
|
||||
user.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
|
||||
user.accepted_terms_version = '1.0';
|
||||
user.accepted_eula_version = '1.0';
|
||||
user.accepted_dpa_version = '1.0';
|
||||
user.email_verified = true;
|
||||
user.auto_renew = false;
|
||||
user.license_alert = false;
|
||||
@ -374,7 +375,8 @@ export const makeDefaultUsersRepositoryMockValue =
|
||||
user.account_id = 1234567890123456;
|
||||
user.role = 'none admin';
|
||||
user.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
|
||||
user.accepted_terms_version = '1.0';
|
||||
user.accepted_eula_version = '1.0';
|
||||
user.accepted_dpa_version = '1.0';
|
||||
user.email_verified = true;
|
||||
user.auto_renew = false;
|
||||
user.license_alert = false;
|
||||
@ -422,7 +424,8 @@ export const makeDefaultUserGroupsRepositoryMockValue =
|
||||
user.account_id = 1234567890123456;
|
||||
user.role = 'none admin';
|
||||
user.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
|
||||
user.accepted_terms_version = '1.0';
|
||||
user.accepted_eula_version = '1.0';
|
||||
user.accepted_dpa_version = '1.0';
|
||||
user.email_verified = true;
|
||||
user.auto_renew = false;
|
||||
user.license_alert = false;
|
||||
|
||||
@ -43,8 +43,10 @@ export class CreateAccountRequest {
|
||||
@ApiProperty()
|
||||
@IsAdminPasswordvalid()
|
||||
adminPassword: string;
|
||||
@ApiProperty({ description: '同意済み利用規約のバージョン' })
|
||||
acceptedTermsVersion: string;
|
||||
@ApiProperty({ description: '同意済み利用規約のバージョン(EULA)' })
|
||||
acceptedEulaVersion: string;
|
||||
@ApiProperty({ description: '同意済み利用規約のバージョン(DPA)' })
|
||||
acceptedDpaVersion: string;
|
||||
@ApiProperty({ description: 'reCAPTCHA Token' })
|
||||
token: string;
|
||||
}
|
||||
@ -577,3 +579,13 @@ export class DeleteAccountRequest {
|
||||
}
|
||||
|
||||
export class DeleteAccountResponse {}
|
||||
|
||||
export class GetAccountInfoMinimalAccessRequest {
|
||||
@ApiProperty({ description: 'idトークン' })
|
||||
idToken: string;
|
||||
}
|
||||
|
||||
export class GetAccountInfoMinimalAccessResponse {
|
||||
@ApiProperty({ description: '階層' })
|
||||
tier: number;
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ export class AuthController {
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
description: '認証エラー/同意済み利用規約が最新でない場合',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
|
||||
@ -127,7 +127,8 @@ export const makeDefaultUsersRepositoryMockValue =
|
||||
account_id: 1234567890123456,
|
||||
role: 'none',
|
||||
author_id: '',
|
||||
accepted_terms_version: '1.0',
|
||||
accepted_eula_version: '1.0',
|
||||
accepted_dpa_version: '1.0',
|
||||
email_verified: true,
|
||||
deleted_at: null,
|
||||
created_by: 'test',
|
||||
|
||||
@ -116,7 +116,8 @@ export const makeDefaultUsersRepositoryMockValue =
|
||||
user1.account_id = 1234567890123456;
|
||||
user1.role = 'none';
|
||||
user1.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
|
||||
user1.accepted_terms_version = '1.0';
|
||||
user1.accepted_eula_version = '1.0';
|
||||
user1.accepted_dpa_version = '1.0';
|
||||
user1.email_verified = true;
|
||||
user1.auto_renew = false;
|
||||
user1.license_alert = false;
|
||||
|
||||
@ -5,6 +5,8 @@ import {
|
||||
CardLicenseIssue,
|
||||
LicenseAllocationHistory,
|
||||
LicenseOrder,
|
||||
LicenseArchive,
|
||||
LicenseAllocationHistoryArchive,
|
||||
} from '../../../repositories/licenses/entity/license.entity';
|
||||
|
||||
export const createLicense = async (
|
||||
@ -189,3 +191,25 @@ export const selectOrderLicense = async (
|
||||
});
|
||||
return { orderLicense };
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: ライセンス退避テーブルの内容を取得する
|
||||
* @param dataSource データソース
|
||||
* @returns ライセンス退避テーブルの内容
|
||||
*/
|
||||
export const getLicenseArchive = async (
|
||||
dataSource: DataSource,
|
||||
): Promise<LicenseArchive[]> => {
|
||||
return await dataSource.getRepository(LicenseArchive).find();
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: ライセンス割り当て履歴退避テーブルの内容を取得する
|
||||
* @param dataSource データソース
|
||||
* @returns ライセンス割り当て履歴退避テーブルの内容
|
||||
*/
|
||||
export const getLicenseAllocationHistoryArchive = async (
|
||||
dataSource: DataSource,
|
||||
): Promise<LicenseAllocationHistoryArchive[]> => {
|
||||
return await dataSource.getRepository(LicenseAllocationHistoryArchive).find();
|
||||
};
|
||||
|
||||
@ -78,7 +78,8 @@ export const makeDefaultUsersRepositoryMockValue =
|
||||
user.account_id = 123;
|
||||
user.role = 'none';
|
||||
user.author_id = undefined;
|
||||
user.accepted_terms_version = '1.0';
|
||||
user.accepted_eula_version = '1.0';
|
||||
user.accepted_dpa_version = '1.0';
|
||||
user.email_verified = true;
|
||||
user.auto_renew = false;
|
||||
user.license_alert = false;
|
||||
|
||||
@ -423,7 +423,8 @@ const defaultTasksRepositoryMockValue: {
|
||||
account_id: 1,
|
||||
external_id: 'userId',
|
||||
role: 'typist',
|
||||
accepted_terms_version: '',
|
||||
accepted_eula_version: '',
|
||||
accepted_dpa_version: '',
|
||||
email_verified: true,
|
||||
auto_renew: true,
|
||||
license_alert: true,
|
||||
|
||||
20
dictation_server/src/features/terms/terms.controller.spec.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { TermsController } from './terms.controller';
|
||||
import { TermsService } from './terms.service';
|
||||
|
||||
describe('TermsController', () => {
|
||||
let controller: TermsController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [TermsController],
|
||||
providers: [TermsService],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<TermsController>(TermsController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
40
dictation_server/src/features/terms/terms.controller.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Controller, HttpStatus, Post } from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { TermsService } from '../terms/terms.service';
|
||||
import { ErrorResponse } from '../../common/error/types/types';
|
||||
import { makeContext } from '../../common/log';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { GetTermsInfoResponse, TermInfo } from './types/types';
|
||||
|
||||
@ApiTags('terms')
|
||||
@Controller('terms')
|
||||
export class TermsController {
|
||||
constructor(
|
||||
private readonly termsService: TermsService, //private readonly cryptoService: CryptoService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: GetTermsInfoResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({ operationId: 'getTermsInfo' })
|
||||
async getTermsInfo(): Promise<GetTermsInfoResponse> {
|
||||
const context = makeContext(uuidv4());
|
||||
|
||||
// TODO 仮実装。API実装タスクで本実装する。
|
||||
// const termInfo = await this.termsService.getTermsInfo(context);
|
||||
const termsInfo = [
|
||||
{ documentType: 'EULA', version: '1.0' },
|
||||
{ documentType: 'DPA', version: '1.1' },
|
||||
] as TermInfo[];
|
||||
|
||||
return { termsInfo };
|
||||
}
|
||||
}
|
||||
9
dictation_server/src/features/terms/terms.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TermsController } from './terms.controller';
|
||||
import { TermsService } from './terms.service';
|
||||
|
||||
@Module({
|
||||
controllers: [TermsController],
|
||||
providers: [TermsService]
|
||||
})
|
||||
export class TermsModule {}
|
||||
18
dictation_server/src/features/terms/terms.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { TermsService } from './terms.service';
|
||||
|
||||
describe('TermsService', () => {
|
||||
let service: TermsService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [TermsService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<TermsService>(TermsService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
4
dictation_server/src/features/terms/terms.service.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class TermsService {}
|
||||
12
dictation_server/src/features/terms/types/types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class GetTermsInfoResponse {
|
||||
termsInfo: TermInfo[];
|
||||
}
|
||||
|
||||
export class TermInfo {
|
||||
@ApiProperty({ description: '利用規約種別' })
|
||||
documentType: string;
|
||||
@ApiProperty({ description: 'バージョン' })
|
||||
version: string;
|
||||
}
|
||||
@ -356,7 +356,8 @@ export const makeDefaultUsersRepositoryMockValue =
|
||||
user1.account_id = 1234567890123456;
|
||||
user1.role = 'none';
|
||||
user1.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
|
||||
user1.accepted_terms_version = '1.0';
|
||||
user1.accepted_eula_version = '1.0';
|
||||
user1.accepted_dpa_version = '1.0';
|
||||
user1.email_verified = true;
|
||||
user1.auto_renew = false;
|
||||
user1.license_alert = false;
|
||||
@ -375,7 +376,8 @@ export const makeDefaultUsersRepositoryMockValue =
|
||||
user2.account_id = 1234567890123456;
|
||||
user2.role = 'none';
|
||||
user2.author_id = '551c4077-5b55-a38c-2c55-cd1edd537aa8';
|
||||
user2.accepted_terms_version = '1.0';
|
||||
user2.accepted_eula_version = '1.0';
|
||||
user2.accepted_dpa_version = '1.0';
|
||||
user2.email_verified = true;
|
||||
user2.auto_renew = false;
|
||||
user2.license_alert = false;
|
||||
|
||||
@ -255,3 +255,14 @@ export class DeallocateLicenseRequest {
|
||||
}
|
||||
|
||||
export class DeallocateLicenseResponse {}
|
||||
|
||||
export class UpdateAcceptedVersionRequest {
|
||||
@ApiProperty({ description: 'IDトークン' })
|
||||
idToken: string;
|
||||
@ApiProperty({ description: '更新バージョン(EULA)' })
|
||||
acceptedEULAVersion: string;
|
||||
@ApiProperty({ description: '更新バージョン(DPA)', required: false })
|
||||
acceptedDPAVersion?: string | undefined;
|
||||
}
|
||||
|
||||
export class UpdateAcceptedVersionResponse {}
|
||||
|
||||
@ -37,6 +37,8 @@ import {
|
||||
AllocateLicenseRequest,
|
||||
DeallocateLicenseResponse,
|
||||
DeallocateLicenseRequest,
|
||||
UpdateAcceptedVersionRequest,
|
||||
UpdateAcceptedVersionResponse,
|
||||
} from './types/types';
|
||||
import { UsersService } from './users.service';
|
||||
import jwt from 'jsonwebtoken';
|
||||
@ -469,4 +471,35 @@ export class UsersController {
|
||||
await this.usersService.deallocateLicense(context, body.userId);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: UpdateAcceptedVersionResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'パラメータ不正/対象のユーザidが存在しない場合',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'updateAcceptedVersion',
|
||||
description: '利用規約同意バージョンを更新',
|
||||
})
|
||||
@Post('/accepted-version')
|
||||
async updateAcceptedVersion(
|
||||
@Body() body: UpdateAcceptedVersionRequest,
|
||||
): Promise<UpdateAcceptedVersionResponse> {
|
||||
const context = makeContext(uuidv4());
|
||||
|
||||
// TODO 仮実装。API実装タスクで本実装する。
|
||||
// const idToken = await this.authService.getVerifiedIdToken(body.idToken);
|
||||
// await this.usersService.updateAcceptedVersion(context, idToken);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,7 +188,8 @@ describe('UsersService.confirmUserAndInitPassword', () => {
|
||||
external_id: 'TEST9999',
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_terms_version: 'string',
|
||||
accepted_eula_version: 'string',
|
||||
accepted_dpa_version: 'string',
|
||||
email_verified: false,
|
||||
created_by: 'string;',
|
||||
created_at: new Date(),
|
||||
@ -232,7 +233,8 @@ describe('UsersService.confirmUserAndInitPassword', () => {
|
||||
external_id: 'TEST9999',
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_terms_version: 'string',
|
||||
accepted_eula_version: 'string',
|
||||
accepted_dpa_version: 'string',
|
||||
email_verified: false,
|
||||
created_by: 'string;',
|
||||
created_at: new Date(),
|
||||
@ -272,7 +274,8 @@ describe('UsersService.confirmUserAndInitPassword', () => {
|
||||
external_id: 'TEST9999',
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_terms_version: 'string',
|
||||
accepted_eula_version: 'string',
|
||||
accepted_dpa_version: 'string',
|
||||
email_verified: true,
|
||||
created_by: 'string;',
|
||||
created_at: new Date(),
|
||||
@ -316,7 +319,8 @@ describe('UsersService.confirmUserAndInitPassword', () => {
|
||||
external_id: 'TEST9999',
|
||||
account_id: 1,
|
||||
role: 'None',
|
||||
accepted_terms_version: 'string',
|
||||
accepted_eula_version: 'string',
|
||||
accepted_dpa_version: 'string',
|
||||
email_verified: false,
|
||||
created_by: 'string;',
|
||||
created_at: new Date(),
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
import {
|
||||
ADB2C_SIGN_IN_TYPE,
|
||||
LICENSE_EXPIRATION_THRESHOLD_DAYS,
|
||||
MANUAL_RECOVERY_REQUIRED,
|
||||
USER_LICENSE_STATUS,
|
||||
USER_ROLES,
|
||||
} from '../../constants';
|
||||
@ -300,7 +301,7 @@ export class UsersService {
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete externalUser: ${externalUserId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -313,7 +314,7 @@ export class UsersService {
|
||||
} catch (error) {
|
||||
this.logger.error(`error=${error}`);
|
||||
this.logger.error(
|
||||
`[MANUAL_RECOVERY_REQUIRED] [${context.trackingId}] Failed to delete user: ${userId}`,
|
||||
`${MANUAL_RECOVERY_REQUIRED} [${context.trackingId}] Failed to delete user: ${userId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
94
dictation_server/src/features/workflows/test/utility.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Workflow } from '../../../repositories/workflows/entity/workflow.entity';
|
||||
import { WorkflowTypist } from '../../../repositories/workflows/entity/workflow_typists.entity';
|
||||
|
||||
// Workflowを作成する
|
||||
export const createWorkflow = async (
|
||||
datasource: DataSource,
|
||||
accountId: number,
|
||||
authorId: number,
|
||||
worktypeId?: number | undefined,
|
||||
templateId?: number | undefined,
|
||||
): Promise<Workflow> => {
|
||||
const { identifiers } = await datasource.getRepository(Workflow).insert({
|
||||
account_id: accountId,
|
||||
author_id: authorId,
|
||||
worktype_id: worktypeId ?? undefined,
|
||||
template_id: templateId ?? undefined,
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
const workflow = identifiers.pop() as Workflow;
|
||||
|
||||
return workflow;
|
||||
};
|
||||
|
||||
// Workflow一覧を取得する
|
||||
export const getWorkflows = async (
|
||||
datasource: DataSource,
|
||||
accountId: number,
|
||||
): Promise<Workflow[]> => {
|
||||
return await datasource.getRepository(Workflow).find({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Workflowを取得する
|
||||
export const getWorkflow = async (
|
||||
datasource: DataSource,
|
||||
accountId: number,
|
||||
id: number,
|
||||
): Promise<Workflow> => {
|
||||
return await datasource.getRepository(Workflow).findOne({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Workflowを作成する
|
||||
export const createWorkflowTypist = async (
|
||||
datasource: DataSource,
|
||||
workflowId: number,
|
||||
typistUserId?: number | undefined,
|
||||
typistGroupId?: number | undefined,
|
||||
): Promise<Workflow> => {
|
||||
const { identifiers } = await datasource
|
||||
.getRepository(WorkflowTypist)
|
||||
.insert({
|
||||
workflow_id: workflowId,
|
||||
typist_id: typistUserId ?? undefined,
|
||||
typist_group_id: typistGroupId ?? undefined,
|
||||
created_by: 'test_runner',
|
||||
created_at: new Date(),
|
||||
updated_by: 'updater',
|
||||
updated_at: new Date(),
|
||||
});
|
||||
const workflow = identifiers.pop() as Workflow;
|
||||
|
||||
return workflow;
|
||||
};
|
||||
|
||||
// WorkflowTypist一覧を取得する
|
||||
export const getWorkflowTypists = async (
|
||||
datasource: DataSource,
|
||||
workflowId: number,
|
||||
): Promise<WorkflowTypist[]> => {
|
||||
return await datasource.getRepository(WorkflowTypist).find({
|
||||
where: {
|
||||
workflow_id: workflowId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// WorkflowTypist一覧全件を取得する
|
||||
export const getAllWorkflowTypists = async (
|
||||
datasource: DataSource,
|
||||
): Promise<WorkflowTypist[]> => {
|
||||
return await datasource.getRepository(WorkflowTypist).find();
|
||||
};
|
||||
@ -50,7 +50,7 @@ export class WorkflowTypist {
|
||||
}
|
||||
|
||||
export class CreateWorkflowsRequest {
|
||||
@ApiProperty({ description: 'Authornの内部ID' })
|
||||
@ApiProperty({ description: 'Authorの内部ID' })
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@ -79,3 +79,52 @@ export class CreateWorkflowsRequest {
|
||||
}
|
||||
|
||||
export class CreateWorkflowsResponse {}
|
||||
|
||||
export class UpdateWorkflowRequestParam {
|
||||
@ApiProperty({ description: 'ワークフローの内部ID' })
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
workflowId: number;
|
||||
}
|
||||
|
||||
export class UpdateWorkflowRequest {
|
||||
@ApiProperty({ description: 'Authorの内部ID' })
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
authorId: number;
|
||||
@ApiProperty({ description: 'Worktypeの内部ID', required: false })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
worktypeId?: number | undefined;
|
||||
@ApiProperty({ description: 'テンプレートの内部ID', required: false })
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
templateId?: number | undefined;
|
||||
@ApiProperty({
|
||||
description: 'ルーティング候補のタイピストユーザー/タイピストグループ',
|
||||
type: [WorkflowTypist],
|
||||
minItems: 1,
|
||||
})
|
||||
@Type(() => WorkflowTypist)
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
typists: WorkflowTypist[];
|
||||
}
|
||||
|
||||
export class UpdateWorkflowResponse {}
|
||||
|
||||
export class DeleteWorkflowRequestParam {
|
||||
@ApiProperty({ description: 'ワークフローの内部ID' })
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
workflowId: number;
|
||||
}
|
||||
|
||||
export class DeleteWorkflowResponse {}
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Req,
|
||||
UseGuards,
|
||||
@ -20,6 +21,11 @@ import {
|
||||
GetWorkflowsResponse,
|
||||
CreateWorkflowsRequest,
|
||||
CreateWorkflowsResponse,
|
||||
UpdateWorkflowResponse,
|
||||
UpdateWorkflowRequest,
|
||||
UpdateWorkflowRequestParam,
|
||||
DeleteWorkflowRequestParam,
|
||||
DeleteWorkflowResponse,
|
||||
} from './types/types';
|
||||
import { AuthGuard } from '../../common/guards/auth/authguards';
|
||||
import { RoleGuard } from '../../common/guards/role/roleguards';
|
||||
@ -62,9 +68,10 @@ export class WorkflowsController {
|
||||
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(userId);
|
||||
console.log(context.trackingId);
|
||||
|
||||
return { workflows: [] };
|
||||
const workflows = await this.workflowsService.getWorkflows(context, userId);
|
||||
|
||||
return { workflows };
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
@ -99,14 +106,109 @@ export class WorkflowsController {
|
||||
@Req() req: Request,
|
||||
@Body() body: CreateWorkflowsRequest,
|
||||
): Promise<CreateWorkflowsResponse> {
|
||||
const { authorId } = body;
|
||||
const { authorId, worktypeId, templateId, typists } = body;
|
||||
const token = retrieveAuthorizationToken(req);
|
||||
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(userId);
|
||||
console.log(context.trackingId);
|
||||
console.log(authorId);
|
||||
await this.workflowsService.createWorkflow(
|
||||
context,
|
||||
userId,
|
||||
authorId,
|
||||
worktypeId,
|
||||
templateId,
|
||||
typists,
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: UpdateWorkflowResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'パラメータ不正エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'updateWorkflow',
|
||||
description: 'アカウント内のワークフローを編集します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@Post('/:workflowId')
|
||||
async updateWorkflow(
|
||||
@Req() req: Request,
|
||||
@Param() param: UpdateWorkflowRequestParam,
|
||||
@Body() body: UpdateWorkflowRequest,
|
||||
): Promise<UpdateWorkflowResponse> {
|
||||
const { authorId, worktypeId, templateId, typists } = body;
|
||||
const { workflowId } = param;
|
||||
const token = retrieveAuthorizationToken(req);
|
||||
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(userId);
|
||||
await this.workflowsService.updateWorkflow(
|
||||
context,
|
||||
userId,
|
||||
workflowId,
|
||||
authorId,
|
||||
worktypeId,
|
||||
templateId,
|
||||
typists,
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: DeleteWorkflowResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'deleteWorkflow',
|
||||
description: 'アカウント内のワークフローを削除します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN] }))
|
||||
@Post('/:workflowId/delete')
|
||||
async deleteWorkflow(
|
||||
@Req() req: Request,
|
||||
@Param() param: DeleteWorkflowRequestParam,
|
||||
): Promise<DeleteWorkflowResponse> {
|
||||
const { workflowId } = param;
|
||||
const token = retrieveAuthorizationToken(req);
|
||||
const { userId } = jwt.decode(token, { json: true }) as AccessToken;
|
||||
|
||||
const context = makeContext(userId);
|
||||
console.log(workflowId);
|
||||
console.log(context.trackingId);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
|
||||
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
|
||||
import { WorkflowsController } from './workflows.controller';
|
||||
import { WorkflowsService } from './workflows.service';
|
||||
import { WorkflowsRepositoryModule } from '../../repositories/workflows/workflows.repository.module';
|
||||
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersRepositoryModule],
|
||||
imports: [UsersRepositoryModule, WorkflowsRepositoryModule, AdB2cModule],
|
||||
providers: [WorkflowsService],
|
||||
controllers: [WorkflowsController],
|
||||
})
|
||||
|
||||
2002
dictation_server/src/features/workflows/workflows.service.spec.ts
Normal file
@ -1,7 +1,293 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import { WorkflowsRepositoryService } from '../../repositories/workflows/workflows.repository.service';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
import { Context } from '../../common/log';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import { Workflow, WorkflowTypist } from './types/types';
|
||||
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
|
||||
import { UserNotFoundError } from '../../repositories/users/errors/types';
|
||||
import { TypistGroupNotExistError } from '../../repositories/user_groups/errors/types';
|
||||
import { WorktypeIdNotFoundError } from '../../repositories/worktypes/errors/types';
|
||||
import { TemplateFileNotExistError } from '../../repositories/template_files/errors/types';
|
||||
import {
|
||||
AuthorIdAndWorktypeIdPairAlreadyExistsError,
|
||||
WorkflowIdNotFoundError,
|
||||
} from '../../repositories/workflows/errors/types';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowsService {
|
||||
private readonly logger = new Logger(WorkflowsService.name);
|
||||
constructor() {}
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepositoryService,
|
||||
private readonly workflowsRepository: WorkflowsRepositoryService,
|
||||
private readonly adB2cService: AdB2cService,
|
||||
) {}
|
||||
/**
|
||||
* ワークフロー一覧を取得する
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @returns workflows
|
||||
*/
|
||||
async getWorkflows(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
): Promise<Workflow[]> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.getWorkflows.name} | params: { externalId: ${externalId} };`,
|
||||
);
|
||||
try {
|
||||
const { account_id: accountId } =
|
||||
await this.usersRepository.findUserByExternalId(externalId);
|
||||
|
||||
// DBからワークフロー一覧を取得
|
||||
const workflowRecords = await this.workflowsRepository.getWorkflows(
|
||||
accountId,
|
||||
);
|
||||
|
||||
// ワークフロー一覧からtypistのexternalIdを取得
|
||||
const externalIds = workflowRecords.flatMap((workflow) => {
|
||||
const workflowTypists = workflow.workflowTypists.flatMap(
|
||||
(workflowTypist) => {
|
||||
const { typist } = workflowTypist;
|
||||
return typist ? [typist?.external_id] : [];
|
||||
},
|
||||
);
|
||||
return workflowTypists;
|
||||
});
|
||||
const distinctedExternalIds = [...new Set(externalIds)];
|
||||
|
||||
// ADB2Cからユーザー一覧を取得
|
||||
const adb2cUsers = await this.adB2cService.getUsers(
|
||||
context,
|
||||
distinctedExternalIds,
|
||||
);
|
||||
|
||||
// DBから取得したワークフロー一覧を整形
|
||||
const workflows = workflowRecords.map((workflow) => {
|
||||
const { id, author, worktype, template, workflowTypists } = workflow;
|
||||
|
||||
const authorId = { id: author.id, authorId: author.author_id };
|
||||
const worktypeId = worktype
|
||||
? { id: worktype.id, worktypeId: worktype.custom_worktype_id }
|
||||
: undefined;
|
||||
const templateId = template
|
||||
? { id: template.id, fileName: template.file_name }
|
||||
: undefined;
|
||||
|
||||
// ルーティング候補を整形
|
||||
const typists = workflowTypists.map((workflowTypist) => {
|
||||
const { typist, typistGroup } = workflowTypist;
|
||||
|
||||
// typistがユーザーの場合はADB2Cからユーザー名を取得
|
||||
const typistName = typist
|
||||
? adb2cUsers.find(
|
||||
(adb2cUser) => adb2cUser.id === typist.external_id,
|
||||
).displayName
|
||||
: typistGroup.name;
|
||||
|
||||
return {
|
||||
typistUserId: typist?.id,
|
||||
typistGroupId: typistGroup?.id,
|
||||
typistName,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id,
|
||||
author: authorId,
|
||||
worktype: worktypeId,
|
||||
template: templateId,
|
||||
typists,
|
||||
};
|
||||
});
|
||||
|
||||
return workflows;
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.trackingId}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.getWorkflows.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ワークフローを作成する
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param authorId
|
||||
* @param worktypeId
|
||||
* @param templateId
|
||||
* @param typists
|
||||
* @returns workflow
|
||||
*/
|
||||
async createWorkflow(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
authorId: number,
|
||||
worktypeId?: number | undefined,
|
||||
templateId?: number | undefined,
|
||||
typists?: WorkflowTypist[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.createWorkflow.name} | | params: { ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`authorId: ${authorId}, ` +
|
||||
`worktypeId: ${worktypeId}, ` +
|
||||
`templateId: ${templateId}, ` +
|
||||
`typists: ${JSON.stringify(typists)} };`,
|
||||
);
|
||||
try {
|
||||
const { account_id: accountId } =
|
||||
await this.usersRepository.findUserByExternalId(externalId);
|
||||
|
||||
await this.workflowsRepository.createtWorkflows(
|
||||
accountId,
|
||||
authorId,
|
||||
worktypeId,
|
||||
templateId,
|
||||
typists,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.trackingId}] error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case UserNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010204'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case TypistGroupNotExistError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010908'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case WorktypeIdNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E011003'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case TemplateFileNotExistError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E012001'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case AuthorIdAndWorktypeIdPairAlreadyExistsError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E013001'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.createWorkflow.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* アカウント内のワークフローを更新する
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param workflowId
|
||||
* @param authorId
|
||||
* @param [worktypeId]
|
||||
* @param [templateId]
|
||||
* @param [typists]
|
||||
* @returns workflow
|
||||
*/
|
||||
async updateWorkflow(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
workflowId: number,
|
||||
authorId: number,
|
||||
worktypeId?: number | undefined,
|
||||
templateId?: number | undefined,
|
||||
typists?: WorkflowTypist[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.updateWorkflow.name} | params: { ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`workflowId: ${workflowId}, ` +
|
||||
`authorId: ${authorId}, ` +
|
||||
`worktypeId: ${worktypeId}, ` +
|
||||
`templateId: ${templateId}, ` +
|
||||
`typists: ${JSON.stringify(typists)} };`,
|
||||
);
|
||||
try {
|
||||
const { account_id: accountId } =
|
||||
await this.usersRepository.findUserByExternalId(externalId);
|
||||
|
||||
await this.workflowsRepository.updatetWorkflow(
|
||||
accountId,
|
||||
workflowId,
|
||||
authorId,
|
||||
worktypeId,
|
||||
templateId,
|
||||
typists,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.trackingId}] error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case WorkflowIdNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E013002'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case UserNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010204'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case TypistGroupNotExistError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010908'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case WorktypeIdNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E011003'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case TemplateFileNotExistError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E012001'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case AuthorIdAndWorktypeIdPairAlreadyExistsError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E013001'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.updateWorkflow.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,6 +254,31 @@ export class AdB2cService {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.deleteUser.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Azure AD B2Cからユーザ情報を削除する(複数)
|
||||
* @param externalIds 外部ユーザーID
|
||||
* @param context コンテキスト
|
||||
*/
|
||||
async deleteUsers(externalIds: string[], context: Context): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.deleteUsers.name} | params: { externalIds: ${externalIds} };`,
|
||||
);
|
||||
|
||||
try {
|
||||
// 複数ユーザーを一括削除する方法が不明なため、rate limitの懸念があるのを承知のうえ単一削除の繰り返しで実装
|
||||
// TODO 一括削除する方法が判明したら修正する
|
||||
// https://learn.microsoft.com/en-us/graph/api/user-delete?view=graph-rest-1.0&tabs=javascript#example
|
||||
externalIds.map(
|
||||
async (x) => await this.graphClient.api(`users/${x}`).delete(),
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.deleteUsers.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO [Task2002] 文字列の配列を15要素ずつ区切る(この処理も別タスクで削除予定)
|
||||
|
||||
@ -51,14 +51,9 @@ export class BlobstorageService {
|
||||
this.configService.get('STORAGE_ACCOUNT_ENDPOINT_EU'),
|
||||
this.sharedKeyCredentialEU,
|
||||
);
|
||||
|
||||
const expireTime = Number(
|
||||
this.sasTokenExpireHour = Number(
|
||||
this.configService.get('STORAGE_TOKEN_EXPIRE_TIME'),
|
||||
);
|
||||
if (Number.isNaN(expireTime)) {
|
||||
throw new Error(`STORAGE_TOKEN_EXPIRE_TIME is invalid value NaN`);
|
||||
}
|
||||
this.sasTokenExpireHour = expireTime;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -10,9 +10,15 @@ import {
|
||||
UpdateResult,
|
||||
EntityManager,
|
||||
} from 'typeorm';
|
||||
import { User } from '../users/entity/user.entity';
|
||||
import { User, UserArchive } from '../users/entity/user.entity';
|
||||
import { Account } from './entity/account.entity';
|
||||
import { License, LicenseOrder } from '../licenses/entity/license.entity';
|
||||
import {
|
||||
License,
|
||||
LicenseAllocationHistory,
|
||||
LicenseAllocationHistoryArchive,
|
||||
LicenseArchive,
|
||||
LicenseOrder,
|
||||
} from '../licenses/entity/license.entity';
|
||||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||||
import {
|
||||
getDirection,
|
||||
@ -100,7 +106,8 @@ export class AccountsRepositoryService {
|
||||
* @param tier
|
||||
* @param adminExternalUserId
|
||||
* @param adminUserRole
|
||||
* @param adminUserAcceptedTermsVersion
|
||||
* @param adminUserAcceptedEulaVersion
|
||||
* @param adminUserAcceptedDpaVersion
|
||||
* @returns account/admin user
|
||||
*/
|
||||
async createAccount(
|
||||
@ -110,7 +117,8 @@ export class AccountsRepositoryService {
|
||||
tier: number,
|
||||
adminExternalUserId: string,
|
||||
adminUserRole: string,
|
||||
adminUserAcceptedTermsVersion: string,
|
||||
adminUserAcceptedEulaVersion: string,
|
||||
adminUserAcceptedDpaVersion: string,
|
||||
): Promise<{ newAccount: Account; adminUser: User }> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const account = new Account();
|
||||
@ -130,7 +138,8 @@ export class AccountsRepositoryService {
|
||||
user.account_id = persistedAccount.id;
|
||||
user.external_id = adminExternalUserId;
|
||||
user.role = adminUserRole;
|
||||
user.accepted_terms_version = adminUserAcceptedTermsVersion;
|
||||
user.accepted_eula_version = adminUserAcceptedEulaVersion;
|
||||
user.accepted_dpa_version = adminUserAcceptedDpaVersion;
|
||||
}
|
||||
const usersRepo = entityManager.getRepository(User);
|
||||
const newUser = usersRepo.create(user);
|
||||
@ -902,4 +911,65 @@ export class AccountsRepositoryService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定されたアカウントを削除する
|
||||
* @param accountId
|
||||
* @returns users 削除対象のユーザー
|
||||
*/
|
||||
async deleteAccountAndInsertArchives(accountId: number): Promise<User[]> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
// 削除対象のユーザーを退避テーブルに退避
|
||||
const users = await this.dataSource.getRepository(User).find({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
},
|
||||
});
|
||||
const userArchiveRepo = entityManager.getRepository(UserArchive);
|
||||
await userArchiveRepo
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(UserArchive)
|
||||
.values(users)
|
||||
.execute();
|
||||
|
||||
// 削除対象のライセンスを退避テーブルに退避
|
||||
const licenses = await this.dataSource.getRepository(License).find({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
},
|
||||
});
|
||||
const licenseArchiveRepo = entityManager.getRepository(LicenseArchive);
|
||||
await licenseArchiveRepo
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(LicenseArchive)
|
||||
.values(licenses)
|
||||
.execute();
|
||||
|
||||
// 削除対象のライセンス割り当て履歴を退避テーブルに退避
|
||||
const licenseHistories = await this.dataSource
|
||||
.getRepository(LicenseAllocationHistory)
|
||||
.find({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
},
|
||||
});
|
||||
const licenseHistoryArchiveRepo = entityManager.getRepository(
|
||||
LicenseAllocationHistoryArchive,
|
||||
);
|
||||
await licenseHistoryArchiveRepo
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(LicenseAllocationHistoryArchive)
|
||||
.values(licenseHistories)
|
||||
.execute();
|
||||
|
||||
// アカウントを削除
|
||||
// アカウントを削除することで、外部キー制約がで紐づいている関連テーブルのデータも削除される
|
||||
const accountRepo = entityManager.getRepository(Account);
|
||||
await accountRepo.delete({ id: accountId });
|
||||
return users;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../users/entity/user.entity';
|
||||
|
||||
@ -188,3 +189,90 @@ export class LicenseAllocationHistory {
|
||||
@JoinColumn({ name: 'license_id' })
|
||||
license?: License;
|
||||
}
|
||||
|
||||
@Entity({ name: 'licenses_archive' })
|
||||
export class LicenseArchive {
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
expiry_date: Date;
|
||||
|
||||
@Column()
|
||||
account_id: number;
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column()
|
||||
status: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
allocated_user_id: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
order_id: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
deleted_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
delete_order_id: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
created_by: string;
|
||||
|
||||
@Column()
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
updated_by: string;
|
||||
|
||||
@Column()
|
||||
updated_at: Date;
|
||||
|
||||
@CreateDateColumn()
|
||||
archived_at: Date;
|
||||
}
|
||||
|
||||
@Entity({ name: 'license_allocation_history_archive' })
|
||||
export class LicenseAllocationHistoryArchive {
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
user_id: number;
|
||||
|
||||
@Column()
|
||||
license_id: number;
|
||||
|
||||
@Column()
|
||||
is_allocated: boolean;
|
||||
|
||||
@Column()
|
||||
account_id: number;
|
||||
|
||||
@Column()
|
||||
executed_at: Date;
|
||||
|
||||
@Column()
|
||||
switch_from_type: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
deleted_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
created_by: string;
|
||||
|
||||
@Column()
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
updated_by: string;
|
||||
|
||||
@Column()
|
||||
updated_at: Date;
|
||||
|
||||
@CreateDateColumn()
|
||||
archived_at: Date;
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@ import {
|
||||
License,
|
||||
LicenseOrder,
|
||||
LicenseAllocationHistory,
|
||||
LicenseArchive,
|
||||
LicenseAllocationHistoryArchive,
|
||||
} from './entity/license.entity';
|
||||
import { LicensesRepositoryService } from './licenses.repository.service';
|
||||
|
||||
@ -17,6 +19,8 @@ import { LicensesRepositoryService } from './licenses.repository.service';
|
||||
CardLicense,
|
||||
CardLicenseIssue,
|
||||
LicenseAllocationHistory,
|
||||
LicenseArchive,
|
||||
LicenseAllocationHistoryArchive,
|
||||
]),
|
||||
],
|
||||
providers: [LicensesRepositoryService],
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
// テンプレートファイルが存在しないエラー
|
||||
export class TemplateFileNotExistError extends Error {}
|
||||
@ -9,6 +9,7 @@ import {
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
OneToMany,
|
||||
PrimaryColumn,
|
||||
} from 'typeorm';
|
||||
import { License } from '../../licenses/entity/license.entity';
|
||||
import { UserGroupMember } from '../../user_groups/entity/user_group_member.entity';
|
||||
@ -31,7 +32,10 @@ export class User {
|
||||
author_id?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
accepted_terms_version?: string;
|
||||
accepted_eula_version?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
accepted_dpa_version?: string;
|
||||
|
||||
@Column({ default: false })
|
||||
email_verified: boolean;
|
||||
@ -69,7 +73,7 @@ export class User {
|
||||
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date;
|
||||
|
||||
@ManyToOne(() => Account, (account) => account.user)
|
||||
@ManyToOne(() => Account, (account) => account.user, { onDelete: 'CASCADE' }) // onDeleteはSQLite用設定値.本番用は別途migrationで設定
|
||||
@JoinColumn({ name: 'account_id' })
|
||||
account?: Account;
|
||||
|
||||
@ -80,6 +84,66 @@ export class User {
|
||||
userGroupMembers?: UserGroupMember[];
|
||||
}
|
||||
|
||||
@Entity({ name: 'users_archive' })
|
||||
export class UserArchive {
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
external_id: string;
|
||||
|
||||
@Column()
|
||||
account_id: number;
|
||||
|
||||
@Column()
|
||||
role: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
author_id?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
accepted_eula_version?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
accepted_dpa_version?: string;
|
||||
|
||||
@Column()
|
||||
email_verified: boolean;
|
||||
|
||||
@Column()
|
||||
auto_renew: boolean;
|
||||
|
||||
@Column()
|
||||
license_alert: boolean;
|
||||
|
||||
@Column()
|
||||
notification: boolean;
|
||||
|
||||
@Column()
|
||||
encryption: boolean;
|
||||
|
||||
@Column()
|
||||
prompt: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
deleted_at?: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
created_by: string;
|
||||
|
||||
@Column()
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
updated_by?: string;
|
||||
|
||||
@Column()
|
||||
updated_at: Date;
|
||||
|
||||
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
archived_at: Date;
|
||||
}
|
||||
|
||||
export type newUser = Omit<
|
||||
User,
|
||||
| 'id'
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './entity/user.entity';
|
||||
import { User, UserArchive } from './entity/user.entity';
|
||||
import { UsersRepositoryService } from './users.repository.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
imports: [TypeOrmModule.forFeature([User, UserArchive])],
|
||||
providers: [UsersRepositoryService],
|
||||
exports: [UsersRepositoryService],
|
||||
})
|
||||
|
||||
@ -40,7 +40,8 @@ export class UsersRepositoryService {
|
||||
license_alert,
|
||||
notification,
|
||||
author_id,
|
||||
accepted_terms_version,
|
||||
accepted_eula_version,
|
||||
accepted_dpa_version,
|
||||
encryption,
|
||||
encryption_password: encryptionPassword,
|
||||
prompt,
|
||||
@ -54,7 +55,8 @@ export class UsersRepositoryService {
|
||||
userEntity.license_alert = license_alert;
|
||||
userEntity.notification = notification;
|
||||
userEntity.author_id = author_id;
|
||||
userEntity.accepted_terms_version = accepted_terms_version;
|
||||
userEntity.accepted_eula_version = accepted_eula_version;
|
||||
userEntity.accepted_dpa_version = accepted_dpa_version;
|
||||
userEntity.encryption = encryption;
|
||||
userEntity.encryption_password = encryptionPassword;
|
||||
userEntity.prompt = prompt;
|
||||
@ -375,6 +377,25 @@ export class UsersRepositoryService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウント内のAuthorユーザーを取得する
|
||||
* @param accountId
|
||||
* @returns author users
|
||||
*/
|
||||
async findAuthorUsers(accountId: number): Promise<User[]> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const repo = entityManager.getRepository(User);
|
||||
const authors = await repo.find({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
deleted_at: IsNull(),
|
||||
},
|
||||
});
|
||||
return authors;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UserID指定のユーザーとソート条件を同時に削除する
|
||||
* @param userId
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
} from 'typeorm';
|
||||
import { WorkflowTypist } from './workflow_typists.entity';
|
||||
import { Worktype } from '../../worktypes/entity/worktype.entity';
|
||||
import { TemplateFile } from '../../template_files/entity/template_file.entity';
|
||||
import { User } from '../../users/entity/user.entity';
|
||||
|
||||
@Entity({ name: 'workflows' })
|
||||
export class Workflow {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
account_id: number;
|
||||
|
||||
@Column()
|
||||
author_id: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
worktype_id?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
template_id?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
created_by: string;
|
||||
|
||||
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
updated_by?: string;
|
||||
|
||||
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.id)
|
||||
@JoinColumn({ name: 'author_id' })
|
||||
author?: User;
|
||||
|
||||
@ManyToOne(() => Worktype, (worktype) => worktype.id)
|
||||
@JoinColumn({ name: 'worktype_id' })
|
||||
worktype?: Worktype;
|
||||
|
||||
@ManyToOne(() => TemplateFile, (templateFile) => templateFile.id)
|
||||
@JoinColumn({ name: 'template_id' })
|
||||
template?: TemplateFile;
|
||||
|
||||
@OneToMany(() => WorkflowTypist, (workflowTypist) => workflowTypist.workflow)
|
||||
workflowTypists?: WorkflowTypist[];
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Workflow } from './workflow.entity';
|
||||
import { User } from '../../users/entity/user.entity';
|
||||
import { UserGroup } from '../../user_groups/entity/user_group.entity';
|
||||
|
||||
@Entity({ name: 'workflow_typists' })
|
||||
export class WorkflowTypist {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
workflow_id: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
typist_id?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
typist_group_id?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
created_by: string;
|
||||
|
||||
@CreateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
updated_by?: string;
|
||||
|
||||
@UpdateDateColumn({ default: () => "datetime('now', 'localtime')" }) // defaultはSQLite用設定値.本番用は別途migrationで設定
|
||||
updated_at: Date;
|
||||
|
||||
@ManyToOne(() => Workflow, (workflow) => workflow.id)
|
||||
@JoinColumn({ name: 'workflow_id' })
|
||||
workflow?: Workflow;
|
||||
|
||||
@ManyToOne(() => User, (user) => user.id)
|
||||
@JoinColumn({ name: 'typist_id' })
|
||||
typist?: User;
|
||||
|
||||
@ManyToOne(() => UserGroup, (userGroup) => userGroup.id)
|
||||
@JoinColumn({ name: 'typist_group_id' })
|
||||
typistGroup?: UserGroup;
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
// AuthorIDとWorktypeIDのペア重複エラー
|
||||
export class AuthorIdAndWorktypeIdPairAlreadyExistsError extends Error {}
|
||||
// WorkflowID存在エラー
|
||||
export class WorkflowIdNotFoundError extends Error {}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WorkflowTypist } from './entity/workflow_typists.entity';
|
||||
import { Workflow } from './entity/workflow.entity';
|
||||
import { WorkflowsRepositoryService } from './workflows.repository.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Workflow, WorkflowTypist])],
|
||||
providers: [WorkflowsRepositoryService],
|
||||
exports: [WorkflowsRepositoryService],
|
||||
})
|
||||
export class WorkflowsRepositoryModule {}
|
||||
@ -0,0 +1,345 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, IsNull } from 'typeorm';
|
||||
import { Workflow } from './entity/workflow.entity';
|
||||
import { WorkflowTypist as DbWorkflowTypist } from './entity/workflow_typists.entity';
|
||||
import { User } from '../users/entity/user.entity';
|
||||
import { WorkflowTypist } from '../../features/workflows/types/types';
|
||||
import { Worktype } from '../worktypes/entity/worktype.entity';
|
||||
import { TemplateFile } from '../template_files/entity/template_file.entity';
|
||||
import { UserGroup } from '../user_groups/entity/user_group.entity';
|
||||
import { TypistGroupNotExistError } from '../user_groups/errors/types';
|
||||
import { UserNotFoundError } from '../users/errors/types';
|
||||
import { WorktypeIdNotFoundError } from '../worktypes/errors/types';
|
||||
import { TemplateFileNotExistError } from '../template_files/errors/types';
|
||||
import {
|
||||
AuthorIdAndWorktypeIdPairAlreadyExistsError,
|
||||
WorkflowIdNotFoundError,
|
||||
} from './errors/types';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowsRepositoryService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
/**
|
||||
* ワークフロー一を取得する
|
||||
* @param externalId
|
||||
* @returns worktypes and active worktype id
|
||||
*/
|
||||
async getWorkflows(accountId: number): Promise<Workflow[]> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const workflowRepo = entityManager.getRepository(Workflow);
|
||||
|
||||
const workflows = await workflowRepo.find({
|
||||
where: { account_id: accountId },
|
||||
relations: {
|
||||
author: true,
|
||||
worktype: true,
|
||||
template: true,
|
||||
workflowTypists: {
|
||||
typist: true,
|
||||
typistGroup: true,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
id: 'ASC',
|
||||
},
|
||||
});
|
||||
|
||||
return workflows;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ワークフローを作成する
|
||||
* @param accountId
|
||||
* @param authorId
|
||||
* @param worktypeId
|
||||
* @param templateId
|
||||
* @param typists
|
||||
* @returns workflows
|
||||
*/
|
||||
async createtWorkflows(
|
||||
accountId: number,
|
||||
authorId: number,
|
||||
worktypeId?: number | undefined,
|
||||
templateId?: number | undefined,
|
||||
typists?: WorkflowTypist[],
|
||||
): Promise<void> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
// authorの存在確認
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const author = await userRepo.findOne({
|
||||
where: { account_id: accountId, id: authorId },
|
||||
});
|
||||
if (!author) {
|
||||
throw new UserNotFoundError(`author not found. id: ${authorId}`);
|
||||
}
|
||||
|
||||
// worktypeの存在確認
|
||||
if (worktypeId !== undefined) {
|
||||
const worktypeRepo = entityManager.getRepository(Worktype);
|
||||
const worktypes = await worktypeRepo.find({
|
||||
where: { account_id: accountId, id: worktypeId },
|
||||
});
|
||||
if (worktypes.length === 0) {
|
||||
throw new WorktypeIdNotFoundError(
|
||||
`worktype not found. id: ${worktypeId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// templateの存在確認
|
||||
if (templateId !== undefined) {
|
||||
const templateRepo = entityManager.getRepository(TemplateFile);
|
||||
const template = await templateRepo.findOne({
|
||||
where: { account_id: accountId, id: templateId },
|
||||
});
|
||||
if (!template) {
|
||||
throw new TemplateFileNotExistError('template not found');
|
||||
}
|
||||
}
|
||||
|
||||
// ルーティング候補ユーザーの存在確認
|
||||
const typistIds = typists.flatMap((typist) =>
|
||||
typist.typistId ? [typist.typistId] : [],
|
||||
);
|
||||
const typistUsers = await userRepo.find({
|
||||
where: { account_id: accountId, id: In(typistIds) },
|
||||
});
|
||||
if (typistUsers.length !== typistIds.length) {
|
||||
throw new UserNotFoundError(`typist not found. ids: ${typistIds}`);
|
||||
}
|
||||
|
||||
// ルーティング候補ユーザーグループの存在確認
|
||||
const groupIds = typists.flatMap((typist) => {
|
||||
return typist.typistGroupId ? [typist.typistGroupId] : [];
|
||||
});
|
||||
const userGroupRepo = entityManager.getRepository(UserGroup);
|
||||
const typistGroups = await userGroupRepo.find({
|
||||
where: { account_id: accountId, id: In(groupIds) },
|
||||
});
|
||||
if (typistGroups.length !== groupIds.length) {
|
||||
throw new TypistGroupNotExistError(
|
||||
`typist group not found. ids: ${groupIds}`,
|
||||
);
|
||||
}
|
||||
|
||||
const workflowRepo = entityManager.getRepository(Workflow);
|
||||
|
||||
// ワークフローの重複確認
|
||||
const workflow = await workflowRepo.find({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
author_id: authorId,
|
||||
worktype_id: worktypeId !== undefined ? worktypeId : IsNull(),
|
||||
},
|
||||
});
|
||||
if (workflow.length !== 0) {
|
||||
throw new AuthorIdAndWorktypeIdPairAlreadyExistsError(
|
||||
'workflow already exists',
|
||||
);
|
||||
}
|
||||
|
||||
// ワークフローのデータ作成
|
||||
const newWorkflow = this.makeWorkflow(
|
||||
accountId,
|
||||
authorId,
|
||||
worktypeId,
|
||||
templateId,
|
||||
);
|
||||
|
||||
await workflowRepo.save(newWorkflow);
|
||||
|
||||
// ルーティング候補のデータ作成
|
||||
const workflowTypists = typists.map((typist) =>
|
||||
this.makeWorkflowTypist(
|
||||
newWorkflow.id,
|
||||
typist.typistId,
|
||||
typist.typistGroupId,
|
||||
),
|
||||
);
|
||||
|
||||
const workflowTypistsRepo = entityManager.getRepository(DbWorkflowTypist);
|
||||
await workflowTypistsRepo.save(workflowTypists);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ワークフローを更新する
|
||||
* @param accountId
|
||||
* @param workflowId
|
||||
* @param authorId
|
||||
* @param [worktypeId]
|
||||
* @param [templateId]
|
||||
* @param [typists]
|
||||
* @returns workflow
|
||||
*/
|
||||
async updatetWorkflow(
|
||||
accountId: number,
|
||||
workflowId: number,
|
||||
authorId: number,
|
||||
worktypeId?: number | undefined,
|
||||
templateId?: number | undefined,
|
||||
typists?: WorkflowTypist[],
|
||||
): Promise<void> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const workflowRepo = entityManager.getRepository(Workflow);
|
||||
|
||||
// ワークフローの存在確認
|
||||
const targetWorkflow = await workflowRepo.findOne({
|
||||
where: { account_id: accountId, id: workflowId },
|
||||
});
|
||||
if (!targetWorkflow) {
|
||||
throw new WorkflowIdNotFoundError(
|
||||
`workflow not found. id: ${workflowId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// authorの存在確認
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const author = await userRepo.findOne({
|
||||
where: { account_id: accountId, id: authorId },
|
||||
});
|
||||
if (!author) {
|
||||
throw new UserNotFoundError(`author not found. id: ${authorId}`);
|
||||
}
|
||||
|
||||
// worktypeの存在確認
|
||||
if (worktypeId !== undefined) {
|
||||
const worktypeRepo = entityManager.getRepository(Worktype);
|
||||
const worktypes = await worktypeRepo.find({
|
||||
where: { account_id: accountId, id: worktypeId },
|
||||
});
|
||||
if (worktypes.length === 0) {
|
||||
throw new WorktypeIdNotFoundError(
|
||||
`worktype not found. id: ${worktypeId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// templateの存在確認
|
||||
if (templateId !== undefined) {
|
||||
const templateRepo = entityManager.getRepository(TemplateFile);
|
||||
const template = await templateRepo.findOne({
|
||||
where: { account_id: accountId, id: templateId },
|
||||
});
|
||||
if (!template) {
|
||||
throw new TemplateFileNotExistError(
|
||||
`template not found. id: ${templateId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ルーティング候補ユーザーの存在確認
|
||||
const typistIds = typists.flatMap((typist) =>
|
||||
typist.typistId ? [typist.typistId] : [],
|
||||
);
|
||||
const typistUsers = await userRepo.find({
|
||||
where: { account_id: accountId, id: In(typistIds) },
|
||||
});
|
||||
if (typistUsers.length !== typistIds.length) {
|
||||
throw new UserNotFoundError(`typist not found. ids: ${typistIds}`);
|
||||
}
|
||||
|
||||
// ルーティング候補ユーザーグループの存在確認
|
||||
const groupIds = typists.flatMap((typist) => {
|
||||
return typist.typistGroupId ? [typist.typistGroupId] : [];
|
||||
});
|
||||
const userGroupRepo = entityManager.getRepository(UserGroup);
|
||||
const typistGroups = await userGroupRepo.find({
|
||||
where: { account_id: accountId, id: In(groupIds) },
|
||||
});
|
||||
if (typistGroups.length !== groupIds.length) {
|
||||
throw new TypistGroupNotExistError(
|
||||
`typist group not found. ids: ${groupIds}`,
|
||||
);
|
||||
}
|
||||
|
||||
const workflowTypistsRepo = entityManager.getRepository(DbWorkflowTypist);
|
||||
|
||||
// 既存データの削除
|
||||
await workflowTypistsRepo.delete({ workflow_id: workflowId });
|
||||
await workflowRepo.delete(workflowId);
|
||||
|
||||
{
|
||||
// ワークフローの重複確認
|
||||
const duplicateWorkflow = await workflowRepo.find({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
author_id: authorId,
|
||||
worktype_id: worktypeId !== undefined ? worktypeId : IsNull(),
|
||||
},
|
||||
});
|
||||
if (duplicateWorkflow.length !== 0) {
|
||||
throw new AuthorIdAndWorktypeIdPairAlreadyExistsError(
|
||||
'workflow already exists',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ワークフローのデータ作成
|
||||
const newWorkflow = this.makeWorkflow(
|
||||
accountId,
|
||||
authorId,
|
||||
worktypeId,
|
||||
templateId,
|
||||
);
|
||||
|
||||
await workflowRepo.save(newWorkflow);
|
||||
|
||||
// ルーティング候補のデータ作成
|
||||
const workflowTypists = typists.map((typist) =>
|
||||
this.makeWorkflowTypist(
|
||||
newWorkflow.id,
|
||||
typist.typistId,
|
||||
typist.typistGroupId,
|
||||
),
|
||||
);
|
||||
|
||||
await workflowTypistsRepo.save(workflowTypists);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DBに保存するワークフローデータを作成する
|
||||
* @param accountId
|
||||
* @param authorId
|
||||
* @param worktypeId
|
||||
* @param templateId
|
||||
* @returns workflow
|
||||
*/
|
||||
private makeWorkflow(
|
||||
accountId: number,
|
||||
authorId: number,
|
||||
worktypeId?: number | undefined,
|
||||
templateId?: number | undefined,
|
||||
): Workflow {
|
||||
const workflow = new Workflow();
|
||||
workflow.account_id = accountId;
|
||||
workflow.author_id = authorId;
|
||||
workflow.worktype_id = worktypeId;
|
||||
workflow.template_id = templateId;
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* DBに保存するルーティング候補データを作成する
|
||||
* @param workflowId
|
||||
* @param typistId
|
||||
* @param typistGroupId
|
||||
* @returns workflow typist
|
||||
*/
|
||||
private makeWorkflowTypist(
|
||||
workflowId: number,
|
||||
typistId: number,
|
||||
typistGroupId: number,
|
||||
): DbWorkflowTypist {
|
||||
const workflowTypist = new DbWorkflowTypist();
|
||||
workflowTypist.workflow_id = workflowId;
|
||||
workflowTypist.typist_id = typistId;
|
||||
workflowTypist.typist_group_id = typistGroupId;
|
||||
|
||||
return workflowTypist;
|
||||
}
|
||||
}
|
||||