Merge branch 'develop'
This commit is contained in:
commit
3ba1350bbd
@ -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" ]
|
||||
CMD ["node", "./dist/main.js" ]
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<RequestArgs> => {
|
||||
// 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<object>> {
|
||||
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<object> {
|
||||
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<object> {
|
||||
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
|
||||
|
||||
@ -53,4 +53,5 @@ export const errorCodes = [
|
||||
"E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
|
||||
"E011001", // ワークタイプ重複エラー
|
||||
"E011002", // ワークタイプ登録上限超過エラー
|
||||
"E011003", // ワークタイプ不在エラー
|
||||
] as const;
|
||||
|
||||
@ -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;
|
||||
@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
20
dictation_client/src/features/workflow/worktype/types.ts
Normal file
20
dictation_client/src/features/workflow/worktype/types.ts
Normal file
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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 => {
|
||||
<td>{x.email ?? "-"}</td>
|
||||
<td>
|
||||
<img
|
||||
src={checkFill}
|
||||
src={x.dealerManagement ? checkFill : ""}
|
||||
alt=""
|
||||
className={styles.icCheckCircle}
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -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<EditOptionItemsPopupProps> = (
|
||||
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<boolean>(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 (
|
||||
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
|
||||
<div className={styles.modalBox}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("worktypeIdSetting.label.optionItem"))}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<img
|
||||
src={close}
|
||||
className={styles.modalTitleIcon}
|
||||
alt="close"
|
||||
onClick={closePopup}
|
||||
/>
|
||||
</p>
|
||||
<form className={styles.form}>
|
||||
<dl className={`${styles.formList} ${styles.hasbg}`}>
|
||||
<dd className={styles.full}>
|
||||
<div className={styles.tableWrap}>
|
||||
<table className={styles.table}>
|
||||
<tr className={styles.tableHeader}>
|
||||
<th className={styles.noLine}>
|
||||
{t(getTranslationID("worktypeIdSetting.label.itemLabel"))}
|
||||
</th>
|
||||
<th className={styles.noLine}>
|
||||
{t(
|
||||
getTranslationID("worktypeIdSetting.label.defaultValue")
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID("worktypeIdSetting.label.initialValue")
|
||||
)}
|
||||
</th>
|
||||
</tr>
|
||||
{optionItems?.map((item) => (
|
||||
<tr key={`optionItem_${item.id}`}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={16}
|
||||
name="itemLabel"
|
||||
value={item.itemLabel}
|
||||
className={styles.formInput}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
// optionItemsの更新
|
||||
const newOptionItem = {
|
||||
...item,
|
||||
itemLabel: value,
|
||||
};
|
||||
onChangeOptionItem(newOptionItem);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
name="defaultValueType"
|
||||
className={styles.formInput}
|
||||
value={item.defaultValueType}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
// optionItemsの更新
|
||||
const newOptionItem = {
|
||||
...item,
|
||||
defaultValueType: value,
|
||||
};
|
||||
onChangeOptionItem(newOptionItem);
|
||||
}}
|
||||
>
|
||||
<option
|
||||
value={OPTION_ITEMS_DEFAULT_VALUE_TYPE.DEFAULT}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"worktypeIdSetting.label.default"
|
||||
)
|
||||
)}
|
||||
</option>
|
||||
<option value={OPTION_ITEMS_DEFAULT_VALUE_TYPE.BLANK}>
|
||||
{t(
|
||||
getTranslationID("worktypeIdSetting.label.blank")
|
||||
)}
|
||||
</option>
|
||||
<option
|
||||
value={OPTION_ITEMS_DEFAULT_VALUE_TYPE.LAST_INPUT}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"worktypeIdSetting.label.lastInput"
|
||||
)
|
||||
)}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
{item.defaultValueType ===
|
||||
OPTION_ITEMS_DEFAULT_VALUE_TYPE.DEFAULT ? (
|
||||
<input
|
||||
type="text"
|
||||
maxLength={20}
|
||||
name="initialValue"
|
||||
value={item.initialValue}
|
||||
className={styles.formInput}
|
||||
onChange={(e) => {
|
||||
const { value } = e.target;
|
||||
// optionItemsの更新
|
||||
const newOptionItem = {
|
||||
...item,
|
||||
initialValue: value,
|
||||
};
|
||||
onChangeOptionItem(newOptionItem);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
{isPushSaveButton && hasInvalidOptionItems && (
|
||||
<span className={`${styles.formError} ${styles.alignCenter}`}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"worktypeIdSetting.message.optionItemInvalidError"
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{isPushSaveButton && hasIncorrectPatternOptionItems && (
|
||||
<span className={`${styles.formError} ${styles.alignCenter}`}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"worktypeIdSetting.message.optionItemIncorrectError"
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
style={{ display: "block" }}
|
||||
className={`${styles.formComment} ${styles.alignCenter}`}
|
||||
>
|
||||
{t(
|
||||
getTranslationID("worktypeIdSetting.label.optionItemTerms")
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<input
|
||||
type="button"
|
||||
name="submit"
|
||||
value={t(getTranslationID("common.label.save"))}
|
||||
className={`${styles.formSubmit} ${styles.marginBtm1} ${styles.isActive}`}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
{isLoading && (
|
||||
<img
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -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<boolean>(false);
|
||||
// 編集Popupの表示制御
|
||||
const [isShowEditPopup, setIsShowEditPopup] = useState<boolean>(false);
|
||||
const [isShowEditOptionItemPopup, setIsShowEditOptionItemPopup] =
|
||||
useState<boolean>(false);
|
||||
useEffect(() => {
|
||||
dispatch(listWorktypesAsync());
|
||||
}, [dispatch]);
|
||||
@ -49,6 +52,12 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
isOpen={isShowEditPopup}
|
||||
/>
|
||||
<EditOptionItemsPopup
|
||||
onClose={() => {
|
||||
setIsShowEditOptionItemPopup(false);
|
||||
}}
|
||||
isOpen={isShowEditOptionItemPopup}
|
||||
/>
|
||||
<div className={styles.wrap}>
|
||||
<Header userName="XXXXXXX" />
|
||||
<UpdateTokenTimer />
|
||||
@ -165,9 +174,13 @@ const WorktypeIdSettingPage: React.FC = (): JSX.Element => {
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
// onClick={}
|
||||
onClick={() => {
|
||||
dispatch(changeSelectedId({ id: worktype.id }));
|
||||
setIsShowEditOptionItemPopup(true);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
YOweYATRY5PBN1G9dHDpfWLbSeVvGNpe61DtYdLSC82pqmPNyIw8EHXwTa6o4iNQB5rNSa
|
||||
@ -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
|
||||
|
||||
@ -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`;
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -3372,17 +3372,21 @@ describe('getWorktypes', () => {
|
||||
const service = module.get<AccountsService>(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>(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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -364,38 +364,47 @@ export class AccountsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* アクセストークンからアカウント情報を取得する
|
||||
* @param token
|
||||
* @returns accountId
|
||||
* パラメータのユーザIDからアカウント情報を取得する
|
||||
* @param externalId
|
||||
* @returns GetMyAccountResponse
|
||||
*/
|
||||
async getMyAccountInfo(token: AccessToken): Promise<GetMyAccountResponse> {
|
||||
this.logger.log(`[IN] ${this.getMyAccountInfo.name}`);
|
||||
|
||||
let userInfo: User;
|
||||
async getAccountInfo(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
): Promise<GetMyAccountResponse> {
|
||||
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<TypistGroup[]> {
|
||||
@ -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,
|
||||
|
||||
@ -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<Worktype> => {
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -159,7 +159,7 @@ export class GetRelationsResponse {
|
||||
isEncrypted: boolean;
|
||||
@ApiProperty({
|
||||
description: 'ユーザーが暗号化を掛ける場合のパスワード',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
encryptionPassword?: string | undefined;
|
||||
@ApiProperty({
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<Worktype[]> {
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user