diff --git a/DockerfileServerDictation.dockerfile b/DockerfileServerDictation.dockerfile index fa06e84..aa666ba 100644 --- a/DockerfileServerDictation.dockerfile +++ b/DockerfileServerDictation.dockerfile @@ -1,14 +1,14 @@ -FROM node:18.13.0-buster AS build-container +FROM node:18.17.1-buster AS build-container WORKDIR /app RUN mkdir dictation_server COPY dictation_server/ dictation_server/ -RUN npm install --force -g n && n 18.13.0 \ +RUN npm install --force -g n && n 18.17.1 \ && cd dictation_server \ && npm ci \ && npm run build \ && cd .. -FROM node:18.13.0-alpine +FROM node:18.17.1-alpine RUN apk --no-cache add tzdata \ && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \ && apk del tzdata \ @@ -20,4 +20,4 @@ RUN mkdir build \ COPY --from=build-container app/dictation_server/dist/ dist/ COPY --from=build-container app/dictation_server/.env ./ COPY --from=build-container app/dictation_server/node_modules/ node_modules/ -CMD ["node", "./dist/main.js" ] \ No newline at end of file +CMD ["node", "./dist/main.js" ] diff --git a/dictation_client/.devcontainer/Dockerfile b/dictation_client/.devcontainer/Dockerfile index e897ee6..5e980cb 100644 --- a/dictation_client/.devcontainer/Dockerfile +++ b/dictation_client/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.13.0-buster +FROM node:18.17.1-buster RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \ echo "Asia/Tokyo" > /etc/timezone @@ -17,6 +17,10 @@ RUN bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "$ && apt-get install default-jre -y \ && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts + +# Update NPM +RUN npm install -g npm + # Install mob RUN curl -sL install.mob.sh | sh diff --git a/dictation_client/package.json b/dictation_client/package.json index 25c1b3b..7e9c601 100644 --- a/dictation_client/package.json +++ b/dictation_client/package.json @@ -7,9 +7,9 @@ }, "scripts": { "start": "vite", - "build": "tsc && vite build", - "build:prod": "tsc && vite build", - "build:local": "tsc && vite build && sh localdeploy.sh", + "build": "tsc && vite build && cp -r static_contents/. build/", + "build:prod": "tsc && vite build && cp -r static_contents/. build/", + "build:local": "tsc && vite build && cp -r static_contents/. build/ && sh localdeploy.sh", "preview": "vite preview", "typecheck": "tsc --noEmit", "codegen": "sh codegen.sh", @@ -96,4 +96,4 @@ } ] } -} +} \ No newline at end of file diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index d8fa420..f5d0b72 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -826,7 +826,7 @@ export interface GetRelationsResponse { */ 'encryptionPassword': string | null; /** - * アカウントがデフォルトで利用するWorkTypeID(アカウントに紐づくWorkTypeIDから一つ指定) + * アカウントがデフォルトで利用するWorkTypeID(アカウントに紐づくWorkTypeIDから一つ指定。activeWorktypeがなければ空文字を返却する) * @type {string} * @memberof GetRelationsResponse */ @@ -1307,6 +1307,12 @@ export interface PostUpdateUserRequest { * @interface PostWorktypeOptionItem */ export interface PostWorktypeOptionItem { + /** + * + * @type {number} + * @memberof PostWorktypeOptionItem + */ + 'id': number; /** * * @type {string} @@ -1665,6 +1671,37 @@ export interface TypistGroup { */ 'name': string; } +/** + * + * @export + * @interface UpdateAccountInfoRequest + */ +export interface UpdateAccountInfoRequest { + /** + * 親アカウントのID + * @type {number} + * @memberof UpdateAccountInfoRequest + */ + 'parentAccountId': number; + /** + * 代行操作許可 + * @type {boolean} + * @memberof UpdateAccountInfoRequest + */ + 'delegationPermission': boolean; + /** + * プライマリ管理者ID + * @type {number} + * @memberof UpdateAccountInfoRequest + */ + 'primaryAdminUserId': number; + /** + * セカンダリ管理者ID + * @type {number} + * @memberof UpdateAccountInfoRequest + */ + 'secondryAdminUserId': number; +} /** * * @export @@ -2531,6 +2568,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + me: async (updateAccountInfoRequest: UpdateAccountInfoRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateAccountInfoRequest' is not null or undefined + assertParamExists('me', 'updateAccountInfoRequest', updateAccountInfoRequest) + const localVarPath = `/accounts/me`; + // 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(updateAccountInfoRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -2867,6 +2944,17 @@ export const AccountsApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.issueLicense(issueLicenseRequest, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async me(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.me(updateAccountInfoRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary @@ -3089,6 +3177,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: any): AxiosPromise { return localVarFp.issueLicense(issueLicenseRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + me(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: any): AxiosPromise { + return localVarFp.me(updateAccountInfoRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -3344,6 +3442,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).issueLicense(issueLicenseRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public me(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).me(updateAccountInfoRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index d8eb794..2dc6bdc 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -53,4 +53,5 @@ export const errorCodes = [ "E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) "E011001", // ワークタイプ重複エラー "E011002", // ワークタイプ登録上限超過エラー + "E011003", // ワークタイプ不在エラー ] as const; diff --git a/dictation_client/src/features/workflow/worktype/constants.ts b/dictation_client/src/features/workflow/worktype/constants.ts new file mode 100644 index 0000000..ba53d9d --- /dev/null +++ b/dictation_client/src/features/workflow/worktype/constants.ts @@ -0,0 +1,6 @@ +export const OPTION_ITEMS_DEFAULT_VALUE_TYPE = { + DEFAULT: "Default", + BLANK: "Blank", + // eslint-disable-next-line @typescript-eslint/naming-convention + LAST_INPUT: "LastInput", +} as const; diff --git a/dictation_client/src/features/workflow/worktype/operations.ts b/dictation_client/src/features/workflow/worktype/operations.ts index 4945831..1875d48 100644 --- a/dictation_client/src/features/workflow/worktype/operations.ts +++ b/dictation_client/src/features/workflow/worktype/operations.ts @@ -2,7 +2,11 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import type { RootState } from "app/store"; import { openSnackbar } from "features/ui/uiSlice"; import { getTranslationID } from "translation"; -import { AccountsApi, GetWorktypesResponse } from "../../../api/api"; +import { + AccountsApi, + GetOptionItemsResponse, + GetWorktypesResponse, +} from "../../../api/api"; import { Configuration } from "../../../api/configuration"; import { ErrorObject, createErrorObject } from "../../../common/errors"; @@ -129,7 +133,7 @@ export const editWorktypeAsync = createAsyncThunk< const { configuration, accessToken } = state.auth; const config = new Configuration(configuration); const accountsApi = new AccountsApi(config); - // stateからworktypeIdとdescriptionを取得する + // stateからselectedId,worktypeId,descriptionを取得する const { selectedId, worktypeId, description } = state.worktype.apps; try { @@ -173,3 +177,110 @@ export const editWorktypeAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const getOptionItemsAsync = createAsyncThunk< + GetOptionItemsResponse, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/getOptionItemsAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + // stateからselectedIdを取得する + const { selectedId } = state.worktype.apps; + + try { + const optionItems = ( + await accountsApi.getOptionItems(selectedId, { + headers: { authorization: `Bearer ${accessToken}` }, + }) + ).data; + + return optionItems; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); + +export const editOptionItemsAsync = createAsyncThunk< + { + // return empty + }, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/editOptionItemsAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration, accessToken } = state.auth; + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + // stateからselectedId,optionItemsを取得する + const { selectedId, optionItems } = state.worktype.apps; + + if (!optionItems) { + throw new Error("optionItems is undefined"); + } + + try { + await accountsApi.updateOptionItems( + selectedId, + { + optionItems, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + // OptionItemの保存に失敗した場合 + if (error.code === "E011003") { + errorMessage = getTranslationID( + "worktypeIdSetting.message.optionItemSaveFailedError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/workflow/worktype/selectors.ts b/dictation_client/src/features/workflow/worktype/selectors.ts index 41d3a75..1f4aabc 100644 --- a/dictation_client/src/features/workflow/worktype/selectors.ts +++ b/dictation_client/src/features/workflow/worktype/selectors.ts @@ -33,3 +33,40 @@ export const selectHasErrorWorktypeId = (state: RootState) => { return { isEmptyWorktypeId, hasIncorrectPatternWorktypeId }; }; + +export const selectOptionItems = (state: RootState) => + state.worktype.apps.optionItems; + +// OptionItemsの値をチェックする +export const selectHasErrorOptionItems = (state: RootState) => { + const { optionItems } = state.worktype.apps; + if (!optionItems) { + return { + hasInvalidOptionItems: false, + hasIncorrectPatternOptionItems: false, + }; + } + // optionItemsに以下の状態の要素が含まれている場合はエラー + // itemLabelに値が入っている + // defaultValueTypeがDefault + // initialValueに値が入っていない + const hasInvalidOptionItems = optionItems.some( + (item) => + item.itemLabel !== "" && + item.defaultValueType === "Default" && + item.initialValue === "" + ); + // optionItemsに以下の状態の要素が含まれている場合はエラー + // itemLabelとinitialValueのどちらかに\/ : * ? “< > | .が含まれている + const incorrectPattern = /[\\/:*?"<>|.]|[^ -~]/; + const hasIncorrectPatternOptionItems = optionItems.some( + (item) => + incorrectPattern.test(item.itemLabel) || + incorrectPattern.test(item.initialValue) + ); + return { hasInvalidOptionItems, hasIncorrectPatternOptionItems }; +}; + +// isOptionItemsLoadingを取得する +export const selectIsOptionItemsLoading = (state: RootState) => + state.worktype.apps.isOptionItemsLoading; diff --git a/dictation_client/src/features/workflow/worktype/state.ts b/dictation_client/src/features/workflow/worktype/state.ts index d2006e2..0475ab5 100644 --- a/dictation_client/src/features/workflow/worktype/state.ts +++ b/dictation_client/src/features/workflow/worktype/state.ts @@ -1,4 +1,5 @@ import { Worktype } from "api"; +import { OptionItem } from "./types"; export interface WorktypeState { apps: Apps; @@ -9,9 +10,11 @@ export interface Apps { isLoading: boolean; isAddLoading: boolean; isEditLoading: boolean; + isOptionItemsLoading: boolean; selectedId: number; worktypeId: string; description?: string; + optionItems?: OptionItem[]; } export interface Domain { diff --git a/dictation_client/src/features/workflow/worktype/types.ts b/dictation_client/src/features/workflow/worktype/types.ts new file mode 100644 index 0000000..239cb26 --- /dev/null +++ b/dictation_client/src/features/workflow/worktype/types.ts @@ -0,0 +1,20 @@ +import { OPTION_ITEMS_DEFAULT_VALUE_TYPE } from "./constants"; + +// OPTION_ITEMS_DEFAULT_VALUE_TYPEからOptionItemDefaultValueTypeを作成する +export type OptionItemsDefaultValueType = + typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE]; + +// 受け取った値がOptionItemDefaultValueType型かどうかを判定する +export const isOptionItemDefaultValueType = ( + value: string +): value is OptionItemsDefaultValueType => + value === OPTION_ITEMS_DEFAULT_VALUE_TYPE.DEFAULT || + value === OPTION_ITEMS_DEFAULT_VALUE_TYPE.BLANK || + value === OPTION_ITEMS_DEFAULT_VALUE_TYPE.LAST_INPUT; + +export interface OptionItem { + id: number; + defaultValueType: OptionItemsDefaultValueType; + itemLabel: string; + initialValue: string; +} diff --git a/dictation_client/src/features/workflow/worktype/worktypeSlice.ts b/dictation_client/src/features/workflow/worktype/worktypeSlice.ts index 353d342..833aa27 100644 --- a/dictation_client/src/features/workflow/worktype/worktypeSlice.ts +++ b/dictation_client/src/features/workflow/worktype/worktypeSlice.ts @@ -2,18 +2,24 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { WorktypeState } from "./state"; import { addWorktypeAsync, + editOptionItemsAsync, editWorktypeAsync, + getOptionItemsAsync, listWorktypesAsync, } from "./operations"; +import { OptionItem, isOptionItemDefaultValueType } from "./types"; +import { OPTION_ITEMS_DEFAULT_VALUE_TYPE } from "./constants"; const initialState: WorktypeState = { apps: { isLoading: false, isAddLoading: false, isEditLoading: false, + isOptionItemsLoading: false, selectedId: NaN, worktypeId: "", description: undefined, + optionItems: undefined, }, domain: {}, }; @@ -26,6 +32,7 @@ export const worktypeSlice = createSlice({ state.apps.selectedId = initialState.apps.selectedId; state.apps.worktypeId = initialState.apps.worktypeId; state.apps.description = initialState.apps.description; + state.apps.optionItems = initialState.apps.optionItems; }, changeSelectedId: (state, action: PayloadAction<{ id: number }>) => { const { id } = action.payload; @@ -45,6 +52,30 @@ export const worktypeSlice = createSlice({ const { description } = action.payload; state.apps.description = description; }, + changeOptionItems: ( + state, + action: PayloadAction<{ optionItem: OptionItem }> + ) => { + const { optionItem } = action.payload; + + // defaultValueTypeがDefault以外の場合はinitialValueを空文字にする + if ( + optionItem.defaultValueType !== OPTION_ITEMS_DEFAULT_VALUE_TYPE.DEFAULT + ) { + optionItem.initialValue = ""; + } + + // idが一致するoptionItemを削除して、新しいoptionItemを追加する。一致するidがない場合は何もしない + const optionItems = state.apps.optionItems?.filter( + (item) => item.id !== optionItem.id + ); + if (optionItems) { + optionItems.push(optionItem); + // optionItemsをidで昇順にソートする + optionItems.sort((a, b) => a.id - b.id); + state.apps.optionItems = optionItems; + } + }, }, extraReducers: (builder) => { builder.addCase(listWorktypesAsync.pending, (state) => { @@ -77,6 +108,35 @@ export const worktypeSlice = createSlice({ builder.addCase(editWorktypeAsync.fulfilled, (state) => { state.apps.isEditLoading = false; }); + builder.addCase(getOptionItemsAsync.pending, (state) => { + state.apps.isOptionItemsLoading = true; + }); + builder.addCase(getOptionItemsAsync.fulfilled, (state, action) => { + state.apps.isOptionItemsLoading = false; + const optionItems: OptionItem[] = action.payload.optionItems.map( + (item) => ({ + id: item.id, + itemLabel: item.itemLabel, + defaultValueType: isOptionItemDefaultValueType(item.defaultValueType) + ? item.defaultValueType + : OPTION_ITEMS_DEFAULT_VALUE_TYPE.DEFAULT, + initialValue: item.initialValue, + }) + ); + state.apps.optionItems = optionItems; + }); + builder.addCase(getOptionItemsAsync.rejected, (state) => { + state.apps.isOptionItemsLoading = false; + }); + builder.addCase(editOptionItemsAsync.pending, (state) => { + state.apps.isOptionItemsLoading = true; + }); + builder.addCase(editOptionItemsAsync.fulfilled, (state) => { + state.apps.isOptionItemsLoading = false; + }); + builder.addCase(editOptionItemsAsync.rejected, (state) => { + state.apps.isOptionItemsLoading = false; + }); }, }); export const { @@ -84,5 +144,6 @@ export const { changeWorktypeId, changeSelectedId, cleanupWorktype, + changeOptionItems, } = worktypeSlice.actions; export default worktypeSlice.reducer; diff --git a/dictation_client/src/pages/PartnerPage/index.tsx b/dictation_client/src/pages/PartnerPage/index.tsx index 03d43c4..617015b 100644 --- a/dictation_client/src/pages/PartnerPage/index.tsx +++ b/dictation_client/src/pages/PartnerPage/index.tsx @@ -24,6 +24,7 @@ import personAdd from "../../assets/images/person_add.svg"; import { TIERS } from "../../components/auth/constants"; import { AddPartnerAccountPopup } from "./addPartnerAccountPopup"; import checkFill from "../../assets/images/check_fill.svg"; +import checkOutline from "../../assets/images/check_outline.svg"; const PartnerPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -181,9 +182,9 @@ const PartnerPage: React.FC = (): JSX.Element => { {x.email ?? "-"} diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/editOptionItemsPopup.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/editOptionItemsPopup.tsx new file mode 100644 index 0000000..36f07a7 --- /dev/null +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/editOptionItemsPopup.tsx @@ -0,0 +1,261 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import styles from "styles/app.module.scss"; +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch } from "app/store"; +import { getTranslationID } from "translation"; +import { + changeOptionItems, + cleanupWorktype, + editOptionItemsAsync, + getOptionItemsAsync, + selectHasErrorOptionItems, + selectIsOptionItemsLoading, + selectOptionItems, +} from "features/workflow/worktype"; +import { OPTION_ITEMS_DEFAULT_VALUE_TYPE } from "features/workflow/worktype/constants"; +import { isOptionItemDefaultValueType } from "features/workflow/worktype/types"; +import close from "../../assets/images/close.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +// popupのpropsの型定義 +interface EditOptionItemsPopupProps { + onClose: () => void; + isOpen: boolean; +} + +export const EditOptionItemsPopup: React.FC = ( + props: EditOptionItemsPopupProps +): JSX.Element => { + const { onClose, isOpen } = props; + const dispatch: AppDispatch = useDispatch(); + // optionItemsを取得 + const optionItems = useSelector(selectOptionItems); + const isLoading = useSelector(selectIsOptionItemsLoading); + const [t] = useTranslation(); + + const closePopup = useCallback(() => { + onClose(); + dispatch(cleanupWorktype()); + setIsPushSaveButton(false); + }, [onClose, dispatch]); + + // 今の入力状態のエラー有無 + const { hasIncorrectPatternOptionItems, hasInvalidOptionItems } = useSelector( + selectHasErrorOptionItems + ); + // 保存ボタンを押したかどうか + const [isPushSaveButton, setIsPushSaveButton] = useState(false); + + useEffect(() => { + // optionItems取得 + if (isOpen) { + dispatch(getOptionItemsAsync()); + } + }, [dispatch, isOpen]); + + const onChangeOptionItem = useCallback( + (optionItem: { + defaultValueType: string; + id: number; + itemLabel: string; + initialValue: string; + }) => { + const { defaultValueType } = optionItem; + if (isOptionItemDefaultValueType(defaultValueType)) { + dispatch( + changeOptionItems({ optionItem: { ...optionItem, defaultValueType } }) + ); + } + }, + [dispatch] + ); + + // optionItemsの更新 + const onSubmit = useCallback(async () => { + setIsPushSaveButton(true); + if (hasIncorrectPatternOptionItems || hasInvalidOptionItems) { + return; + } + + // optionItemsの更新API呼び出し + const { meta } = await dispatch(editOptionItemsAsync()); + if (meta.requestStatus === "fulfilled") { + closePopup(); + } + }, [ + closePopup, + dispatch, + hasIncorrectPatternOptionItems, + hasInvalidOptionItems, + ]); + + return ( +
+
+

