diff --git a/dictation_client/src/AppRouter.tsx b/dictation_client/src/AppRouter.tsx index 8cfed31..cf6e1d3 100644 --- a/dictation_client/src/AppRouter.tsx +++ b/dictation_client/src/AppRouter.tsx @@ -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 = () => ( @@ -81,6 +82,8 @@ const AppRouter: React.FC = () => ( path="/partners" element={} />} /> + } /> + } /> ); diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index eb4d4e2..36a1050 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -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; } +/** + * + * @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} + * @memberof CreateWorkflowsRequest + */ + 'typists': Array; +} /** * * @export @@ -606,6 +656,19 @@ export interface GetAllocatableLicensesResponse { */ 'allocatableLicenses': Array; } +/** + * + * @export + * @interface GetAuthorsResponse + */ +export interface GetAuthorsResponse { + /** + * + * @type {Array} + * @memberof GetAuthorsResponse + */ + 'authors': Array; +} /** * * @export @@ -989,6 +1052,19 @@ export interface GetUsersResponse { */ 'users': Array; } +/** + * + * @export + * @interface GetWorkflowsResponse + */ +export interface GetWorkflowsResponse { + /** + * ワークフローの一覧 + * @type {Array} + * @memberof GetWorkflowsResponse + */ + 'workflows': Array; +} /** * * @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} + * @memberof Workflow + */ + 'typists': Array; +} +/** + * + * @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 => { + 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> { + 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 { return localVarFp.deleteAccount(deleteAccountRequest, options).then((request) => request(axios, basePath)); }, + /** + * ログインしているユーザーのアカウント配下のAuthor一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAuthors(options?: any): AxiosPromise { + 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 => { + // 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 => { + 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> { + 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> { + 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 { + return localVarFp.createWorkflows(createWorkflowsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * アカウント内のワークフローの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getWorkflows(options?: any): AxiosPromise { + 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)); + } +} + + + diff --git a/dictation_client/src/app/store.ts b/dictation_client/src/app/store.ts index d42c776..e48a4ba 100644 --- a/dictation_client/src/app/store.ts +++ b/dictation_client/src/app/store.ts @@ -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, }, }); diff --git a/dictation_client/src/assets/images/download.svg b/dictation_client/src/assets/images/download.svg new file mode 100644 index 0000000..504fce3 --- /dev/null +++ b/dictation_client/src/assets/images/download.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/dictation_client/src/assets/images/exit.svg b/dictation_client/src/assets/images/exit.svg new file mode 100644 index 0000000..242dd28 --- /dev/null +++ b/dictation_client/src/assets/images/exit.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/dictation_client/src/assets/images/group_setting.svg b/dictation_client/src/assets/images/group_setting.svg new file mode 100644 index 0000000..b86f803 --- /dev/null +++ b/dictation_client/src/assets/images/group_setting.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/dictation_client/src/assets/images/logout.svg b/dictation_client/src/assets/images/logout.svg new file mode 100644 index 0000000..7cc7e86 --- /dev/null +++ b/dictation_client/src/assets/images/logout.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/dictation_client/src/assets/images/rule_add.svg b/dictation_client/src/assets/images/rule_add.svg new file mode 100644 index 0000000..4d4d6e6 --- /dev/null +++ b/dictation_client/src/assets/images/rule_add.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/dictation_client/src/assets/images/template_setting.svg b/dictation_client/src/assets/images/template_setting.svg new file mode 100644 index 0000000..54c8955 --- /dev/null +++ b/dictation_client/src/assets/images/template_setting.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/dictation_client/src/assets/images/worktype_setting.svg b/dictation_client/src/assets/images/worktype_setting.svg new file mode 100644 index 0000000..ad5cd24 --- /dev/null +++ b/dictation_client/src/assets/images/worktype_setting.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 7d1b632..41368a5 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -55,4 +55,5 @@ export const errorCodes = [ "E011001", // ワークタイプ重複エラー "E011002", // ワークタイプ登録上限超過エラー "E011003", // ワークタイプ不在エラー + "E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー ] as const; diff --git a/dictation_client/src/features/dictation/dictationSlice.ts b/dictation_client/src/features/dictation/dictationSlice.ts index f90c5ed..fe97dff 100644 --- a/dictation_client/src/features/dictation/dictationSlice.ts +++ b/dictation_client/src/features/dictation/dictationSlice.ts @@ -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; }); diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index 225fbfe..bd3d750 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -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 }); } }); diff --git a/dictation_client/src/features/workflow/index.ts b/dictation_client/src/features/workflow/index.ts new file mode 100644 index 0000000..460a995 --- /dev/null +++ b/dictation_client/src/features/workflow/index.ts @@ -0,0 +1,4 @@ +export * from "./workflowSlice"; +export * from "./state"; +export * from "./selectors"; +export * from "./operations"; diff --git a/dictation_client/src/features/workflow/operations.ts b/dictation_client/src/features/workflow/operations.ts new file mode 100644 index 0000000..76e84a5 --- /dev/null +++ b/dictation_client/src/features/workflow/operations.ts @@ -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 }); + } +}); diff --git a/dictation_client/src/features/workflow/selectors.ts b/dictation_client/src/features/workflow/selectors.ts new file mode 100644 index 0000000..a71ba4a --- /dev/null +++ b/dictation_client/src/features/workflow/selectors.ts @@ -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, + }; +}; diff --git a/dictation_client/src/features/workflow/state.ts b/dictation_client/src/features/workflow/state.ts new file mode 100644 index 0000000..e99a186 --- /dev/null +++ b/dictation_client/src/features/workflow/state.ts @@ -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[]; +} diff --git a/dictation_client/src/features/workflow/workflowSlice.ts b/dictation_client/src/features/workflow/workflowSlice.ts new file mode 100644 index 0000000..bb6aa9f --- /dev/null +++ b/dictation_client/src/features/workflow/workflowSlice.ts @@ -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; diff --git a/dictation_client/src/pages/AccountPage/accountDeleteSuccess.tsx b/dictation_client/src/pages/AccountPage/accountDeleteSuccess.tsx new file mode 100644 index 0000000..790af01 --- /dev/null +++ b/dictation_client/src/pages/AccountPage/accountDeleteSuccess.tsx @@ -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 ( +
+
+
+
+
+

+ {t(getTranslationID("accountDeleteSuccess.label.title"))} +

+
+ +
+
+
+
+ {t(getTranslationID("accountDeleteSuccess.label.message"))} +
+
+ + {t( + getTranslationID( + "accountDeleteSuccess.label.backToTopPageLink" + ) + )} + +
+
+
+
+
+
+
+ ); +}; diff --git a/dictation_client/src/pages/AccountPage/deleteAccountPopup.tsx b/dictation_client/src/pages/AccountPage/deleteAccountPopup.tsx index 0d94684..2fcb13c 100644 --- a/dictation_client/src/pages/AccountPage/deleteAccountPopup.tsx +++ b/dictation_client/src/pages/AccountPage/deleteAccountPopup.tsx @@ -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 = ( +export const DeleteAccountPopup: React.FC = ( props ) => { const { onClose } = props; @@ -21,6 +22,8 @@ export const DeleteAccountPopup: React.FC = ( const dispatch: AppDispatch = useDispatch(); const isLoading = useSelector(selectIsLoading); + const { instance } = useMsal(); + const accountInfo = useSelector(selectAccountInfo); // ポップアップを閉じる処理 @@ -31,13 +34,21 @@ export const DeleteAccountPopup: React.FC = ( 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 = (
= ( ))} + {!isLoading && licenseOrderHistory.length === 0 && ( +

+ {t(getTranslationID("common.message.listEmpty"))} +

+ )} {isLoading && ( { ))} + {!isLoading && childrenPartnerLicensesInfo.length === 0 && ( +

+ {t(getTranslationID("common.message.listEmpty"))} +

+ )} {/* pagenation */}