+ {t(getTranslationID("worktypeIdSetting.label.optionItem"))} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */} + close +

+
+
+
+
+ + + + + + + {optionItems?.map((item) => ( + + + + + + ))} +
+ {t(getTranslationID("worktypeIdSetting.label.itemLabel"))} + + {t( + getTranslationID("worktypeIdSetting.label.defaultValue") + )} + + {t( + getTranslationID("worktypeIdSetting.label.initialValue") + )} +
+ { + const { value } = e.target; + // optionItemsの更新 + const newOptionItem = { + ...item, + itemLabel: value, + }; + onChangeOptionItem(newOptionItem); + }} + /> + + + + {item.defaultValueType === + OPTION_ITEMS_DEFAULT_VALUE_TYPE.DEFAULT ? ( + { + const { value } = e.target; + // optionItemsの更新 + const newOptionItem = { + ...item, + initialValue: value, + }; + onChangeOptionItem(newOptionItem); + }} + /> + ) : ( + "-" + )} +
+ {isPushSaveButton && hasInvalidOptionItems && ( + + {t( + getTranslationID( + "worktypeIdSetting.message.optionItemInvalidError" + ) + )} + + )} + {isPushSaveButton && hasIncorrectPatternOptionItems && ( + + {t( + getTranslationID( + "worktypeIdSetting.message.optionItemIncorrectError" + ) + )} + + )} + + {t( + getTranslationID("worktypeIdSetting.label.optionItemTerms") + )} + +
+
+
+ + {isLoading && ( + Loading + )} +
+
+
+
+
+ ); +}; diff --git a/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx b/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx index 6d3e7cf..d33fa1b 100644 --- a/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx +++ b/dictation_client/src/pages/WorkTypeIdSettingPage/index.tsx @@ -20,6 +20,7 @@ import { import { AppDispatch } from "app/store"; import { AddWorktypeIdPopup } from "./addWorktypeIdPopup"; import { EditWorktypeIdPopup } from "./editWorktypeIdPopup"; +import { EditOptionItemsPopup } from "./editOptionItemsPopup"; const WorktypeIdSettingPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -31,6 +32,8 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { const [isShowAddPopup, setIsShowAddPopup] = useState(false); // 編集Popupの表示制御 const [isShowEditPopup, setIsShowEditPopup] = useState(false); + const [isShowEditOptionItemPopup, setIsShowEditOptionItemPopup] = + useState(false); useEffect(() => { dispatch(listWorktypesAsync()); }, [dispatch]); @@ -49,6 +52,12 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => { }} isOpen={isShowEditPopup} /> + { + setIsShowEditOptionItemPopup(false); + }} + isOpen={isShowEditOptionItemPopup} + />
@@ -165,9 +174,13 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
  • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} { + dispatch(changeSelectedId({ id: worktype.id })); + setIsShowEditOptionItemPopup(true); + }} > {t( getTranslationID( diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 5a7bd67..7131bc7 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -393,12 +393,23 @@ "worktypeIdTerms": "(de)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", "addWorktype": "(de)Add Worktype", "editWorktypeId": "(de)Edit Worktype ID", - "saveChange": "(de)Save Changes" + "saveChange": "(de)Save Changes", + "editOptionItems": "(de)Option Item", + "itemLabel": "(de)Item label", + "defaultValue": "(de)Default value", + "initialValue": "(de)Initial value", + "default": "(de)Default", + "blank": "(de)Blank", + "lastInput": "(de)Last Input", + "optionItemTerms": "(de)The Item label and Initial value should be alphanumeric and symbols, but not include: \\ / : * ? “ < > | ." }, "message": { "worktypeIdIncorrectError": "(de)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", "alreadyWorktypeIdExistError": "(de)このWorktype IDは既に登録されています。他のWorktype IDで登録してください。", - "worktypeIDLimitError": "(de)Worktype IDが登録件数の上限に達しているため追加できません。" + "worktypeIDLimitError": "(de)Worktype IDが登録件数の上限に達しているため追加できません。", + "optionItemInvalidError": "(de)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。", + "optionItemSaveFailedError": "(de)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください", + "optionItemIncorrectError": "(de)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください" } }, "partnerPage": { @@ -416,4 +427,4 @@ "deleteAccount": "(de)Delete Account" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 63ce4e3..8d5c740 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -393,12 +393,23 @@ "worktypeIdTerms": "WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", "addWorktype": "Add Worktype", "editWorktypeId": "Edit Worktype ID", - "saveChange": "Save Changes" + "saveChange": "Save Changes", + "editOptionItems": "Option Item", + "itemLabel": "Item label", + "defaultValue": "Default value", + "initialValue": "Initial value", + "default": "Default", + "blank": "Blank", + "lastInput": "Last Input", + "optionItemTerms": "The Item label and Initial value should be alphanumeric and symbols, but not include: \\ / : * ? “ < > | ." }, "message": { "worktypeIdIncorrectError": "入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", "alreadyWorktypeIdExistError": "このWorktype IDは既に登録されています。他のWorktype IDで登録してください。", - "worktypeIDLimitError": "Worktype IDが登録件数の上限に達しているため追加できません。" + "worktypeIDLimitError": "Worktype IDが登録件数の上限に達しているため追加できません。", + "optionItemInvalidError": "Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。", + "optionItemSaveFailedError": "オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください", + "optionItemIncorrectError": "入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください" } }, "partnerPage": { @@ -416,4 +427,4 @@ "deleteAccount": "Delete Account" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 3edb794..59b9d93 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -393,12 +393,23 @@ "worktypeIdTerms": "(es)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", "addWorktype": "(es)Add Worktype", "editWorktypeId": "(es)Edit Worktype ID", - "saveChange": "(es)Save Changes" + "saveChange": "(es)Save Changes", + "editOptionItems": "(es)Option Item", + "itemLabel": "(es)Item label", + "defaultValue": "(es)Default value", + "initialValue": "(es)Initial value", + "default": "(es)Default", + "blank": "(es)Blank", + "lastInput": "(es)Last Input", + "optionItemTerms": "(es)The Item label and Initial value should be alphanumeric and symbols, but not include: \\ / : * ? “ < > | ." }, "message": { "worktypeIdIncorrectError": "(es)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", "alreadyWorktypeIdExistError": "(es)このWorktype IDは既に登録されています。他のWorktype IDで登録してください。", - "worktypeIDLimitError": "(es)Worktype IDが登録件数の上限に達しているため追加できません。" + "worktypeIDLimitError": "(es)Worktype IDが登録件数の上限に達しているため追加できません。", + "optionItemInvalidError": "(es)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。", + "optionItemSaveFailedError": "(es)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください", + "optionItemIncorrectError": "(es)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください" } }, "partnerPage": { @@ -416,4 +427,4 @@ "deleteAccount": "(es)Delete Account" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 5ccc10f..781dc35 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -393,12 +393,23 @@ "worktypeIdTerms": "(fr)WorktypeID should be alphanumeric and symbols,\nbut not include: \\ / : * ? “ < > | .", "addWorktype": "(fr)Add Worktype", "editWorktypeId": "(fr)Edit Worktype ID", - "saveChange": "(fr)Save Changes" + "saveChange": "(fr)Save Changes", + "editOptionItems": "(fr)Option Item", + "itemLabel": "(fr)Item label", + "defaultValue": "(fr)Default value", + "initialValue": "(fr)Initial value", + "default": "(fr)Default", + "blank": "(fr)Blank", + "lastInput": "(fr)Last Input", + "optionItemTerms": "(fr)The Item label and Initial value should be alphanumeric and symbols, but not include: \\ / : * ? “ < > | ." }, "message": { "worktypeIdIncorrectError": "(fr)入力されたWorktypeIDがルールを満たしていません。下記のルールを満たすWorktypeIDを入力してください", "alreadyWorktypeIdExistError": "(fr)このWorktype IDは既に登録されています。他のWorktype IDで登録してください。", - "worktypeIDLimitError": "(fr)Worktype IDが登録件数の上限に達しているため追加できません。" + "worktypeIDLimitError": "(fr)Worktype IDが登録件数の上限に達しているため追加できません。", + "optionItemInvalidError": "(fr)Default valueがDefaultに設定されている場合、Initial valueは入力が必須です。", + "optionItemSaveFailedError": "(fr)オプションアイテムの保存に失敗しました。画面を更新し、再度実行してください", + "optionItemIncorrectError": "(fr)入力されたItem labelまたはInitial valueがルールを満たしていません。下記のルールを満たす値を入力してください" } }, "partnerPage": { @@ -416,4 +427,4 @@ "deleteAccount": "(fr)Delete Account" } } -} \ No newline at end of file +} diff --git a/dictation_client/static_contents/YOweYATRY5PBN1G9dHDpfWLbSeVvGNpe61DtYdLSC82pqmPNyIw8EHXwTa6o4iNQB5rNSa.html b/dictation_client/static_contents/YOweYATRY5PBN1G9dHDpfWLbSeVvGNpe61DtYdLSC82pqmPNyIw8EHXwTa6o4iNQB5rNSa.html new file mode 100644 index 0000000..6ed15bd --- /dev/null +++ b/dictation_client/static_contents/YOweYATRY5PBN1G9dHDpfWLbSeVvGNpe61DtYdLSC82pqmPNyIw8EHXwTa6o4iNQB5rNSa.html @@ -0,0 +1 @@ +YOweYATRY5PBN1G9dHDpfWLbSeVvGNpe61DtYdLSC82pqmPNyIw8EHXwTa6o4iNQB5rNSa \ No newline at end of file diff --git a/dictation_server/.devcontainer/Dockerfile b/dictation_server/.devcontainer/Dockerfile index 0f57e4b..be42795 100644 --- a/dictation_server/.devcontainer/Dockerfile +++ b/dictation_server/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.13.0-buster +FROM node:18.17.1-buster RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \ echo "Asia/Tokyo" > /etc/timezone diff --git a/dictation_server/db/migrations/034-add_active_worktype_column.sql b/dictation_server/db/migrations/034-add_active_worktype_column.sql new file mode 100644 index 0000000..62d90db --- /dev/null +++ b/dictation_server/db/migrations/034-add_active_worktype_column.sql @@ -0,0 +1,9 @@ +-- +migrate Up +ALTER TABLE `accounts` +ADD COLUMN `active_worktype_id` BIGINT UNSIGNED COMMENT 'アカウントで利用するデフォルトのWorkTypeID(Active WorktypeID)の内部ID' AFTER `secondary_admin_user_id`, +ADD CONSTRAINT active_worktype_id_fk FOREIGN KEY (active_worktype_id) REFERENCES worktypes(id) ON DELETE SET NULL; + +-- +migrate Down +ALTER TABLE `accounts` +DROP FOREIGN KEY active_worktype_id_fk, +DROP COLUMN `active_worktype_id`; \ No newline at end of file diff --git a/dictation_server/package.json b/dictation_server/package.json index bea2e4e..6523cee 100644 --- a/dictation_server/package.json +++ b/dictation_server/package.json @@ -25,7 +25,8 @@ "og": "openapi-generator-cli", "openapi-format": "cat \"src/api/odms/openapi.json\" | jq -c . > \"src/api/odms/openapi.json\" && prettier --write \"src/api/odms/*.json\"", "migrate:up": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=local", - "migrate:down": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=local" + "migrate:down": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=local", + "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=local" }, "dependencies": { "@azure/identity": "^3.1.3", diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index 0e6a814..9931cf2 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -2837,9 +2837,21 @@ "type": "object", "properties": { "accountId": { "type": "number" }, - "companyName": { "type": "string" } + "companyName": { "type": "string" }, + "tier": { "type": "number" }, + "country": { "type": "string" }, + "parentAccountId": { "type": "number" }, + "delegationPermission": { "type": "boolean" }, + "primaryAdminUserId": { "type": "number" }, + "secondryAdminUserId": { "type": "number" } }, - "required": ["accountId", "companyName"] + "required": [ + "accountId", + "companyName", + "tier", + "country", + "delegationPermission" + ] }, "GetMyAccountResponse": { "type": "object", @@ -3094,7 +3106,7 @@ "type": "array", "items": { "$ref": "#/components/schemas/Worktype" } }, - "acrive": { + "active": { "type": "number", "description": "Active WorktypeIDに設定されているWorkTypeの内部ID" } @@ -3107,6 +3119,7 @@ "worktypeId": { "type": "string", "minLength": 1, + "maxLength": 255, "description": "WorktypeID" }, "description": { "type": "string", "description": "Worktypeの説明" } @@ -3250,12 +3263,7 @@ "description": "セカンダリ管理者ID" } }, - "required": [ - "parentAccountId", - "delegationPermission", - "primaryAdminUserId", - "secondryAdminUserId" - ] + "required": ["delegationPermission"] }, "UpdateAccountInfoResponse": { "type": "object", "properties": {} }, "ConfirmRequest": { @@ -3394,8 +3402,7 @@ }, "encryptionPassword": { "type": "string", - "description": "ユーザーが暗号化を掛ける場合のパスワード", - "nullable": true + "description": "ユーザーが暗号化を掛ける場合のパスワード" }, "activeWorktype": { "type": "string", @@ -3415,7 +3422,6 @@ "authorIdList", "workTypeList", "isEncrypted", - "encryptionPassword", "activeWorktype", "audioFormat", "prompt" diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index 88cd108..27a3980 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -190,8 +190,12 @@ export class AccountsController { // アクセストークン取得 const accessToken = retrieveAuthorizationToken(req); const payload = jwt.decode(accessToken, { json: true }) as AccessToken; + const context = makeContext(payload.userId); //アカウントID取得処理 - const accountInfo = await this.accountService.getMyAccountInfo(payload); + const accountInfo = await this.accountService.getAccountInfo( + context, + payload.userId, + ); return accountInfo; } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index e240d75..2e30743 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -3372,17 +3372,21 @@ describe('getWorktypes', () => { const service = module.get(AccountsService); const context = makeContext(admin.external_id); - await createWorktype(source, account.id, 'worktype1', 'description1'); + await createWorktype(source, account.id, 'worktype1', 'description1', true); await createWorktype(source, account.id, 'worktype2'); //作成したデータを確認 const worktypes = await getWorktypes(source, account.id); + const accounts = await getAccounts(source); { expect(worktypes.length).toBe(2); expect(worktypes[0].custom_worktype_id).toBe('worktype1'); expect(worktypes[1].custom_worktype_id).toBe('worktype2'); expect(worktypes[0].description).toBe('description1'); expect(worktypes[1].description).toBeNull(); + + expect(accounts.length).toBe(1); + expect(accounts[0].active_worktype_id).toBe(worktypes[0].id); } const resWorktypes = await service.getWorktypes(context, admin.external_id); @@ -3394,6 +3398,8 @@ describe('getWorktypes', () => { expect(resWorktypes.worktypes[1].worktypeId).toBe('worktype2'); expect(resWorktypes.worktypes[0].description).toBe('description1'); expect(resWorktypes.worktypes[1].description).toBe(undefined); + + expect(resWorktypes.active).toBe(worktypes[0].id); } }); @@ -4751,3 +4757,56 @@ describe('パートナー一覧取得', () => { expect(partners.total).toBe(0); }); }); + +describe('getAccountInfo', () => { + 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 { account, admin } = await makeTestAccount(source, { + parent_account_id: 123, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id); + + const accountResponse = await service.getAccountInfo( + context, + admin.external_id, + ); + + //実行結果を確認 + { + expect(accountResponse.account.accountId).toBe(account.id); + expect(accountResponse.account.companyName).toBe(account.company_name); + expect(accountResponse.account.country).toBe(account.country); + expect(accountResponse.account.delegationPermission).toBe( + account.delegation_permission, + ); + expect(accountResponse.account.parentAccountId).toBe( + account.parent_account_id, + ); + expect(accountResponse.account.primaryAdminUserId).toBe( + account.primary_admin_user_id, + ); + expect(accountResponse.account.secondryAdminUserId).toBe(undefined); + expect(accountResponse.account.tier).toBe(account.tier); + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 93f3293..024089f 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -364,38 +364,47 @@ export class AccountsService { } /** - * アクセストークンからアカウント情報を取得する - * @param token - * @returns accountId + * パラメータのユーザIDからアカウント情報を取得する + * @param externalId + * @returns GetMyAccountResponse */ - async getMyAccountInfo(token: AccessToken): Promise { - this.logger.log(`[IN] ${this.getMyAccountInfo.name}`); - - let userInfo: User; + async getAccountInfo( + context: Context, + externalId: string, + ): Promise { + this.logger.log( + `[IN] [${context.trackingId}] ${this.getAccountInfo.name} | params: { ` + + `name: ${externalId}, };`, + ); try { - userInfo = await this.usersRepository.findUserByExternalId(token.userId); + let userInfo: User; + userInfo = await this.usersRepository.findUserByExternalId(externalId); + + let accountInfo: Account; + accountInfo = await this.accountRepository.findAccountById( + userInfo.account_id, + ); + + return { + account: { + accountId: userInfo.account_id, + companyName: accountInfo.company_name, + tier: accountInfo.tier, + country: accountInfo.country, + parentAccountId: accountInfo.parent_account_id ?? undefined, + delegationPermission: accountInfo.delegation_permission, + primaryAdminUserId: accountInfo.primary_admin_user_id ?? undefined, + secondryAdminUserId: accountInfo.secondary_admin_user_id ?? undefined, + }, + }; } catch (e) { + this.logger.error(`[${context.trackingId}] error=${e}`); switch (e.constructor) { case UserNotFoundError: throw new HttpException( makeErrorResponse('E010204'), HttpStatus.BAD_REQUEST, ); - default: - throw new HttpException( - makeErrorResponse('E009999'), - HttpStatus.INTERNAL_SERVER_ERROR, - ); - } - } - - let accountInfo: Account; - try { - accountInfo = await this.accountRepository.findAccountById( - userInfo.account_id, - ); - } catch (e) { - switch (e.constructor) { case AccountNotFoundError: throw new HttpException( makeErrorResponse('E010501'), @@ -407,15 +416,11 @@ export class AccountsService { HttpStatus.INTERNAL_SERVER_ERROR, ); } + } finally { + this.logger.log( + `[OUT] [${context.trackingId}] ${this.getAccountInfo.name}`, + ); } - - this.logger.log(`[OUT] ${this.getMyAccountInfo.name}`); - return { - account: { - accountId: userInfo.account_id, - companyName: accountInfo.company_name, - }, - }; } async getTypistGroups(externalId: string): Promise { @@ -1169,8 +1174,9 @@ export class AccountsService { const { account_id: accountId } = await this.usersRepository.findUserByExternalId(externalId); - // ワークタイプ一覧を取得する - const worktypes = await this.worktypesRepository.getWorktypes(accountId); + // ワークタイプ一覧とActiveWorktypeIDを取得する + const { worktypes, active_worktype_id } = + await this.worktypesRepository.getWorktypes(accountId); return { worktypes: worktypes.map((x) => ({ @@ -1178,9 +1184,24 @@ export class AccountsService { worktypeId: x.custom_worktype_id, description: x.description ?? undefined, })), + active: active_worktype_id, }; } catch (e) { - this.logger.error(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, diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index 32a2586..a91a8d4 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -9,6 +9,7 @@ import { UserGroupMember } from '../../../repositories/user_groups/entity/user_g import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity'; import { OptionItem } from '../../../repositories/worktypes/entity/option_item.entity'; import { OPTION_ITEM_VALUE_TYPE } from '../../../constants'; +import { Account } from '../../../repositories/accounts/entity/account.entity'; /** * テスト ユーティリティ: すべてのソート条件を取得する @@ -127,6 +128,7 @@ export const createWorktype = async ( accountId: number, worktypeId: string, description?: string, + isActive?: boolean, ): Promise => { const { identifiers } = await datasource.getRepository(Worktype).insert({ account_id: accountId, @@ -139,6 +141,16 @@ export const createWorktype = async ( updated_at: new Date(), }); const worktype = identifiers.pop() as Worktype; + + if (isActive) { + await datasource.getRepository(Account).update( + { id: accountId }, + { + active_worktype_id: worktype.id, + }, + ); + } + return worktype; }; diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 81fccfb..99672e3 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -103,8 +103,27 @@ export class GetLicenseSummaryResponse { export class Account { @ApiProperty() accountId: number; + @ApiProperty() companyName: string; + + @ApiProperty() + tier: number; + + @ApiProperty() + country: string; + + @ApiProperty({ required: false }) + parentAccountId?: number | undefined; + + @ApiProperty() + delegationPermission: boolean; + + @ApiProperty({ required: false }) + primaryAdminUserId?: number | undefined; + + @ApiProperty({ required: false }) + secondryAdminUserId?: number | undefined; } export class GetMyAccountResponse { @@ -358,7 +377,7 @@ export class GetWorktypesResponse { required: false, description: 'Active WorktypeIDに設定されているWorkTypeの内部ID', }) - acrive?: number | undefined; + active?: number | undefined; } export class CreateWorktypesRequest { @@ -523,16 +542,17 @@ export type PartnerInfoFromDb = { }; export class UpdateAccountInfoRequest { - @ApiProperty({ description: '親アカウントのID' }) - parentAccountId: number; + @ApiProperty({ description: '親アカウントのID', required: false }) + @IsOptional() + parentAccountId?: number | undefined; @ApiProperty({ description: '代行操作許可' }) delegationPermission: boolean; - @ApiProperty({ description: 'プライマリ管理者ID' }) + @ApiProperty({ description: 'プライマリ管理者ID', required: false }) @IsOptional() - primaryAdminUserId?: number; - @ApiProperty({ description: 'セカンダリ管理者ID' }) + primaryAdminUserId?: number | undefined; + @ApiProperty({ description: 'セカンダリ管理者ID', required: false }) @IsOptional() - secondryAdminUserId?: number; + secondryAdminUserId?: number | undefined; } export class UpdateAccountInfoResponse {} diff --git a/dictation_server/src/features/tasks/test/tasks.service.mock.ts b/dictation_server/src/features/tasks/test/tasks.service.mock.ts index 38a9aba..8c9ba89 100644 --- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts +++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts @@ -330,7 +330,7 @@ const defaultTasksRepositoryMockValue: { audio_file_id: 1, status: 'Uploaded', priority: '00', - created_at: new Date('2023-01-01T01:01:01.000'), + created_at: new Date('2023-01-01T01:01:01.000Z'), option_items: [ { id: 1, @@ -401,10 +401,10 @@ const defaultTasksRepositoryMockValue: { file_name: 'test.zip', author_id: 'AUTHOR', work_type_id: 'WorkType', - started_at: new Date('2023-01-01T01:01:01.000'), + started_at: new Date('2023-01-01T01:01:01.000Z'), duration: '123000', - finished_at: new Date('2023-01-01T01:01:01.000'), - uploaded_at: new Date('2023-01-01T01:01:01.000'), + finished_at: new Date('2023-01-01T01:01:01.000Z'), + uploaded_at: new Date('2023-01-01T01:01:01.000Z'), file_size: 123000, priority: '00', audio_format: 'DS', diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 54fd6b0..c3df59c 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -159,7 +159,7 @@ export class GetRelationsResponse { isEncrypted: boolean; @ApiProperty({ description: 'ユーザーが暗号化を掛ける場合のパスワード', - nullable: true, + required: false, }) encryptionPassword?: string | undefined; @ApiProperty({ diff --git a/dictation_server/src/repositories/accounts/entity/account.entity.ts b/dictation_server/src/repositories/accounts/entity/account.entity.ts index ad65437..f6787d9 100644 --- a/dictation_server/src/repositories/accounts/entity/account.entity.ts +++ b/dictation_server/src/repositories/accounts/entity/account.entity.ts @@ -40,6 +40,9 @@ export class Account { @Column({ nullable: true }) secondary_admin_user_id?: number; + @Column({ nullable: true }) + active_worktype_id?: number; + @Column({ nullable: true }) deleted_at?: Date; diff --git a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts index bd856e6..7ce50da 100644 --- a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts +++ b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts @@ -13,22 +13,41 @@ import { } from './errors/types'; import { OptionItem } from './entity/option_item.entity'; import { PostWorktypeOptionItem } from '../../features/accounts/types/types'; +import { AccountNotFoundError } from '../accounts/errors/types'; +import { Account } from '../accounts/entity/account.entity'; @Injectable() export class WorktypesRepositoryService { constructor(private dataSource: DataSource) {} /** - * ワークタイプ一覧を取得する - * @param accountId - * @returns worktypes + * ワークタイプ一覧とActiveWorktypeIDを取得する + * @param externalId + * @returns worktypes and active worktype id */ - async getWorktypes(accountId: number): Promise { + async getWorktypes(accountId: number): Promise<{ + worktypes: Worktype[]; + active_worktype_id?: number | undefined; + }> { return await this.dataSource.transaction(async (entityManager) => { - const repo = entityManager.getRepository(Worktype); + const WorktypeRepo = entityManager.getRepository(Worktype); + const accountRepo = entityManager.getRepository(Account); - const worktypes = await repo.find({ where: { account_id: accountId } }); - return worktypes; + const account = await accountRepo.findOne({ where: { id: accountId } }); + + // 運用上アカウントがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!account) { + throw new AccountNotFoundError(); + } + + const worktypes = await WorktypeRepo.find({ + where: { account_id: account.id }, + }); + + return { + worktypes, + active_worktype_id: account.active_worktype_id ?? undefined, + }; }); } /**