Merged PR 1006: 2025/1/27 PH1エンハンス 本番リリース
This commit is contained in:
parent
aef17893d9
commit
b529388871
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
environment_building_tools/logfile.log
|
||||
@ -27,6 +27,7 @@ module.exports = {
|
||||
rules: {
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/require-default-props": "off",
|
||||
"react/function-component-definition": [
|
||||
"error",
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,8 @@ import licenseCardIssue from "features/license/licenseCardIssue/licenseCardIssue
|
||||
import licenseCardActivate from "features/license/licenseCardActivate/licenseCardActivateSlice";
|
||||
import licenseSummary from "features/license/licenseSummary/licenseSummarySlice";
|
||||
import partnerLicense from "features/license/partnerLicense/partnerLicenseSlice";
|
||||
import licenseTrialIssue from "features/license/licenseTrialIssue/licenseTrialIssueSlice";
|
||||
import searchPartners from "features/license/searchPartner/searchPartnerSlice";
|
||||
import dictation from "features/dictation/dictationSlice";
|
||||
import partner from "features/partner/partnerSlice";
|
||||
import licenseOrderHistory from "features/license/licenseOrderHistory/licenseOrderHistorySlice";
|
||||
@ -35,6 +37,8 @@ export const store = configureStore({
|
||||
licenseSummary,
|
||||
licenseOrderHistory,
|
||||
partnerLicense,
|
||||
licenseTrialIssue,
|
||||
searchPartners,
|
||||
dictation,
|
||||
partner,
|
||||
typistGroup,
|
||||
|
||||
7
dictation_client/src/assets/images/search.svg
Normal file
7
dictation_client/src/assets/images/search.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48px" height="48px" viewBox="0 0 48 48" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.686275%,15.686275%,15.686275%);fill-opacity:1;" d="M 42.949219 37.109375 L 33.898438 28.0625 L 33.007812 28.949219 L 31.476562 27.414062 C 33.183594 24.882812 34.046875 21.945312 34.042969 19.011719 C 34.046875 15.171875 32.578125 11.320312 29.644531 8.390625 C 26.722656 5.464844 22.867188 4 19.019531 4.003906 C 15.179688 4 11.324219 5.46875 8.398438 8.390625 C 5.46875 11.320312 4 15.167969 4.003906 19.011719 C 4 22.851562 5.46875 26.703125 8.394531 29.628906 C 11.328125 32.554688 15.179688 34.019531 19.023438 34.019531 C 21.957031 34.019531 24.902344 33.160156 27.433594 31.453125 L 28.96875 32.988281 L 28.082031 33.871094 L 37.136719 42.917969 C 37.847656 43.636719 38.796875 43.996094 39.738281 43.996094 C 40.671875 43.996094 41.621094 43.636719 42.335938 42.917969 L 42.949219 42.308594 C 43.664062 41.59375 44.027344 40.644531 44.027344 39.707031 C 44.027344 38.769531 43.664062 37.824219 42.949219 37.109375 Z M 19.023438 32.003906 C 15.6875 32.003906 12.359375 30.738281 9.824219 28.199219 C 7.285156 25.667969 6.019531 22.34375 6.019531 19.011719 C 6.019531 15.675781 7.289062 12.351562 9.824219 9.820312 C 12.359375 7.285156 15.6875 6.019531 19.019531 6.019531 C 22.355469 6.019531 25.683594 7.285156 28.21875 9.820312 C 30.757812 12.351562 32.023438 15.675781 32.027344 19.011719 C 32.023438 22.34375 30.757812 25.667969 28.222656 28.199219 C 25.683594 30.738281 22.355469 32.003906 19.023438 32.003906 Z M 28.78125 30.421875 C 29.074219 30.171875 29.367188 29.910156 29.648438 29.628906 C 29.929688 29.351562 30.191406 29.058594 30.445312 28.761719 L 31.820312 30.136719 L 30.15625 31.800781 Z M 41.523438 40.882812 L 40.910156 41.492188 C 40.582031 41.820312 40.164062 41.976562 39.734375 41.980469 C 39.308594 41.980469 38.890625 41.820312 38.5625 41.496094 L 30.9375 33.875 L 33.898438 30.917969 L 41.523438 38.535156 C 41.847656 38.863281 42.007812 39.28125 42.007812 39.707031 C 42.007812 40.136719 41.847656 40.554688 41.523438 40.882812 Z M 41.523438 40.882812 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.686275%,15.686275%,15.686275%);fill-opacity:1;" d="M 25.695312 12.34375 C 23.855469 10.507812 21.433594 9.585938 19.023438 9.585938 C 16.609375 9.585938 14.191406 10.507812 12.351562 12.34375 C 10.511719 14.179688 9.589844 16.601562 9.59375 19.011719 C 9.589844 21.421875 10.511719 23.84375 12.351562 25.675781 C 14.191406 27.511719 16.609375 28.433594 19.019531 28.433594 C 21.433594 28.433594 23.855469 27.511719 25.695312 25.675781 C 27.535156 23.839844 28.453125 21.421875 28.453125 19.011719 C 28.457031 16.601562 27.53125 14.183594 25.695312 12.34375 Z M 24.503906 24.488281 C 22.992188 25.996094 21.011719 26.753906 19.019531 26.75 C 17.03125 26.75 15.050781 25.996094 13.539062 24.488281 C 12.027344 22.976562 11.277344 21 11.277344 19.011719 C 11.277344 17.023438 12.027344 15.042969 13.539062 13.53125 C 15.054688 12.023438 17.03125 11.269531 19.023438 11.265625 C 21.011719 11.269531 22.992188 12.023438 24.503906 13.53125 C 26.015625 15.042969 26.769531 17.023438 26.769531 19.011719 C 26.769531 21 26.015625 22.976562 24.503906 24.488281 Z M 24.503906 24.488281 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@ -47,6 +47,7 @@ export const KEYS_TO_PRESERVE = [
|
||||
"accessToken",
|
||||
"refreshToken",
|
||||
"displayInfo",
|
||||
"filterCriteria",
|
||||
"sortCriteria",
|
||||
];
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ export const getAccountRelationsAsync = createAsyncThunk<
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
const dealers = await accountsApi.getDealers();
|
||||
const users = await usersApi.getUsers({
|
||||
const users = await usersApi.getUsers(undefined, undefined, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
return {
|
||||
|
||||
@ -43,6 +43,8 @@ const initialState: DictationState = {
|
||||
direction: DIRECTION.ASC,
|
||||
paramName: SORTABLE_COLUMN.JobNumber,
|
||||
selectedTask: undefined,
|
||||
authorId: "",
|
||||
fileName: "",
|
||||
assignee: {
|
||||
selected: [],
|
||||
pool: [],
|
||||
@ -78,6 +80,14 @@ export const dictationSlice = createSlice({
|
||||
const { paramName } = action.payload;
|
||||
state.apps.paramName = paramName;
|
||||
},
|
||||
changeAuthorId: (state, action: PayloadAction<{ authorId: string }>) => {
|
||||
const { authorId } = action.payload;
|
||||
state.apps.authorId = authorId;
|
||||
},
|
||||
changeFileName: (state, action: PayloadAction<{ fileName: string }>) => {
|
||||
const { fileName } = action.payload;
|
||||
state.apps.fileName = fileName;
|
||||
},
|
||||
changeSelectedTask: (state, action: PayloadAction<{ task: Task }>) => {
|
||||
const { task } = action.payload;
|
||||
state.apps.selectedTask = task;
|
||||
@ -246,6 +256,8 @@ export const {
|
||||
changeDisplayInfo,
|
||||
changeDirection,
|
||||
changeParamName,
|
||||
changeAuthorId,
|
||||
changeFileName,
|
||||
changeSelectedTask,
|
||||
changeAssignee,
|
||||
changeBackupTaskChecked,
|
||||
|
||||
@ -35,6 +35,8 @@ export const listTasksAsync = createAsyncThunk<
|
||||
filter?: string;
|
||||
direction: DirectionType;
|
||||
paramName: SortableColumnType;
|
||||
authorId?: string;
|
||||
fileName?: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -43,7 +45,8 @@ export const listTasksAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("dictations/listTasksAsync", async (args, thunkApi) => {
|
||||
const { limit, offset, filter, direction, paramName } = args;
|
||||
const { limit, offset, filter, direction, paramName, authorId, fileName } =
|
||||
args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
@ -60,6 +63,8 @@ export const listTasksAsync = createAsyncThunk<
|
||||
filter,
|
||||
direction,
|
||||
paramName,
|
||||
authorId,
|
||||
fileName,
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
@ -80,6 +85,136 @@ export const listTasksAsync = createAsyncThunk<
|
||||
}
|
||||
});
|
||||
|
||||
export const getTaskFiltersAsync = createAsyncThunk<
|
||||
{
|
||||
authorId?: string;
|
||||
fileName?: string;
|
||||
},
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("dictations/getTaskFiltersAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
|
||||
try {
|
||||
const usertaskfilter = await usersApi.getTaskFilter({
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
const { authorId, fileName } = usertaskfilter.data;
|
||||
return { authorId, fileName };
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const updateTaskFiltersAsync = createAsyncThunk<
|
||||
{
|
||||
/** empty */
|
||||
},
|
||||
{
|
||||
filterConditionAuthorId: string;
|
||||
filterConditionFileName: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("dictations/updateTaskFiltersAsync", async (args, thunkApi) => {
|
||||
const { filterConditionAuthorId, filterConditionFileName } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
try {
|
||||
return await usersApi.updateTaskFilter(
|
||||
{ filterConditionAuthorId, filterConditionFileName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const updateSortColumnAsync = createAsyncThunk<
|
||||
{
|
||||
/** empty */
|
||||
},
|
||||
{
|
||||
direction: DirectionType;
|
||||
paramName: SortableColumnType;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("dictations/updateSortColumnAsync", async (args, thunkApi) => {
|
||||
const { direction, paramName } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
|
||||
const usersApi = new UsersApi(config);
|
||||
|
||||
try {
|
||||
return await usersApi.updateSortCriteria(
|
||||
{ direction, paramName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const getSortColumnAsync = createAsyncThunk<
|
||||
{
|
||||
direction: DirectionType;
|
||||
@ -280,6 +415,8 @@ export const playbackAsync = createAsyncThunk<
|
||||
direction: DirectionType;
|
||||
paramName: SortableColumnType;
|
||||
audioFileId: number;
|
||||
filterConditionAuthorId: string;
|
||||
filterConditionFileName: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -288,7 +425,13 @@ export const playbackAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("dictations/playbackAsync", async (args, thunkApi) => {
|
||||
const { audioFileId, direction, paramName } = args;
|
||||
const {
|
||||
audioFileId,
|
||||
direction,
|
||||
paramName,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
} = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
@ -305,6 +448,12 @@ export const playbackAsync = createAsyncThunk<
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
await usersApi.updateTaskFilter(
|
||||
{ filterConditionAuthorId, filterConditionFileName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
await tasksApi.checkout(audioFileId, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
@ -387,6 +536,8 @@ export const cancelAsync = createAsyncThunk<
|
||||
paramName: SortableColumnType;
|
||||
audioFileId: number;
|
||||
isTypist: boolean;
|
||||
filterConditionAuthorId: string;
|
||||
filterConditionFileName: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -395,7 +546,14 @@ export const cancelAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("dictations/cancelAsync", async (args, thunkApi) => {
|
||||
const { audioFileId, direction, paramName, isTypist } = args;
|
||||
const {
|
||||
audioFileId,
|
||||
direction,
|
||||
paramName,
|
||||
isTypist,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
} = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
@ -406,15 +564,25 @@ export const cancelAsync = createAsyncThunk<
|
||||
const tasksApi = new TasksApi(config);
|
||||
const usersApi = new UsersApi(config);
|
||||
try {
|
||||
// ユーザーがタイピストである場合に、ソート条件を保存する
|
||||
// ユーザーがタイピストである場合に、ソート条件と検索条件を保存する
|
||||
if (isTypist) {
|
||||
await usersApi.updateSortCriteria(
|
||||
{ direction, paramName },
|
||||
{
|
||||
direction,
|
||||
paramName,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
await usersApi.updateTaskFilter(
|
||||
{ filterConditionAuthorId, filterConditionFileName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
await tasksApi.cancel(audioFileId, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
@ -459,6 +627,8 @@ export const reopenAsync = createAsyncThunk<
|
||||
paramName: SortableColumnType;
|
||||
audioFileId: number;
|
||||
isTypist: boolean;
|
||||
filterConditionAuthorId: string;
|
||||
filterConditionFileName: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -467,7 +637,14 @@ export const reopenAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("dictations/reopenAsync", async (args, thunkApi) => {
|
||||
const { audioFileId, direction, paramName, isTypist } = args;
|
||||
const {
|
||||
audioFileId,
|
||||
direction,
|
||||
paramName,
|
||||
isTypist,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
} = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
@ -478,7 +655,7 @@ export const reopenAsync = createAsyncThunk<
|
||||
const tasksApi = new TasksApi(config);
|
||||
const usersApi = new UsersApi(config);
|
||||
try {
|
||||
// ユーザーがタイピストである場合に、ソート条件を保存する
|
||||
// ユーザーがタイピストである場合に、ソート条件と検索条件を保存する
|
||||
if (isTypist) {
|
||||
await usersApi.updateSortCriteria(
|
||||
{ direction, paramName },
|
||||
@ -486,6 +663,12 @@ export const reopenAsync = createAsyncThunk<
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
await usersApi.updateTaskFilter(
|
||||
{ filterConditionAuthorId, filterConditionFileName },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
}
|
||||
await tasksApi.reopen(audioFileId, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
@ -552,6 +735,8 @@ export const listBackupPopupTasksAsync = createAsyncThunk<
|
||||
BACKUP_POPUP_LIST_STATUS.join(","), // ステータスはFinished,Backupのみ
|
||||
DIRECTION.DESC,
|
||||
SORTABLE_COLUMN.Status,
|
||||
undefined, // backupポップアップ表示時には検索条件は未指定
|
||||
undefined, // backupポップアップ表示時には検索条件は未指定
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
|
||||
@ -72,6 +72,12 @@ export const selectDirection = (state: RootState) =>
|
||||
export const selectParamName = (state: RootState) =>
|
||||
state.dictation.apps.paramName;
|
||||
|
||||
export const selectAuthorId = (state: RootState) =>
|
||||
state.dictation.apps.authorId;
|
||||
|
||||
export const selectFilename = (state: RootState) =>
|
||||
state.dictation.apps.fileName;
|
||||
|
||||
export const selectSelectedTask = (state: RootState) =>
|
||||
state.dictation.apps.selectedTask;
|
||||
|
||||
|
||||
@ -25,6 +25,8 @@ export interface Apps {
|
||||
displayInfo: DisplayInfoType;
|
||||
direction: DirectionType;
|
||||
paramName: SortableColumnType;
|
||||
authorId: string;
|
||||
fileName: string;
|
||||
selectedTask?: Task;
|
||||
selectedFileTask?: Task;
|
||||
assignee: {
|
||||
|
||||
@ -7,3 +7,8 @@ export const STATUS = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
ORDER_CANCELED: "Order Canceled",
|
||||
} as const;
|
||||
|
||||
export const LICENSE_TYPE = {
|
||||
NORMAL: "NORMAL",
|
||||
TRIAL: "TRIAL",
|
||||
} as const;
|
||||
|
||||
@ -3,7 +3,12 @@ import type { RootState } from "app/store";
|
||||
import { getTranslationID } from "translation";
|
||||
import { openSnackbar } from "features/ui/uiSlice";
|
||||
import { getAccessToken } from "features/auth";
|
||||
import { AccountsApi, LicensesApi } from "../../../api/api";
|
||||
import {
|
||||
AccountsApi,
|
||||
LicensesApi,
|
||||
SearchPartner,
|
||||
PartnerLicenseInfo,
|
||||
} from "../../../api/api";
|
||||
import { Configuration } from "../../../api/configuration";
|
||||
import { ErrorObject, createErrorObject } from "../../../common/errors";
|
||||
import { OrderHistoryView } from "./types";
|
||||
@ -15,6 +20,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
|
||||
// パラメータ
|
||||
limit: number;
|
||||
offset: number;
|
||||
selectedRow?: PartnerLicenseInfo | SearchPartner;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
@ -23,7 +29,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
|
||||
};
|
||||
}
|
||||
>("licenses/licenseOrderHisotyAsync", async (args, thunkApi) => {
|
||||
const { limit, offset } = args;
|
||||
const { limit, offset, selectedRow } = args;
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
@ -33,7 +39,6 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
|
||||
const accountsApi = new AccountsApi(config);
|
||||
|
||||
try {
|
||||
const { selectedRow } = state.partnerLicense.apps;
|
||||
let accountId = 0;
|
||||
let companyName = "";
|
||||
// 他の画面から指定されていない場合はログインアカウントのidを取得する
|
||||
@ -46,7 +51,9 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
|
||||
companyName = getMyAccountResponse.data.account.companyName;
|
||||
} else {
|
||||
accountId = selectedRow.accountId;
|
||||
companyName = selectedRow.companyName;
|
||||
// パートナーライセンスとパートナー検索で型が異なるため、型ガードで推論させる
|
||||
if ("companyName" in selectedRow) companyName = selectedRow.companyName;
|
||||
if ("name" in selectedRow) companyName = selectedRow.name;
|
||||
}
|
||||
|
||||
const res = await accountsApi.getOrderHistories(
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
AccountsApi,
|
||||
GetCompanyNameResponse,
|
||||
GetLicenseSummaryResponse,
|
||||
SearchPartner,
|
||||
PartnerLicenseInfo,
|
||||
UpdateRestrictionStatusRequest,
|
||||
} from "../../../api/api";
|
||||
@ -17,7 +18,7 @@ export const getLicenseSummaryAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
GetLicenseSummaryResponse,
|
||||
// 引数
|
||||
{ selectedRow?: PartnerLicenseInfo },
|
||||
{ selectedRow?: PartnerLicenseInfo | SearchPartner },
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
@ -73,7 +74,7 @@ export const getCompanyNameAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
GetCompanyNameResponse,
|
||||
// 引数
|
||||
{ selectedRow?: PartnerLicenseInfo },
|
||||
{ selectedRow?: PartnerLicenseInfo | SearchPartner },
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export const ISSUED_TRIAL_LICENSE_QUANTITY = 10;
|
||||
export const TRIAL_LICENSE_EXPIRATION_DAY = 30;
|
||||
@ -0,0 +1,5 @@
|
||||
export * from "./state";
|
||||
export * from "./operations";
|
||||
export * from "./selectors";
|
||||
export * from "./licenseTrialIssueSlice";
|
||||
export * from "./constants";
|
||||
@ -0,0 +1,60 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { convertLocalToUTCDate } from "common/convertLocalToUTCDate";
|
||||
import { LicenseTrialIssueState } from "./state";
|
||||
import { issueTrialLicenseAsync } from "./operations";
|
||||
import {
|
||||
TRIAL_LICENSE_EXPIRATION_DAY,
|
||||
ISSUED_TRIAL_LICENSE_QUANTITY,
|
||||
} from "./constants";
|
||||
|
||||
const initialState: LicenseTrialIssueState = {
|
||||
apps: {
|
||||
isLoading: false,
|
||||
expirationDate: "",
|
||||
quantity: ISSUED_TRIAL_LICENSE_QUANTITY,
|
||||
},
|
||||
};
|
||||
export const licenseTrialIssueSlice = createSlice({
|
||||
name: "licenseTrialIssue",
|
||||
initialState,
|
||||
reducers: {
|
||||
cleanupApps: (state) => {
|
||||
state.apps = initialState.apps;
|
||||
},
|
||||
setExpirationDate: (state) => {
|
||||
// 有効期限を設定
|
||||
const currentDate = new Date();
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setDate(currentDate.getDate() + TRIAL_LICENSE_EXPIRATION_DAY);
|
||||
// タイムゾーンオフセットを考慮して、ローカルタイムでの日付を取得
|
||||
const expirationDateLocal = convertLocalToUTCDate(expiryDate);
|
||||
const expirationDateWithoutTime = new Date(
|
||||
expirationDateLocal.getFullYear(),
|
||||
expirationDateLocal.getMonth(),
|
||||
expirationDateLocal.getDate()
|
||||
);
|
||||
const expirationYear = expirationDateWithoutTime.getFullYear();
|
||||
const expirationMonth = expirationDateWithoutTime.getMonth() + 1; // getMonth() の結果は0から始まるため、1を足して実際の月に合わせる
|
||||
const expirationDay = expirationDateWithoutTime.getDate();
|
||||
const formattedExpirationDate = `${expirationYear}/${expirationMonth}/${expirationDay} (${TRIAL_LICENSE_EXPIRATION_DAY})`;
|
||||
|
||||
state.apps.expirationDate = formattedExpirationDate;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(issueTrialLicenseAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(issueTrialLicenseAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(issueTrialLicenseAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { cleanupApps, setExpirationDate } =
|
||||
licenseTrialIssueSlice.actions;
|
||||
|
||||
export default licenseTrialIssueSlice.reducer;
|
||||
@ -0,0 +1,84 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import type { RootState } from "app/store";
|
||||
import { getTranslationID } from "translation";
|
||||
import { openSnackbar } from "features/ui/uiSlice";
|
||||
import { getAccessToken } from "features/auth";
|
||||
import {
|
||||
LicensesApi,
|
||||
SearchPartner,
|
||||
PartnerLicenseInfo,
|
||||
} from "../../../api/api";
|
||||
import { Configuration } from "../../../api/configuration";
|
||||
import { ErrorObject, createErrorObject } from "../../../common/errors";
|
||||
|
||||
export const issueTrialLicenseAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
{
|
||||
selectedRow?: PartnerLicenseInfo | SearchPartner;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("licenses/issueTrialLicenseAsync", async (args, thunkApi) => {
|
||||
const { selectedRow } = args;
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const licensesApi = new LicensesApi(config);
|
||||
|
||||
try {
|
||||
if (!selectedRow) {
|
||||
// アカウントが選択されていない場合はエラーとする。
|
||||
const errorMessage = getTranslationID(
|
||||
"trialLicenseIssuePopupPage.message.accountNotSelected"
|
||||
);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// トライアルライセンス発行処理を実行
|
||||
await licensesApi.issueTrialLicenses(
|
||||
{
|
||||
issuedAccount: selectedRow.accountId,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
|
||||
const errorMessage = getTranslationID("common.message.internalServerError");
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { RootState } from "app/store";
|
||||
|
||||
export const selectIsLoading = (state: RootState) =>
|
||||
state.licenseTrialIssue.apps.isLoading;
|
||||
|
||||
export const selectExpirationDate = (state: RootState) =>
|
||||
state.licenseTrialIssue.apps.expirationDate;
|
||||
|
||||
export const selectNumberOfLicenses = (state: RootState) =>
|
||||
state.licenseTrialIssue.apps.quantity;
|
||||
@ -0,0 +1,9 @@
|
||||
export interface LicenseTrialIssueState {
|
||||
apps: Apps;
|
||||
}
|
||||
|
||||
export interface Apps {
|
||||
isLoading: boolean;
|
||||
expirationDate: string;
|
||||
quantity: number;
|
||||
}
|
||||
@ -25,6 +25,7 @@ const initialState: PartnerLicensesState = {
|
||||
tier: 0,
|
||||
companyName: "",
|
||||
stockLicense: 0,
|
||||
allocatedLicense: 0,
|
||||
issuedRequested: 0,
|
||||
shortage: 0,
|
||||
issueRequesting: 0,
|
||||
@ -39,6 +40,9 @@ const initialState: PartnerLicensesState = {
|
||||
hierarchicalElements: [],
|
||||
isLoading: true,
|
||||
selectedRow: undefined,
|
||||
isLicenseOrderHistoryOpen: false,
|
||||
isViewDetailsOpen: false,
|
||||
isSearchPopupOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
@ -88,6 +92,24 @@ export const partnerLicenseSlice = createSlice({
|
||||
state.apps.limit = limit;
|
||||
state.apps.offset = offset;
|
||||
},
|
||||
setIsLicenseOrderHistoryOpen: (
|
||||
state,
|
||||
action: PayloadAction<{ value: boolean }>
|
||||
) => {
|
||||
state.apps.isLicenseOrderHistoryOpen = action.payload.value;
|
||||
},
|
||||
setIsViewDetailsOpen: (
|
||||
state,
|
||||
action: PayloadAction<{ value: boolean }>
|
||||
) => {
|
||||
state.apps.isViewDetailsOpen = action.payload.value;
|
||||
},
|
||||
setIsSearchPopupOpen: (
|
||||
state,
|
||||
action: PayloadAction<{ value: boolean }>
|
||||
) => {
|
||||
state.apps.isSearchPopupOpen = action.payload.value;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(getMyAccountAsync.pending, (state) => {
|
||||
@ -131,6 +153,9 @@ export const {
|
||||
clearHierarchicalElement,
|
||||
changeSelectedRow,
|
||||
savePageInfo,
|
||||
setIsLicenseOrderHistoryOpen,
|
||||
setIsViewDetailsOpen,
|
||||
setIsSearchPopupOpen,
|
||||
} = partnerLicenseSlice.actions;
|
||||
|
||||
export default partnerLicenseSlice.reducer;
|
||||
|
||||
@ -30,3 +30,10 @@ export const selectCurrentPage = (state: RootState) => {
|
||||
};
|
||||
export const selectSelectedRow = (state: RootState) =>
|
||||
state.partnerLicense.apps.selectedRow;
|
||||
|
||||
export const selectIsLicenseOrderHistoryOpen = (state: RootState) =>
|
||||
state.partnerLicense.apps.isLicenseOrderHistoryOpen;
|
||||
export const selectIsViewDetailsOpen = (state: RootState) =>
|
||||
state.partnerLicense.apps.isViewDetailsOpen;
|
||||
export const selectIsSearchPopupOpen = (state: RootState) =>
|
||||
state.partnerLicense.apps.isSearchPopupOpen;
|
||||
|
||||
@ -20,6 +20,9 @@ export interface Apps {
|
||||
hierarchicalElements: HierarchicalElement[];
|
||||
isLoading: boolean;
|
||||
selectedRow?: PartnerLicenseInfo;
|
||||
isLicenseOrderHistoryOpen: boolean;
|
||||
isViewDetailsOpen: boolean;
|
||||
isSearchPopupOpen: boolean;
|
||||
}
|
||||
|
||||
export interface HierarchicalElement {
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./state";
|
||||
export * from "./operations";
|
||||
export * from "./selectors";
|
||||
export * from "./searchPartnerSlice";
|
||||
@ -0,0 +1,96 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { getAccessToken } from "features/auth";
|
||||
import type { RootState } from "../../../app/store";
|
||||
import { getTranslationID } from "../../../translation";
|
||||
import { openSnackbar } from "../../ui/uiSlice";
|
||||
import { AccountsApi, SearchPartner, PartnerHierarchy } from "../../../api/api";
|
||||
import { Configuration } from "../../../api/configuration";
|
||||
import { ErrorObject, createErrorObject } from "../../../common/errors";
|
||||
|
||||
export const searchPartnersAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
SearchPartner[],
|
||||
// 引数
|
||||
{
|
||||
companyName?: string;
|
||||
accountId?: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("licenses/searchPartners", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { companyName, accountId } = args;
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountsApi = new AccountsApi(config);
|
||||
try {
|
||||
const searchPartnerResponse = await accountsApi.searchPartners(
|
||||
companyName,
|
||||
accountId,
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
return searchPartnerResponse.data.searchResult;
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const getPartnerHierarchy = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
PartnerHierarchy[],
|
||||
// 引数
|
||||
{
|
||||
accountId: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("licenses/getPartnerHierarchy", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { accountId } = args;
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountsApi = new AccountsApi(config);
|
||||
try {
|
||||
const partnerHierarchyResponse = await accountsApi.getPartnerHierarchy(
|
||||
accountId,
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
return partnerHierarchyResponse.data.accountHierarchy;
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,80 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { SearchPartner } from "../../../api";
|
||||
import { SearchPartnerState } from "./state";
|
||||
import { searchPartnersAsync, getPartnerHierarchy } from "./operations";
|
||||
|
||||
const initialState: SearchPartnerState = {
|
||||
domain: {
|
||||
searchResult: [],
|
||||
partnerHierarchy: [],
|
||||
},
|
||||
apps: {
|
||||
isLoading: false,
|
||||
selectedRow: undefined,
|
||||
isLicenseOrderHistoryOpen: false,
|
||||
isViewDetailsOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const searchPartnersSlice = createSlice({
|
||||
name: "searchPartners",
|
||||
initialState,
|
||||
reducers: {
|
||||
changeSelectedRow: (
|
||||
state,
|
||||
action: PayloadAction<{ value?: SearchPartner }>
|
||||
) => {
|
||||
const { value } = action.payload;
|
||||
state.apps.selectedRow = value;
|
||||
},
|
||||
setIsLicenseOrderHistoryOpen: (
|
||||
state,
|
||||
action: PayloadAction<{ value: boolean }>
|
||||
) => {
|
||||
state.apps.isLicenseOrderHistoryOpen = action.payload.value;
|
||||
},
|
||||
setIsViewDetailsOpen: (
|
||||
state,
|
||||
action: PayloadAction<{ value: boolean }>
|
||||
) => {
|
||||
state.apps.isViewDetailsOpen = action.payload.value;
|
||||
},
|
||||
cleanupSearchResult: (state) => {
|
||||
state.domain.searchResult = initialState.domain.searchResult;
|
||||
},
|
||||
cleanupPartnerHierarchy: (state) => {
|
||||
state.domain.partnerHierarchy = initialState.domain.partnerHierarchy;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(searchPartnersAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(searchPartnersAsync.fulfilled, (state, action) => {
|
||||
state.domain.searchResult = action.payload;
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(searchPartnersAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getPartnerHierarchy.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(getPartnerHierarchy.fulfilled, (state, action) => {
|
||||
state.domain.partnerHierarchy = action.payload;
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getPartnerHierarchy.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
export const {
|
||||
changeSelectedRow,
|
||||
setIsLicenseOrderHistoryOpen,
|
||||
setIsViewDetailsOpen,
|
||||
cleanupSearchResult,
|
||||
cleanupPartnerHierarchy,
|
||||
} = searchPartnersSlice.actions;
|
||||
|
||||
export default searchPartnersSlice.reducer;
|
||||
@ -0,0 +1,14 @@
|
||||
import { RootState } from "../../../app/store";
|
||||
|
||||
export const selectSearchResult = (state: RootState) =>
|
||||
state.searchPartners.domain.searchResult;
|
||||
export const selectPartnerHierarchy = (state: RootState) =>
|
||||
state.searchPartners.domain.partnerHierarchy;
|
||||
export const selectIsLoading = (state: RootState) =>
|
||||
state.searchPartners.apps.isLoading;
|
||||
export const selectSelectedRow = (state: RootState) =>
|
||||
state.searchPartners.apps.selectedRow;
|
||||
export const selectIsLicenseOrderHistoryOpen = (state: RootState) =>
|
||||
state.searchPartners.apps.isLicenseOrderHistoryOpen;
|
||||
export const selectIsViewDetailsOpen = (state: RootState) =>
|
||||
state.searchPartners.apps.isViewDetailsOpen;
|
||||
18
dictation_client/src/features/license/searchPartner/state.ts
Normal file
18
dictation_client/src/features/license/searchPartner/state.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { SearchPartner, PartnerHierarchy } from "../../../api/api";
|
||||
|
||||
export interface SearchPartnerState {
|
||||
domain: Domain;
|
||||
apps: Apps;
|
||||
}
|
||||
|
||||
export interface Domain {
|
||||
searchResult: SearchPartner[];
|
||||
partnerHierarchy: PartnerHierarchy[];
|
||||
}
|
||||
|
||||
export interface Apps {
|
||||
isLoading: boolean;
|
||||
selectedRow?: SearchPartner;
|
||||
isLicenseOrderHistoryOpen: boolean;
|
||||
isViewDetailsOpen: boolean;
|
||||
}
|
||||
@ -18,7 +18,7 @@ export const listUsersAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
GetUsersResponse,
|
||||
// 引数
|
||||
void,
|
||||
undefined | { userInputUserName?: string; userInputEmail?: string },
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
@ -33,9 +33,11 @@ export const listUsersAsync = createAsyncThunk<
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
const userInputUserName = args?.userInputUserName;
|
||||
const userInputEmail = args?.userInputEmail;
|
||||
|
||||
try {
|
||||
const res = await usersApi.getUsers({
|
||||
const res = await usersApi.getUsers(userInputUserName, userInputEmail, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
@ -500,6 +502,72 @@ export const deleteUserAsync = createAsyncThunk<
|
||||
}
|
||||
});
|
||||
|
||||
export const confirmUserForceAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
// 引数
|
||||
{
|
||||
userId: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("users/confirmUserForceAsync", async (args, thunkApi) => {
|
||||
const { userId } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
|
||||
try {
|
||||
await usersApi.confirmUserForce(
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
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");
|
||||
|
||||
// ユーザーが既に認証済みのため、強制認証不可
|
||||
if (error.code === "E010202") {
|
||||
errorMessage = getTranslationID(
|
||||
"userListPage.message.alreadyEmailVerifiedError"
|
||||
);
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const importUsersAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
{
|
||||
|
||||
@ -22,6 +22,8 @@ import {
|
||||
selectDirection,
|
||||
changeParamName,
|
||||
changeDirection,
|
||||
changeAuthorId,
|
||||
changeFileName,
|
||||
changeSelectedTask,
|
||||
openFilePropertyInfo,
|
||||
SortableColumnType,
|
||||
@ -37,6 +39,11 @@ import {
|
||||
deleteTaskAsync,
|
||||
isSortableColumnType,
|
||||
isDirectionType,
|
||||
getTaskFiltersAsync,
|
||||
selectAuthorId,
|
||||
selectFilename,
|
||||
updateTaskFiltersAsync,
|
||||
updateSortColumnAsync,
|
||||
} from "features/dictation";
|
||||
import { getTranslationID } from "translation";
|
||||
import { Task } from "api/api";
|
||||
@ -55,6 +62,7 @@ import { DisPlayInfo } from "./displayInfo";
|
||||
import { ChangeTranscriptionistPopup } from "./changeTranscriptionistPopup";
|
||||
import { BackupPopup } from "./backupPopup";
|
||||
import { FilePropertyPopup } from "./filePropertyPopup";
|
||||
import searchIcon from "../../assets/images/search.svg";
|
||||
|
||||
const DictationPage: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
@ -101,10 +109,18 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
const [filterFinished, setFilterFinished] = useState(true);
|
||||
const [filterBackup, setFilterBackup] = useState(false);
|
||||
|
||||
// 検索条件の入力値
|
||||
const [filterConditionAuthorId, setFilterConditionAuthorId] = useState("");
|
||||
const [filterConditionFileName, setFilterConditionFileName] = useState("");
|
||||
|
||||
// ソート対象カラム
|
||||
const sortableParamName = useSelector(selectParamName);
|
||||
const sortDirection = useSelector(selectDirection);
|
||||
|
||||
// task_filtersテーブルの検索条件
|
||||
const authorId = useSelector(selectAuthorId);
|
||||
const fileName = useSelector(selectFilename);
|
||||
|
||||
const tasks = useSelector(selectTasks);
|
||||
const total = useSelector(selectTotal);
|
||||
const totalPage = useSelector(selectTotalPage);
|
||||
@ -128,6 +144,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -141,6 +159,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]);
|
||||
|
||||
const getLastPage = useCallback(() => {
|
||||
@ -159,6 +179,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -173,6 +195,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]);
|
||||
|
||||
const getPrevPage = useCallback(() => {
|
||||
@ -191,6 +215,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -205,6 +231,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]);
|
||||
|
||||
const getNextPage = useCallback(() => {
|
||||
@ -223,6 +251,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -237,6 +267,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]);
|
||||
|
||||
const updateSortColumn = useCallback(
|
||||
@ -269,6 +301,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: currentDirection,
|
||||
paramName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -283,6 +317,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterPending,
|
||||
filterFinished,
|
||||
filterBackup,
|
||||
authorId,
|
||||
fileName,
|
||||
]
|
||||
);
|
||||
|
||||
@ -332,6 +368,19 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
hasFinished,
|
||||
hasBackup
|
||||
);
|
||||
|
||||
// フィルターの状態をローカルストレージに保存する
|
||||
localStorage.setItem(
|
||||
"filterCriteria",
|
||||
JSON.stringify({
|
||||
Uploaded: hasUploaded,
|
||||
InProgress: hasInProgress,
|
||||
Pending: hasPending,
|
||||
Finished: hasFinished,
|
||||
Backup: hasBackup,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
listTasksAsync({
|
||||
limit: LIMIT_TASK_NUM,
|
||||
@ -339,12 +388,14 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
dispatch(listTypistGroupsAsync());
|
||||
},
|
||||
[dispatch, sortDirection, sortableParamName]
|
||||
[dispatch, sortDirection, sortableParamName, authorId, fileName]
|
||||
);
|
||||
|
||||
const onPlayBack = useCallback(
|
||||
@ -360,6 +411,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
audioFileId,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
filterConditionAuthorId: authorId,
|
||||
filterConditionFileName: fileName,
|
||||
})
|
||||
);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
@ -379,13 +432,14 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
dispatch(listTypistGroupsAsync());
|
||||
|
||||
const url = `${
|
||||
import.meta.env.VITE_DESK_TOP_APP_SCHEME
|
||||
const url = `${import.meta.env.VITE_DESK_TOP_APP_SCHEME
|
||||
}:playback?audioId=${audioFileId}`;
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
@ -403,6 +457,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterUploaded,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
t,
|
||||
]
|
||||
);
|
||||
@ -424,6 +480,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -438,6 +496,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]
|
||||
);
|
||||
|
||||
@ -455,6 +515,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
isTypist,
|
||||
filterConditionAuthorId: authorId,
|
||||
filterConditionFileName: fileName,
|
||||
})
|
||||
);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
@ -472,6 +534,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -488,6 +552,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
isTypist,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
t,
|
||||
]
|
||||
);
|
||||
@ -507,8 +573,11 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
isTypist,
|
||||
filterConditionAuthorId: authorId,
|
||||
filterConditionFileName: fileName,
|
||||
})
|
||||
);
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
const filter = getFilter(
|
||||
filterUploaded,
|
||||
@ -524,6 +593,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -540,6 +611,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
isTypist,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
t,
|
||||
]
|
||||
);
|
||||
@ -569,6 +642,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -583,9 +658,28 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
]
|
||||
);
|
||||
|
||||
const onChangeFilterConditionFileName = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterConditionFileName(e.target.value.trimStart());
|
||||
},
|
||||
[setFilterConditionFileName]
|
||||
);
|
||||
|
||||
const onChangeFilterConditionAuthorId = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// 先頭に%が入力されるとWAFのルールでブロックされてしまう。
|
||||
// Authorの登録時に「_」以外の記号は許可されていないため、「_」と半角英数字以外の文字は除去。
|
||||
const correctAuthorId = e.target.value.replace(/[^a-zA-Z0-9_]/g, "");
|
||||
setFilterConditionAuthorId(correctAuthorId);
|
||||
},
|
||||
[setFilterConditionAuthorId]
|
||||
);
|
||||
|
||||
const sortIconClass = (
|
||||
currentParam: SortableColumnType,
|
||||
currentDirection: DirectionType,
|
||||
@ -628,6 +722,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -643,10 +739,68 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filterUploaded,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
authorId,
|
||||
fileName,
|
||||
t,
|
||||
]
|
||||
);
|
||||
|
||||
const requestSearch = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const { meta: taskFilterMeta } = await dispatch(
|
||||
updateTaskFiltersAsync({
|
||||
filterConditionFileName,
|
||||
filterConditionAuthorId,
|
||||
})
|
||||
);
|
||||
const { meta: sortCriteriaMeta } = await dispatch(
|
||||
updateSortColumnAsync({
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
})
|
||||
);
|
||||
if (
|
||||
taskFilterMeta.requestStatus === "fulfilled" &&
|
||||
sortCriteriaMeta.requestStatus === "fulfilled"
|
||||
) {
|
||||
const filter = getFilter(
|
||||
filterUploaded,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterFinished,
|
||||
filterBackup
|
||||
);
|
||||
dispatch(changeAuthorId({ authorId: filterConditionAuthorId }));
|
||||
dispatch(changeFileName({ fileName: filterConditionFileName }));
|
||||
// 検索した条件でタスク一覧を取得する
|
||||
dispatch(
|
||||
listTasksAsync({
|
||||
limit: LIMIT_TASK_NUM,
|
||||
offset: 0,
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
authorId: filterConditionAuthorId,
|
||||
fileName: filterConditionFileName,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
filterBackup,
|
||||
filterFinished,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterUploaded,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
]
|
||||
);
|
||||
|
||||
// 初回読み込み処理
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -662,21 +816,73 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
|
||||
dispatch(changeDisplayInfo({ column: displayInfo }));
|
||||
|
||||
const filter = getFilter(true, true, true, true, false);
|
||||
// フィルター状態をローカルストレージから取得する
|
||||
const filterValue = localStorage.getItem("filterCriteria");
|
||||
|
||||
const { meta, payload } = await dispatch(getSortColumnAsync());
|
||||
let filter: string | undefined;
|
||||
if (filterValue) {
|
||||
const parsedFilter = JSON.parse(filterValue);
|
||||
setFilterUploaded(parsedFilter.Uploaded);
|
||||
setFilterInProgress(parsedFilter.InProgress);
|
||||
setFilterPending(parsedFilter.Pending);
|
||||
setFilterFinished(parsedFilter.Finished);
|
||||
setFilterBackup(parsedFilter.Backup);
|
||||
|
||||
filter = getFilter(
|
||||
parsedFilter.Uploaded,
|
||||
parsedFilter.InProgress,
|
||||
parsedFilter.Pending,
|
||||
parsedFilter.Finished,
|
||||
parsedFilter.Backup
|
||||
);
|
||||
} else {
|
||||
filter = getFilter(true, true, true, true, false);
|
||||
localStorage.setItem(
|
||||
"filterCriteria",
|
||||
JSON.stringify({
|
||||
Uploaded: true,
|
||||
InProgress: true,
|
||||
Pending: true,
|
||||
Finished: true,
|
||||
Backup: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
// タスクフィルター条件
|
||||
const { meta: taskFilterMeta, payload: taskfilterPayload } =
|
||||
await dispatch(getTaskFiltersAsync());
|
||||
let payloadAuthorId: string | undefined;
|
||||
let payloadFileName: string | undefined;
|
||||
if (
|
||||
meta.requestStatus === "fulfilled" &&
|
||||
payload &&
|
||||
!("error" in payload)
|
||||
taskFilterMeta.requestStatus === "fulfilled" &&
|
||||
taskfilterPayload &&
|
||||
!("error" in taskfilterPayload)
|
||||
) {
|
||||
payloadAuthorId = taskfilterPayload.authorId ?? "";
|
||||
payloadFileName = taskfilterPayload.fileName ?? "";
|
||||
|
||||
dispatch(changeAuthorId({ authorId: payloadAuthorId }));
|
||||
dispatch(changeFileName({ fileName: payloadFileName }));
|
||||
// 初回表示時に検索フォームにtask_filtersテーブルの値を設定する。
|
||||
setFilterConditionAuthorId(payloadAuthorId);
|
||||
setFilterConditionFileName(payloadFileName);
|
||||
}
|
||||
|
||||
// ソート条件
|
||||
const { meta: sortCriteriaMeta, payload: sortCriteriaPayload } =
|
||||
await dispatch(getSortColumnAsync());
|
||||
let direction: DirectionType = "ASC";
|
||||
let paramName: SortableColumnType = "JOB_NUMBER";
|
||||
if (
|
||||
sortCriteriaMeta.requestStatus === "fulfilled" &&
|
||||
sortCriteriaPayload &&
|
||||
!("error" in sortCriteriaPayload)
|
||||
) {
|
||||
// ソート情報をローカルストレージから取得する
|
||||
const sortColumnValue = localStorage.getItem("sortCriteria") ?? "";
|
||||
let direction: DirectionType;
|
||||
let paramName: SortableColumnType;
|
||||
if (sortColumnValue === "") {
|
||||
direction = payload.direction;
|
||||
paramName = payload.paramName;
|
||||
direction = sortCriteriaPayload.direction;
|
||||
paramName = sortCriteriaPayload.paramName;
|
||||
} else {
|
||||
// ソート情報をDirectionとParamNameに分割する
|
||||
const sortColumn = sortColumnValue?.split(",");
|
||||
@ -687,15 +893,18 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
// 正常なソート情報がローカルストレージに存在する場合はローカルストレージの情報を使用する
|
||||
direction = isDirectionType(localStorageDirection)
|
||||
? localStorageDirection
|
||||
: payload.direction;
|
||||
: sortCriteriaPayload.direction;
|
||||
paramName = isSortableColumnType(localStorageParamName)
|
||||
? localStorageParamName
|
||||
: payload.paramName;
|
||||
: sortCriteriaPayload.paramName;
|
||||
|
||||
dispatch(changeDirection({ direction }));
|
||||
dispatch(changeParamName({ paramName }));
|
||||
}
|
||||
}
|
||||
|
||||
// タスク一覧を取得する
|
||||
if (isDirectionType(direction) && isSortableColumnType(paramName)) {
|
||||
dispatch(
|
||||
listTasksAsync({
|
||||
limit: LIMIT_TASK_NUM,
|
||||
@ -703,6 +912,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
filter,
|
||||
direction,
|
||||
paramName,
|
||||
authorId: payloadAuthorId,
|
||||
fileName: payloadFileName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
@ -735,8 +946,12 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
<section className={styles.dictation}>
|
||||
<div>
|
||||
<DisPlayInfo />
|
||||
<ul className={styles.menuAction}>
|
||||
<li>
|
||||
<ul className={styles.tableFilter}>
|
||||
<li>{t(getTranslationID("dictationPage.label.filter"))}:</li>
|
||||
<li>
|
||||
{t(getTranslationID("dictationPage.label.filter"))}:
|
||||
</li>
|
||||
<li>
|
||||
<label htmlFor="uploaded">
|
||||
<input
|
||||
@ -780,7 +995,9 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{t(getTranslationID("dictationPage.label.inProgress"))}
|
||||
{t(
|
||||
getTranslationID("dictationPage.label.inProgress")
|
||||
)}
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
@ -853,7 +1070,47 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li className={styles.floatRight}>
|
||||
<form
|
||||
className={styles.searchBar}
|
||||
onSubmit={(e) => requestSearch(e)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t(
|
||||
getTranslationID("dictationPage.label.fileName")
|
||||
)}
|
||||
value={filterConditionFileName}
|
||||
onChange={(e) => onChangeFilterConditionFileName(e)}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t(
|
||||
getTranslationID("dictationPage.label.authorId")
|
||||
)}
|
||||
value={filterConditionAuthorId}
|
||||
onChange={(e) => onChangeFilterConditionAuthorId(e)}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<button
|
||||
type="submit"
|
||||
className={`${styles.menuLink} ${!isLoading ? styles.isActive : ""
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={searchIcon}
|
||||
alt="search"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(getTranslationID("dictationPage.label.search"))}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={styles.tableWrap}>
|
||||
<table className={`${styles.table} ${styles.dictation}`}>
|
||||
<tr className={styles.tableHeader}>
|
||||
@ -1540,8 +1797,7 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
)}`}</span>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${
|
||||
!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
className={`${!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={getFirstPage}
|
||||
>
|
||||
@ -1549,8 +1805,7 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
</a>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${
|
||||
!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
className={`${!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={getPrevPage}
|
||||
>
|
||||
@ -1559,8 +1814,7 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
{`${currentPage} of ${totalPage}`}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${
|
||||
!isLoading && currentPage < totalPage
|
||||
className={`${!isLoading && currentPage < totalPage
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
@ -1570,8 +1824,7 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
</a>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${
|
||||
!isLoading && currentPage < totalPage
|
||||
className={`${!isLoading && currentPage < totalPage
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
@ -1602,8 +1855,7 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
onClick={onClickBackup}
|
||||
className={`${styles.menuLink} ${
|
||||
isAdmin ? styles.isActive : ""
|
||||
className={`${styles.menuLink} ${isAdmin ? styles.isActive : ""
|
||||
}`}
|
||||
>
|
||||
<img src={download} alt="" className={styles.menuIcon} />
|
||||
|
||||
@ -12,6 +12,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
LIMIT_ORDER_HISORY_NUM,
|
||||
STATUS,
|
||||
LICENSE_TYPE,
|
||||
getLicenseOrderHistoriesAsync,
|
||||
selectCurrentPage,
|
||||
selectIsLoading,
|
||||
@ -25,20 +26,21 @@ import {
|
||||
selectCompanyName,
|
||||
cancelIssueAsync,
|
||||
} from "features/license/licenseOrderHistory";
|
||||
import { selectSelectedRow } from "features/license/partnerLicense";
|
||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||
import { DelegationBar } from "components/delegate";
|
||||
import { LicenseOrder, SearchPartner, PartnerLicenseInfo } from "api/api";
|
||||
import undo from "../../assets/images/undo.svg";
|
||||
import history from "../../assets/images/history.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
interface LicenseOrderHistoryProps {
|
||||
onReturn: () => void;
|
||||
selectedRow?: PartnerLicenseInfo | SearchPartner;
|
||||
}
|
||||
export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
props
|
||||
): JSX.Element => {
|
||||
const { onReturn } = props;
|
||||
const { onReturn, selectedRow } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const total = useSelector(selectTotal);
|
||||
@ -46,7 +48,6 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
const offset = useSelector(selectOffset);
|
||||
const currentPage = useSelector(selectCurrentPage);
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
const selectedRow = useSelector(selectSelectedRow);
|
||||
// 代行操作用のトークンを取得する
|
||||
const delegationAccessToken = useSelector(selectDelegationAccessToken);
|
||||
|
||||
@ -64,6 +65,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
getLicenseOrderHistoriesAsync({
|
||||
limit: LIMIT_ORDER_HISORY_NUM,
|
||||
offset,
|
||||
selectedRow,
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -151,11 +153,15 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
getLicenseOrderHistoriesAsync({
|
||||
limit: LIMIT_ORDER_HISORY_NUM,
|
||||
offset,
|
||||
selectedRow,
|
||||
})
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, currentPage]);
|
||||
|
||||
const isNotTrialLicense = (license: LicenseOrder) =>
|
||||
license.type !== LICENSE_TYPE.TRIAL;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.wrap} ${delegationAccessToken ? styles.manage : ""}`}
|
||||
@ -208,6 +214,11 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
getTranslationID("orderHistoriesPage.label.issueDate")
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID("orderHistoriesPage.label.licenseType")
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID(
|
||||
@ -229,9 +240,10 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<tr>
|
||||
<td>{x.orderDate}</td>
|
||||
<td>{x.issueDate ? x.issueDate : "-"}</td>
|
||||
<td>{x.issueDate ?? "-"}</td>
|
||||
<td>{x.type}</td>
|
||||
<td>{x.numberOfOrder}</td>
|
||||
<td>{x.poNumber}</td>
|
||||
<td>{x.poNumber ?? "-"}</td>
|
||||
<td>
|
||||
{(() => {
|
||||
switch (x.status) {
|
||||
@ -259,7 +271,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
})()}
|
||||
</td>
|
||||
<td>
|
||||
{!selectedRow && (
|
||||
{!selectedRow && isNotTrialLicense(x) && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
@ -284,7 +296,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{selectedRow && (
|
||||
{selectedRow && isNotTrialLicense(x) && (
|
||||
<ul
|
||||
className={`${styles.menuAction} ${styles.inTable}`}
|
||||
>
|
||||
|
||||
@ -14,11 +14,11 @@ import {
|
||||
selectIsLoading,
|
||||
updateRestrictionStatusAsync,
|
||||
} from "features/license/licenseSummary";
|
||||
import { selectSelectedRow } from "features/license/partnerLicense";
|
||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||
import { DelegationBar } from "components/delegate";
|
||||
import { TIERS } from "components/auth/constants";
|
||||
import { isAdminUser, isApproveTier } from "features/auth/utils";
|
||||
import { PartnerLicenseInfo, SearchPartner } from "../../api";
|
||||
import postAdd from "../../assets/images/post_add.svg";
|
||||
import history from "../../assets/images/history.svg";
|
||||
import key from "../../assets/images/key.svg";
|
||||
@ -27,19 +27,20 @@ import circle from "../../assets/images/circle.svg";
|
||||
import returnLabel from "../../assets/images/undo.svg";
|
||||
import { LicenseOrderPopup } from "./licenseOrderPopup";
|
||||
import { CardLicenseActivatePopup } from "./cardLicenseActivatePopup";
|
||||
import { TrialLicenseIssuePopup } from "./trialLicenseIssuePopup";
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import LicenseOrderHistory from "./licenseOrderHistory";
|
||||
|
||||
interface LicenseSummaryProps {
|
||||
onReturn?: () => void;
|
||||
selectedRow?: PartnerLicenseInfo | SearchPartner;
|
||||
}
|
||||
export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
props
|
||||
): JSX.Element => {
|
||||
const { onReturn } = props;
|
||||
const { onReturn, selectedRow } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
const selectedRow = useSelector(selectSelectedRow);
|
||||
// 代行操作用のトークンを取得する
|
||||
const delegationAccessToken = useSelector(selectDelegationAccessToken);
|
||||
|
||||
@ -49,6 +50,8 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
|
||||
const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] =
|
||||
useState(false);
|
||||
const [isTrialLicenseIssuePopupOpen, setIsTrialLicenseIssuePopupOpen] =
|
||||
useState(false);
|
||||
|
||||
const onlicenseOrderOpen = useCallback(() => {
|
||||
setIslicenseOrderPopupOpen(true);
|
||||
@ -58,6 +61,10 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
setIsCardLicenseActivatePopupOpen(true);
|
||||
}, [setIsCardLicenseActivatePopupOpen]);
|
||||
|
||||
const onTrialLicenseIssueOpen = useCallback(() => {
|
||||
setIsTrialLicenseIssuePopupOpen(true);
|
||||
}, [setIsTrialLicenseIssuePopupOpen]);
|
||||
|
||||
// 呼び出し画面制御関係
|
||||
const [islicenseOrderHistoryOpen, setIsLicenseOrderHistoryOpen] =
|
||||
useState(false);
|
||||
@ -71,6 +78,7 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
const companyName = useSelector(selectCompanyName);
|
||||
|
||||
const isTier1 = isApproveTier([TIERS.TIER1]);
|
||||
const isTier2 = isApproveTier([TIERS.TIER2]);
|
||||
const isAdmin = isAdminUser();
|
||||
|
||||
useEffect(() => {
|
||||
@ -133,11 +141,21 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isTrialLicenseIssuePopupOpen && (
|
||||
<TrialLicenseIssuePopup
|
||||
onClose={() => {
|
||||
setIsTrialLicenseIssuePopupOpen(false);
|
||||
dispatch(getLicenseSummaryAsync({ selectedRow }));
|
||||
}}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
)}
|
||||
{islicenseOrderHistoryOpen && (
|
||||
<LicenseOrderHistory
|
||||
onReturn={() => {
|
||||
setIsLicenseOrderHistoryOpen(false);
|
||||
}}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
)}
|
||||
{!islicenseOrderHistoryOpen && (
|
||||
@ -228,6 +246,30 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{/* 第一階層、第二階層の管理者が第五階層アカウントのライセンス情報を見ている場合は、トライアルライセンス注文ボタンを表示 */}
|
||||
{selectedRow &&
|
||||
isAdmin &&
|
||||
selectedRow.tier.toString() === TIERS.TIER5 &&
|
||||
(isTier1 || isTier2) && (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={onTrialLicenseIssueOpen}
|
||||
>
|
||||
<img
|
||||
src={postAdd}
|
||||
alt=""
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"LicenseSummaryPage.label.issueTrialLicense"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
<div className={styles.marginRgt3}>
|
||||
<dl
|
||||
|
||||
@ -1,43 +1,57 @@
|
||||
import React, { useCallback, useState, useEffect } from "react";
|
||||
import { PartnerLicenseInfo } from "api";
|
||||
import { AppDispatch } from "app/store";
|
||||
import Footer from "components/footer";
|
||||
import Header from "components/header";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { getTranslationID } from "translation";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PartnerLicenseInfo } from "api";
|
||||
import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup";
|
||||
import postAdd from "../../assets/images/post_add.svg";
|
||||
import history from "../../assets/images/history.svg";
|
||||
import returnLabel from "../../assets/images/undo.svg";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { getTranslationID } from "translation";
|
||||
import changeOwnerIcon from "../../assets/images/change_circle.svg";
|
||||
import { isApproveTier } from "../../features/auth/utils";
|
||||
import history from "../../assets/images/history.svg";
|
||||
import postAdd from "../../assets/images/post_add.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
import returnLabel from "../../assets/images/undo.svg";
|
||||
import searchIcon from "../../assets/images/search.svg";
|
||||
import { TIERS } from "../../components/auth/constants";
|
||||
import { isApproveTier } from "../../features/auth/utils";
|
||||
import {
|
||||
getPartnerLicenseAsync,
|
||||
ACCOUNTS_VIEW_LIMIT,
|
||||
selectMyAccountInfo,
|
||||
selectTotal,
|
||||
selectOwnPartnerLicense,
|
||||
selectChildrenPartnerLicenses,
|
||||
selectHierarchicalElements,
|
||||
selectTotalPage,
|
||||
selectIsLoading,
|
||||
selectOffset,
|
||||
selectCurrentPage,
|
||||
pushHierarchicalElement,
|
||||
changeSelectedRow,
|
||||
getMyAccountAsync,
|
||||
getPartnerLicenseAsync,
|
||||
popHierarchicalElement,
|
||||
pushHierarchicalElement,
|
||||
spliceHierarchicalElement,
|
||||
savePageInfo,
|
||||
getMyAccountAsync,
|
||||
changeSelectedRow,
|
||||
setIsLicenseOrderHistoryOpen,
|
||||
setIsViewDetailsOpen,
|
||||
selectChildrenPartnerLicenses,
|
||||
selectCurrentPage,
|
||||
selectHierarchicalElements,
|
||||
selectIsLoading,
|
||||
selectMyAccountInfo,
|
||||
selectOffset,
|
||||
selectOwnPartnerLicense,
|
||||
selectTotal,
|
||||
selectTotalPage,
|
||||
selectSelectedRow,
|
||||
selectIsLicenseOrderHistoryOpen,
|
||||
selectIsViewDetailsOpen,
|
||||
setIsSearchPopupOpen,
|
||||
selectIsSearchPopupOpen,
|
||||
} from "../../features/license/partnerLicense";
|
||||
import { LicenseOrderPopup } from "./licenseOrderPopup";
|
||||
import { LicenseOrderHistory } from "./licenseOrderHistory";
|
||||
import { LicenseSummary } from "./licenseSummary";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
import {
|
||||
selectIsViewDetailsOpen as selectIsViewDetailsInSearchOpen,
|
||||
selectIsLicenseOrderHistoryOpen as selectIsLicenseOrderHistoryInSearchOpen,
|
||||
} from "../../features/license/searchPartner";
|
||||
import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup";
|
||||
import ChangeOwnerPopup from "./changeOwnerPopup";
|
||||
import { LicenseOrderHistory } from "./licenseOrderHistory";
|
||||
import { LicenseOrderPopup } from "./licenseOrderPopup";
|
||||
import { LicenseSummary } from "./licenseSummary";
|
||||
import { SearchPartnerPopup } from "./searchPartnerAccountPopup";
|
||||
|
||||
const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
@ -47,9 +61,21 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
const [isCardLicenseIssuePopupOpen, setIsCardLicenseIssuePopupOpen] =
|
||||
useState(false);
|
||||
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
|
||||
const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] =
|
||||
useState(false);
|
||||
const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false);
|
||||
|
||||
// パートナーライセンス画面のOrderHistory, ViewDetailsの表示制御
|
||||
const isLicenseOrderHistoryOpen = useSelector(
|
||||
selectIsLicenseOrderHistoryOpen
|
||||
);
|
||||
const isViewDetailsOpen = useSelector(selectIsViewDetailsOpen);
|
||||
|
||||
// パートナー検索ポップアップのOrderHistory, ViewDetailsの表示制御
|
||||
const isLicenseOrderHistoryInSearchOpen = useSelector(
|
||||
selectIsLicenseOrderHistoryInSearchOpen
|
||||
);
|
||||
const isViewDetailsInSearchOpen = useSelector(
|
||||
selectIsViewDetailsInSearchOpen
|
||||
);
|
||||
const isSearchPopupOpen = useSelector(selectIsSearchPopupOpen);
|
||||
const [isChangeOwnerPopupOpen, setIsChangeOwnerPopupOpen] = useState(false);
|
||||
|
||||
// 階層表示用
|
||||
@ -84,6 +110,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
);
|
||||
const hierarchicalElements = useSelector(selectHierarchicalElements);
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
const selectedRow = useSelector(selectSelectedRow) as PartnerLicenseInfo;
|
||||
|
||||
// ページネーション制御用
|
||||
const currentPage = useSelector(selectCurrentPage);
|
||||
@ -136,18 +163,18 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
const onClickViewDetails = useCallback(
|
||||
(value?: PartnerLicenseInfo) => {
|
||||
dispatch(changeSelectedRow({ value }));
|
||||
setIsViewDetailsOpen(true);
|
||||
dispatch(setIsViewDetailsOpen({ value: true }));
|
||||
},
|
||||
[dispatch, setIsViewDetailsOpen]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// orderHistoryボタン押下時
|
||||
const onClickOrderHistory = useCallback(
|
||||
(value?: PartnerLicenseInfo) => {
|
||||
dispatch(changeSelectedRow({ value }));
|
||||
setIslicenseOrderHistoryOpen(true);
|
||||
dispatch(setIsLicenseOrderHistoryOpen({ value: true }));
|
||||
},
|
||||
[dispatch, setIslicenseOrderHistoryOpen]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// changeOwnerボタン押下時
|
||||
@ -155,6 +182,10 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
setIsChangeOwnerPopupOpen(true);
|
||||
}, [setIsChangeOwnerPopupOpen]);
|
||||
|
||||
const onOpenSearchPopup = useCallback(() => {
|
||||
dispatch(setIsSearchPopupOpen({ value: true }));
|
||||
}, [dispatch]);
|
||||
|
||||
// マウント時のみ実行
|
||||
useEffect(() => {
|
||||
dispatch(getMyAccountAsync());
|
||||
@ -176,7 +207,8 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
}, [myAccountInfo]);
|
||||
|
||||
// 現在の表示階層に合わせたボタン制御用
|
||||
const [buttonLabel, setButtonLabel] = useState("");
|
||||
const [showOrderHistoryButton, setShowOrderHistoryButton] = useState(false);
|
||||
const [showViewDetailsButton, setShowViewDetailsButton] = useState(false);
|
||||
|
||||
// パンくずリスト用stateに自アカウントを追加
|
||||
useEffect(() => {
|
||||
@ -194,15 +226,17 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
);
|
||||
}
|
||||
// 表内のボタン表示判定
|
||||
if (hierarchicalElements.length === 1 && ownPartnerLicenseInfo.tier !== 4) {
|
||||
setButtonLabel(
|
||||
t(getTranslationID("partnerLicense.label.orderHistoryButton"))
|
||||
);
|
||||
if (ownPartnerLicenseInfo.tier !== 4) {
|
||||
setShowOrderHistoryButton(true);
|
||||
setShowViewDetailsButton(false);
|
||||
} else if (ownPartnerLicenseInfo.tier === 4) {
|
||||
setButtonLabel(t(getTranslationID("partnerLicense.label.viewDetails")));
|
||||
setShowOrderHistoryButton(true);
|
||||
setShowViewDetailsButton(true);
|
||||
} else {
|
||||
setButtonLabel("");
|
||||
setShowOrderHistoryButton(false);
|
||||
setShowViewDetailsButton(false);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ownPartnerLicenseInfo]);
|
||||
|
||||
@ -221,6 +255,21 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hierarchicalElements, currentPage]);
|
||||
|
||||
// パートナーライセンス画面からも検索ポップアップからもOrder History/View Detailsが表示されていない時に表示
|
||||
const isVisiblePartnerLicensePage = useMemo(
|
||||
() =>
|
||||
!isLicenseOrderHistoryInSearchOpen &&
|
||||
!isViewDetailsInSearchOpen &&
|
||||
!isLicenseOrderHistoryOpen &&
|
||||
!isViewDetailsOpen,
|
||||
[
|
||||
isLicenseOrderHistoryInSearchOpen,
|
||||
isViewDetailsInSearchOpen,
|
||||
isLicenseOrderHistoryOpen,
|
||||
isViewDetailsOpen,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* isPopupOpenがfalseの場合はポップアップのhtmlを生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */}
|
||||
@ -238,18 +287,20 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{islicenseOrderHistoryOpen && (
|
||||
{isLicenseOrderHistoryOpen && (
|
||||
<LicenseOrderHistory
|
||||
onReturn={() => {
|
||||
setIslicenseOrderHistoryOpen(false);
|
||||
dispatch(setIsLicenseOrderHistoryOpen({ value: false }));
|
||||
}}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
)}
|
||||
{isViewDetailsOpen && (
|
||||
<LicenseSummary
|
||||
onReturn={() => {
|
||||
setIsViewDetailsOpen(false);
|
||||
dispatch(setIsViewDetailsOpen({ value: false }));
|
||||
}}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
)}
|
||||
{isChangeOwnerPopupOpen && (
|
||||
@ -259,7 +310,12 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
|
||||
{isVisiblePartnerSearch() && isSearchPopupOpen && (
|
||||
<SearchPartnerPopup
|
||||
onClose={() => dispatch(setIsSearchPopupOpen({ value: true }))}
|
||||
/>
|
||||
)}
|
||||
{isVisiblePartnerLicensePage && (
|
||||
<div className={styles.wrap}>
|
||||
<Header />
|
||||
<main className={styles.main}>
|
||||
@ -362,6 +418,22 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
<li className={styles.floatRight}>
|
||||
{isVisiblePartnerSearch() && (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive} ${styles.alignRight}`}
|
||||
onClick={onOpenSearchPopup}
|
||||
>
|
||||
<img
|
||||
src={searchIcon}
|
||||
alt="search"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(getTranslationID("partnerLicense.label.search"))}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
<ul className={styles.brCrumbLicense}>
|
||||
{hierarchicalElements.map((value) => (
|
||||
@ -388,6 +460,13 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
<th>
|
||||
{t(getTranslationID("partnerLicense.label.stockLicense"))}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerLicense.label.allocatedLicense"
|
||||
)
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
{t(
|
||||
getTranslationID("partnerLicense.label.issueRequested")
|
||||
@ -413,6 +492,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
? ownPartnerLicenseInfo.stockLicense
|
||||
: "-"}
|
||||
</td>
|
||||
<td>-</td>
|
||||
<td>{ownPartnerLicenseInfo.issuedRequested}</td>
|
||||
<td>
|
||||
<span
|
||||
@ -456,6 +536,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
<td>{tierNames[value.tier]}</td>
|
||||
<td>{value.accountId}</td>
|
||||
<td>{value.stockLicense}</td>
|
||||
<td>{value.tier === 5 ? value.allocatedLicense : "-"}</td>
|
||||
<td>{value.issuedRequested}</td>
|
||||
<td>
|
||||
<span
|
||||
@ -472,20 +553,38 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${styles.menuLink} ${
|
||||
buttonLabel ? styles.isActive : ""
|
||||
className={`${styles.menuLink} ${showOrderHistoryButton ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (ownPartnerLicenseInfo.tier === 4) {
|
||||
onClickViewDetails(value);
|
||||
} else {
|
||||
onClickOrderHistory(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{buttonLabel}
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerLicense.label.orderHistoryButton"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{/* Second button (only if tier is 4) */}
|
||||
{showViewDetailsButton && (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<a
|
||||
className={`${styles.menuLink} ${showViewDetailsButton ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
onClickViewDetails(value);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerLicense.label.viewDetails"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
@ -513,8 +612,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
onClick={() => {
|
||||
movePage(0);
|
||||
}}
|
||||
className={` ${
|
||||
!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
className={` ${!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
>
|
||||
«
|
||||
@ -524,22 +622,19 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
onClick={() => {
|
||||
movePage((currentPage - 2) * ACCOUNTS_VIEW_LIMIT);
|
||||
}}
|
||||
className={`${
|
||||
!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
className={`${!isLoading && currentPage !== 1 ? styles.isActive : ""
|
||||
}`}
|
||||
>
|
||||
‹
|
||||
</a>
|
||||
{` ${total !== 0 ? currentPage : 0} of ${
|
||||
total !== 0 ? totalPage : 0
|
||||
{` ${total !== 0 ? currentPage : 0} of ${total !== 0 ? totalPage : 0
|
||||
} `}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
onClick={() => {
|
||||
movePage(currentPage * ACCOUNTS_VIEW_LIMIT);
|
||||
}}
|
||||
className={`${
|
||||
!isLoading && currentPage < totalPage
|
||||
className={`${!isLoading && currentPage < totalPage
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
@ -551,8 +646,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
onClick={() => {
|
||||
movePage((totalPage - 1) * ACCOUNTS_VIEW_LIMIT);
|
||||
}}
|
||||
className={` ${
|
||||
!isLoading && currentPage < totalPage
|
||||
className={` ${!isLoading && currentPage < totalPage
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
@ -584,4 +678,8 @@ const isVisibleChangeOwner = (partnerTier: number) =>
|
||||
(partnerTier.toString() === TIERS.TIER3 ||
|
||||
partnerTier.toString() === TIERS.TIER4);
|
||||
|
||||
const isVisiblePartnerSearch = () =>
|
||||
// 自身が第一階層〜第四階層の場合のみ表示
|
||||
isApproveTier([TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4]);
|
||||
|
||||
export default PartnerLicense;
|
||||
|
||||
@ -0,0 +1,354 @@
|
||||
import { SearchPartner } from "api";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { AppDispatch } from "app/store";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import {
|
||||
changeSelectedRow,
|
||||
cleanupSearchResult,
|
||||
cleanupPartnerHierarchy,
|
||||
setIsLicenseOrderHistoryOpen,
|
||||
setIsViewDetailsOpen,
|
||||
searchPartnersAsync,
|
||||
selectIsLoading,
|
||||
selectSelectedRow,
|
||||
selectSearchResult,
|
||||
selectPartnerHierarchy,
|
||||
selectIsLicenseOrderHistoryOpen,
|
||||
selectIsViewDetailsOpen,
|
||||
getPartnerHierarchy,
|
||||
} from "features/license/searchPartner";
|
||||
import { setIsSearchPopupOpen } from "features/license/partnerLicense";
|
||||
import { LicenseSummary } from "./licenseSummary";
|
||||
import { LicenseOrderHistory } from "./licenseOrderHistory";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import searchIcon from "../../assets/images/search.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
interface SearchPopupProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const SearchPartnerPopup: React.FC<SearchPopupProps> = (props) => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { onClose } = props;
|
||||
const [t] = useTranslation();
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
const selectedRow = useSelector(selectSelectedRow) as SearchPartner;
|
||||
const searchResult = useSelector(selectSearchResult);
|
||||
const partnerHierarchy = useSelector(selectPartnerHierarchy);
|
||||
const [accountId, setAccountId] = useState("");
|
||||
const [companyName, setCompanyName] = useState("");
|
||||
const [isBreadcrumbOpen, setIsBreadcrumbOpen] = useState(false);
|
||||
const isViewDetailsOpen = useSelector(selectIsViewDetailsOpen);
|
||||
const isLicenseOrderHistoryOpen = useSelector(
|
||||
selectIsLicenseOrderHistoryOpen
|
||||
);
|
||||
const [popupPosition, setPopupPosition] = useState<{ x: number; y: number }>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const breadcrumbRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// フォームの入力チェック
|
||||
const searchButtonEnabled = useMemo(() => {
|
||||
// 両方入力がない場合はボタンを活性化しない。
|
||||
if (!companyName && !accountId) {
|
||||
return false;
|
||||
}
|
||||
// Company Nameは3文字以上入力がない場合はボタンを活性化しない。
|
||||
// サロゲートペアを考慮して、スプレッド構文でリスト化してから文字数をカウントする
|
||||
// 絵文字が入力された場合は救えないが、そもそも入力されても検索できないので考慮しない。
|
||||
if (companyName && [...companyName].length <= 2) {
|
||||
return false;
|
||||
}
|
||||
// Account IDは数字ではないまたは0以下の場合はボタンを活性化しない。
|
||||
if (
|
||||
accountId &&
|
||||
(Number.isNaN(Number(accountId)) || Number(accountId) <= 0)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [companyName, accountId]);
|
||||
|
||||
const requestSearch = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (!searchButtonEnabled) return;
|
||||
dispatch(
|
||||
searchPartnersAsync({
|
||||
companyName,
|
||||
accountId: Number(accountId),
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, companyName, accountId, searchButtonEnabled]
|
||||
);
|
||||
|
||||
const handleAccountNameClick = useCallback(
|
||||
async (clickAccountId: number, event: React.MouseEvent) => {
|
||||
// ロード中はなにもしない。
|
||||
if (isLoading) return;
|
||||
event.stopPropagation();
|
||||
// アカウントの階層を取得
|
||||
await dispatch(getPartnerHierarchy({ accountId: clickAccountId }));
|
||||
setPopupPosition({ x: event.clientX, y: event.clientY });
|
||||
setIsBreadcrumbOpen(true);
|
||||
},
|
||||
[dispatch, setPopupPosition, setIsBreadcrumbOpen, isLoading]
|
||||
);
|
||||
|
||||
const closeBreadcrumbPopup = () => {
|
||||
setIsBreadcrumbOpen(false);
|
||||
};
|
||||
|
||||
// ポップアップ外のクリックポップアップ非表示するイベント
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
breadcrumbRef.current &&
|
||||
!breadcrumbRef.current.contains(event.target as Node)
|
||||
) {
|
||||
closeBreadcrumbPopup();
|
||||
}
|
||||
};
|
||||
if (isBreadcrumbOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
} else {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [isBreadcrumbOpen]);
|
||||
|
||||
const tierNames: { [key: number]: string } = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
1: t(getTranslationID("common.label.tier1")),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
2: t(getTranslationID("common.label.tier2")),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
3: t(getTranslationID("common.label.tier3")),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
4: t(getTranslationID("common.label.tier4")),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
5: t(getTranslationID("common.label.tier5")),
|
||||
};
|
||||
|
||||
const openViewDetails = useCallback(
|
||||
(value: SearchPartner) => {
|
||||
dispatch(changeSelectedRow({ value }));
|
||||
dispatch(setIsViewDetailsOpen({ value: true }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const openOrderHistory = useCallback(
|
||||
(value?: SearchPartner) => {
|
||||
dispatch(changeSelectedRow({ value }));
|
||||
dispatch(setIsLicenseOrderHistoryOpen({ value: true }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
// ポップアップ閉じたら、検索結果をクリアする
|
||||
dispatch(cleanupSearchResult());
|
||||
dispatch(cleanupPartnerHierarchy());
|
||||
onClose();
|
||||
dispatch(setIsSearchPopupOpen({ value: false }));
|
||||
}, [dispatch, onClose]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div>
|
||||
{isViewDetailsOpen && (
|
||||
<div>
|
||||
<LicenseSummary
|
||||
onReturn={() => dispatch(setIsViewDetailsOpen({ value: false }))}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isLicenseOrderHistoryOpen && (
|
||||
<div>
|
||||
<LicenseOrderHistory
|
||||
onReturn={() =>
|
||||
dispatch(setIsLicenseOrderHistoryOpen({ value: false }))
|
||||
}
|
||||
selectedRow={selectedRow}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={`${styles.modal} ${styles.isShow}`}>
|
||||
<div className={styles.searchModalBox}>
|
||||
<div className={styles.headerContainer}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("searchPartnerAccountPopupPage.label.title"))}
|
||||
</p>
|
||||
<form
|
||||
className={styles.searchBar}
|
||||
onSubmit={(e) => requestSearch(e)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t(getTranslationID("partnerLicense.label.name"))}
|
||||
value={companyName}
|
||||
onChange={(e) => setCompanyName(e.target.value.trimStart())}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t(
|
||||
getTranslationID("partnerLicense.label.accountId")
|
||||
)}
|
||||
value={accountId}
|
||||
onChange={(e) => setAccountId(e.target.value.trimStart())}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<button
|
||||
className={`${styles.menuLink} ${!isLoading && searchButtonEnabled ? styles.isActive : ""
|
||||
}`}
|
||||
type="submit"
|
||||
disabled={!searchButtonEnabled || isLoading}
|
||||
>
|
||||
<img
|
||||
src={searchIcon}
|
||||
alt="search"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(getTranslationID("partnerLicense.label.search"))}
|
||||
</button>
|
||||
<button type="button" onClick={handleModalClose}>
|
||||
<img
|
||||
src={close}
|
||||
className={styles.modalTitleIcon}
|
||||
alt="close"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<table
|
||||
className={`${styles.table} ${styles.partner} ${styles.marginBtm3}`}
|
||||
>
|
||||
<tr className={styles.tableHeader}>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.name"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.category"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.accountId"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.country"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.primaryAdmin"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{t(getTranslationID("partnerPage.label.email"))}</a>
|
||||
</th>
|
||||
<th>
|
||||
<a>{"" /** Order History、View Details用の空カラム */}</a>
|
||||
</th>
|
||||
</tr>
|
||||
{searchResult.map((result) => (
|
||||
<tr key={result.accountId}>
|
||||
<td
|
||||
onClick={(event) =>
|
||||
handleAccountNameClick(result.accountId, event)
|
||||
}
|
||||
className={styles.hoverBlue}
|
||||
>
|
||||
{result.name}
|
||||
</td>
|
||||
<td>{tierNames[result.tier]}</td>
|
||||
<td>{result.accountId}</td>
|
||||
<td>{result.country}</td>
|
||||
<td>{result.primaryAdmin}</td>
|
||||
<td>{result.email ?? "-"}</td>
|
||||
<td>
|
||||
<ul className={`${styles.menuAction} ${styles.inTable}`}>
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={() => {
|
||||
openOrderHistory(result);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerLicense.label.orderHistoryButton"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
{result.tier === 5 && (
|
||||
<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={() => {
|
||||
openViewDetails(result);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID("partnerLicense.label.viewDetails")
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
{searchResult.length === 0 && (
|
||||
<p style={{ margin: "10px", textAlign: "center" }}>
|
||||
{t(getTranslationID("common.message.listEmpty"))}
|
||||
</p>
|
||||
)}
|
||||
{/* ローディングオーバーレイ */}
|
||||
<div
|
||||
style={{ display: isLoading ? "inline" : "none" }}
|
||||
className={styles.overlay}
|
||||
>
|
||||
<img
|
||||
src={progress_activit}
|
||||
className={`${styles.icLoading} ${styles.alignCenter}`}
|
||||
alt="Loading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isBreadcrumbOpen && (
|
||||
<div
|
||||
ref={breadcrumbRef}
|
||||
className={styles.breadcrumbPopup}
|
||||
style={{ top: popupPosition.y, left: popupPosition.x }}
|
||||
>
|
||||
<ul className={styles.brCrumbPartner}>
|
||||
{partnerHierarchy.map((parent) => (
|
||||
<li key={parent.tier}>
|
||||
<span>{parent.name}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,123 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
issueTrialLicenseAsync,
|
||||
cleanupApps,
|
||||
selectIsLoading,
|
||||
selectNumberOfLicenses,
|
||||
selectExpirationDate,
|
||||
setExpirationDate,
|
||||
} from "features/license/licenseTrialIssue";
|
||||
import styles from "../../styles/app.module.scss";
|
||||
import { getTranslationID } from "../../translation";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
import { SearchPartner, PartnerLicenseInfo } from "../../api";
|
||||
|
||||
interface TrialLicenseIssuePopupProps {
|
||||
onClose: () => void;
|
||||
selectedRow?: PartnerLicenseInfo | SearchPartner;
|
||||
}
|
||||
|
||||
export const TrialLicenseIssuePopup: React.FC<TrialLicenseIssuePopupProps> = (
|
||||
props
|
||||
) => {
|
||||
const { onClose, selectedRow } = props;
|
||||
const { t } = useTranslation();
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
|
||||
const numberOfLicenses = useSelector(selectNumberOfLicenses);
|
||||
const expirationDate = useSelector(selectExpirationDate);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
// useEffectのreturnとしてcleanupAppsを実行することで、ポップアップのアンマウント時に初期化を行う
|
||||
dispatch(cleanupApps());
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// ポップアップ表示時
|
||||
useEffect(() => {
|
||||
// トライアルライセンスの有効期限を設定。
|
||||
dispatch(setExpirationDate());
|
||||
}, [dispatch]);
|
||||
|
||||
// ポップアップを閉じる処理
|
||||
const closePopup = useCallback(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
onClose();
|
||||
}, [isLoading, onClose]);
|
||||
|
||||
// 発行ボタン押下時
|
||||
const onIssueTrialLicense = useCallback(async () => {
|
||||
// トライアルライセンス発行APIの呼び出し
|
||||
const { meta } = await dispatch(issueTrialLicenseAsync({ selectedRow }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
}
|
||||
}, [dispatch, closePopup, selectedRow]);
|
||||
|
||||
// HTML
|
||||
return (
|
||||
<div className={`${styles.modal} ${styles.isShow}`}>
|
||||
<div className={styles.modalBox}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("trialLicenseIssuePopupPage.label.title"))}
|
||||
<button type="button" onClick={closePopup}>
|
||||
<img src={close} className={styles.modalTitleIcon} alt="close" />
|
||||
</button>
|
||||
</p>
|
||||
<form className={styles.form}>
|
||||
<dl className={`${styles.formList} ${styles.hasbg}`}>
|
||||
<dt className={styles.formTitle}>
|
||||
{t(getTranslationID("trialLicenseIssuePopupPage.label.subTitle"))}
|
||||
</dt>
|
||||
<dt className={styles.overLine}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"trialLicenseIssuePopupPage.label.numberOfLicenses"
|
||||
)
|
||||
)}
|
||||
</dt>
|
||||
<dd>{numberOfLicenses}</dd>
|
||||
<dt>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"trialLicenseIssuePopupPage.label.expirationDate"
|
||||
)
|
||||
)}
|
||||
</dt>
|
||||
<dd>{expirationDate}</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<input
|
||||
type="button"
|
||||
name="submit"
|
||||
value={t(
|
||||
getTranslationID(
|
||||
"trialLicenseIssuePopupPage.label.issueButton"
|
||||
)
|
||||
)}
|
||||
className={`${styles.formSubmit} ${styles.marginBtm1} ${
|
||||
!isLoading ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={onIssueTrialLicense}
|
||||
/>
|
||||
<img
|
||||
style={{ display: isLoading ? "inline" : "none" }}
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -28,12 +28,13 @@ import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
interface AllocateLicensePopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
clearUserSearchInputs: () => void;
|
||||
}
|
||||
|
||||
export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
|
||||
props
|
||||
) => {
|
||||
const { isOpen, onClose } = props;
|
||||
const { isOpen, onClose, clearUserSearchInputs } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -87,6 +88,7 @@ export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
}, [dispatch, closePopup, id, selectedlicenseId, hasErrorEmptyLicense]);
|
||||
@ -219,8 +221,7 @@ export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
|
||||
value={selectedlicenseId ?? ""}
|
||||
>
|
||||
<option value="" hidden>
|
||||
{`--
|
||||
${t(
|
||||
{`-- ${t(
|
||||
getTranslationID(
|
||||
"allocateLicensePopupPage.label.dropDownHeading"
|
||||
)
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
selectIsLoading,
|
||||
deallocateLicenseAsync,
|
||||
deleteUserAsync,
|
||||
confirmUserForceAsync,
|
||||
} from "features/user";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
@ -32,6 +33,7 @@ import checkFill from "../../assets/images/check_fill.svg";
|
||||
import checkOutline from "../../assets/images/check_outline.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
import upload from "../../assets/images/upload.svg";
|
||||
import searchIcon from "../../assets/images/search.svg";
|
||||
import { UserAddPopup } from "./popup";
|
||||
import { UserUpdatePopup } from "./updatePopup";
|
||||
import { AllocateLicensePopup } from "./allocateLicensePopup";
|
||||
@ -48,6 +50,8 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] =
|
||||
useState(false);
|
||||
const [isImportPopupOpen, setIsImportPopupOpen] = useState(false);
|
||||
const [searchEmail, setSearchEmail] = useState("");
|
||||
const [searchUserName, setSearchUserName] = useState("");
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setIsPopupOpen(true);
|
||||
@ -84,6 +88,7 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
|
||||
const { meta } = await dispatch(deallocateLicenseAsync({ userId }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
},
|
||||
@ -102,12 +107,53 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
|
||||
const { meta } = await dispatch(deleteUserAsync({ userId }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
},
|
||||
[dispatch, t]
|
||||
);
|
||||
|
||||
const onForceEmailVerification = useCallback(
|
||||
async (userId: number) => {
|
||||
// ダイアログ確認
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(
|
||||
t(
|
||||
getTranslationID(
|
||||
"userListPage.message.forceEmailVerificationConfirm"
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { meta } = await dispatch(confirmUserForceAsync({ userId }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
},
|
||||
[dispatch, t]
|
||||
);
|
||||
|
||||
const requestSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
dispatch(
|
||||
listUsersAsync({
|
||||
userInputUserName: searchUserName,
|
||||
userInputEmail: searchEmail,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const clearUserSearchInputs = useCallback(() => {
|
||||
setSearchEmail("");
|
||||
setSearchUserName("");
|
||||
}, [setSearchEmail, setSearchUserName]);
|
||||
|
||||
useEffect(() => {
|
||||
// ユーザ一覧取得処理を呼び出す
|
||||
dispatch(listUsersAsync());
|
||||
@ -126,18 +172,21 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
onClose={() => {
|
||||
setIsUpdatePopupOpen(false);
|
||||
}}
|
||||
clearUserSearchInputs={clearUserSearchInputs}
|
||||
/>
|
||||
<UserAddPopup
|
||||
isOpen={isPopupOpen}
|
||||
onClose={() => {
|
||||
setIsPopupOpen(false);
|
||||
}}
|
||||
clearUserSearchInputs={clearUserSearchInputs}
|
||||
/>
|
||||
<AllocateLicensePopup
|
||||
isOpen={isAllocateLicensePopupOpen}
|
||||
onClose={() => {
|
||||
setIsAllocateLicensePopupOpen(false);
|
||||
}}
|
||||
clearUserSearchInputs={clearUserSearchInputs}
|
||||
/>
|
||||
<ImportPopup
|
||||
isOpen={isImportPopupOpen}
|
||||
@ -185,6 +234,50 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
{t(getTranslationID("userListPage.label.bulkImport"))}
|
||||
</a>
|
||||
</li>
|
||||
<li className={styles.floatRight}>
|
||||
<form
|
||||
className={styles.searchBar}
|
||||
onSubmit={(e) => requestSearch(e)}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t(
|
||||
getTranslationID("userListPage.label.name")
|
||||
)}
|
||||
value={searchUserName}
|
||||
onChange={(e) =>
|
||||
setSearchUserName(e.target.value.trimStart())
|
||||
}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t(
|
||||
getTranslationID("userListPage.label.email")
|
||||
)}
|
||||
value={searchEmail}
|
||||
onChange={(e) =>
|
||||
setSearchEmail(e.target.value.trimStart())
|
||||
}
|
||||
className={styles.searchInput}
|
||||
/>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<button
|
||||
className={`${styles.menuLink} ${
|
||||
!isLoading ? styles.isActive : ""
|
||||
}`}
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<img
|
||||
src={searchIcon}
|
||||
alt="search"
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(getTranslationID("userListPage.label.search"))}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={styles.tableWrap}>
|
||||
<table className={`${styles.table} ${styles.user}`}>
|
||||
@ -308,6 +401,23 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
{/* 第五階層の管理者が、メール認証済みではないユーザーの行をマウスオーバーしている場合のみ */}
|
||||
{isTier5 && !user.emailVerified && (
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
onClick={() => {
|
||||
onForceEmailVerification(user.id);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"userListPage.label.forceEmailVerification"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</td>
|
||||
<td> {user.name}</td>
|
||||
|
||||
@ -28,10 +28,11 @@ import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
interface UserAddPopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
clearUserSearchInputs: () => void;
|
||||
}
|
||||
|
||||
export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
const { isOpen, onClose, clearUserSearchInputs } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [isPasswordHide, setIsPasswordHide] = useState<boolean>(true);
|
||||
@ -75,6 +76,7 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
}, [
|
||||
|
||||
@ -28,10 +28,11 @@ import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
interface UserUpdatePopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
clearUserSearchInputs: () => void;
|
||||
}
|
||||
|
||||
export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
const { isOpen, onClose, clearUserSearchInputs } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const closePopup = useCallback(() => {
|
||||
@ -79,6 +80,7 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
clearUserSearchInputs();
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
}, [
|
||||
|
||||
@ -454,7 +454,6 @@ h3 + .brCrumb .tlIcon {
|
||||
.brCrumbLicense li a:hover {
|
||||
color: #0084b2;
|
||||
}
|
||||
|
||||
.buttonNormal {
|
||||
display: inline-block;
|
||||
width: 15rem;
|
||||
@ -1699,9 +1698,9 @@ _:-ms-lang(x)::-ms-backdrop,
|
||||
margin-left: 648px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.menuAction {
|
||||
margin-bottom: 0.6rem;
|
||||
position: relative;
|
||||
}
|
||||
.menuAction li {
|
||||
display: inline-block;
|
||||
@ -2059,6 +2058,9 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
height: 34px;
|
||||
position: relative;
|
||||
}
|
||||
.dictation .menuAction:not(:first-child) {
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
.dictation .menuAction .alignLeft {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@ -2765,4 +2767,106 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.modal.isShow .searchModalBox {
|
||||
display: block;
|
||||
}
|
||||
.searchModalBox {
|
||||
display: none;
|
||||
width: 70vw; /* 70% of the viewport width */
|
||||
height: 70vh; /* 70% of the viewport height */
|
||||
max-height: 95vh;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 0.3rem;
|
||||
overflow: auto;
|
||||
background-color: #fff;
|
||||
padding: 1rem;
|
||||
}
|
||||
.searchBar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.searchInput {
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
margin-left: -30px;
|
||||
}
|
||||
.breadcrumbPopup {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #000;
|
||||
padding: 0.3rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
.brCrumbPartner {
|
||||
margin: 0.5rem 0 0.3rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
.brCrumbPartner li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.04rem;
|
||||
white-space: nowrap;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.brCrumbPartner li:not(:last-child)::after {
|
||||
content: "";
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 7px solid #333333;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.hoverBlue {
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
&:hover {
|
||||
color: #0084b2;
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgb(255, 255, 255);
|
||||
opacity: 0.5;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.overlay .icLoading {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=style.css.map */
|
||||
|
||||
@ -233,5 +233,13 @@ declare const classNames: {
|
||||
readonly txContents: "txContents";
|
||||
readonly txIcon: "txIcon";
|
||||
readonly txWswrap: "txWswrap";
|
||||
readonly searchModalBox: "searchModalBox";
|
||||
readonly searchBar: "searchBar";
|
||||
readonly searchInput: "searchInput";
|
||||
readonly headerContainer: "headerContainer";
|
||||
readonly breadcrumbPopup: "breadcrumbPopup";
|
||||
readonly brCrumbPartner: "brCrumbPartner";
|
||||
readonly hoverBlue: "hoverBlue";
|
||||
readonly overlay: "overlay";
|
||||
};
|
||||
export = classNames;
|
||||
|
||||
@ -262,13 +262,13 @@
|
||||
"taskNotEditable": "Der Transkriptionist kann nicht geändert werden, da die Transkription bereits ausgeführt wird oder die Datei nicht vorhanden ist. Bitte aktualisieren Sie den Bildschirm und prüfen Sie den aktuellen Status.",
|
||||
"backupFailedError": "Der Prozess „Dateisicherung“ ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal. Wenn der Fehler weiterhin besteht, wenden Sie sich an Ihren Systemadministrator.",
|
||||
"cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.",
|
||||
"reopenFailedError": "Der Status kann nicht in „Ausstehend“ geändert werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.",
|
||||
"deleteFailedError": "Die Aufgabe konnte nicht gelöscht werden. Bitte aktualisieren Sie den Bildschirm und überprüfen Sie ihn erneut.",
|
||||
"licenseNotAssignedError": "Die Transkription ist nicht möglich, da keine gültige Lizenz zugewiesen ist. Bitten Sie Ihren Administrator, eine gültige Lizenz zuzuweisen.",
|
||||
"licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen.",
|
||||
"fileAlreadyDeletedError": "Die Bildschirminformationen sind nicht aktuell, sie enthalten bereits gelöschte Audiodateien. Bitte aktualisieren Sie den Bildschirm und wählen Sie die zu löschenden Dateien erneut aus.",
|
||||
"fileRenameFailedError": "Da die Bildschirminformationen nicht aktuell sind, ist eine Inkonsistenz in den Dateiinformationen aufgetreten und die Datei konnte nicht umbenannt werden. Bitte aktualisieren Sie den Bildschirm und versuchen Sie es erneut.",
|
||||
"fileNameAleadyExistsError": "Dieser Dateiname ist bereits registriert. Bitte registrieren Sie sich mit einem anderen Dateinamen.",
|
||||
"reopenFailedError": "Der Status kann nicht in „Ausstehend“ geändert werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut."
|
||||
"fileNameAleadyExistsError": "Dieser Dateiname ist bereits registriert. Bitte registrieren Sie sich mit einem anderen Dateinamen."
|
||||
},
|
||||
"label": {
|
||||
"title": "Diktate",
|
||||
@ -389,7 +389,7 @@
|
||||
"viewDetails": "Details anzeigen",
|
||||
"accounts": "konten",
|
||||
"changeOwnerButton": "Change Owner",
|
||||
"allocatedlicense": "Zugewiesene Lizenzen",
|
||||
"allocatedLicense": "Zugewiesene Lizenzen",
|
||||
"search": "Suche"
|
||||
}
|
||||
},
|
||||
|
||||
@ -262,13 +262,13 @@
|
||||
"taskNotEditable": "The transcriptionist cannot be changed because the transcription is already in progress or the file does not exist. Please refresh the screen and check the latest status.",
|
||||
"backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.",
|
||||
"cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.",
|
||||
"reopenFailedError": "The status could not be changed to Pending. Please refresh the screen to see the current status. Only files with Finished status can be operated.",
|
||||
"deleteFailedError": "Failed to delete the task. Please refresh the screen and check again.",
|
||||
"licenseNotAssignedError": "Transcription is not possible because a valid license is not assigned. Please ask your administrator to assign a valid license.",
|
||||
"licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license.",
|
||||
"fileAlreadyDeletedError": "The screen information is not up to date, it contains audio files that have already been deleted. Please refresh the screen, and select the files to delete again.",
|
||||
"fileRenameFailedError": "Since the screen information is not up-to-date, an inconsistency occurred in the file information and failed to rename the file. Please refresh the screen and try again.",
|
||||
"fileNameAleadyExistsError": "This file name is already registered. Please register with a different file name.",
|
||||
"reopenFailedError": "The status could not be changed to Pending. Please refresh the screen to see the current status. Only files with Finished status can be operated."
|
||||
"fileNameAleadyExistsError": "This file name is already registered. Please register with a different file name."
|
||||
},
|
||||
"label": {
|
||||
"title": "Dictations",
|
||||
@ -389,7 +389,7 @@
|
||||
"viewDetails": "View Details",
|
||||
"accounts": "accounts",
|
||||
"changeOwnerButton": "Change Owner",
|
||||
"allocatedlicense": "Allocated Licenses",
|
||||
"allocatedLicense": "Allocated Licenses",
|
||||
"search": "Search"
|
||||
}
|
||||
},
|
||||
|
||||
@ -262,13 +262,13 @@
|
||||
"taskNotEditable": "No se puede cambiar el transcriptor porque la transcripción ya está en curso o el archivo no existe. Actualice la pantalla y verifique el estado más reciente.",
|
||||
"backupFailedError": "El proceso de \"Copia de seguridad de archivos\" ha fallado. Por favor, inténtelo de nuevo más tarde. Si el error continúa, comuníquese con el administrador del sistema.",
|
||||
"cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.",
|
||||
"reopenFailedError": "No se pudo cambiar el estado a Pendiente. Actualice la pantalla para ver el estado actual. Solo se pueden utilizar los archivos con estado Finalizado.",
|
||||
"deleteFailedError": "No se pudo eliminar la tarea. Actualice la pantalla y verifique nuevamente.",
|
||||
"licenseNotAssignedError": "La transcripción no es posible porque no se ha asignado una licencia válida. Solicite a su administrador que le asigne una licencia válida.",
|
||||
"licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida.",
|
||||
"fileAlreadyDeletedError": "La información de la pantalla no está actualizada, contiene archivos de audio que ya han sido eliminados. Actualice la pantalla y seleccione los archivos que desea eliminar nuevamente.",
|
||||
"fileRenameFailedError": "Dado que la información de la pantalla no está actualizada, se produjo una inconsistencia en la información del archivo y no se pudo cambiar el nombre del archivo. Actualice la pantalla e inténtelo de nuevo.",
|
||||
"fileNameAleadyExistsError": "Este nombre de archivo ya está registrado. Regístrese con un nombre de archivo diferente.",
|
||||
"reopenFailedError": "No se pudo cambiar el estado a Pendiente. Actualice la pantalla para ver el estado actual. Solo se pueden utilizar los archivos con estado Finalizado."
|
||||
"fileNameAleadyExistsError": "Este nombre de archivo ya está registrado. Regístrese con un nombre de archivo diferente."
|
||||
},
|
||||
"label": {
|
||||
"title": "Dictado",
|
||||
@ -389,7 +389,7 @@
|
||||
"viewDetails": "Ver detalles",
|
||||
"accounts": "cuentas",
|
||||
"changeOwnerButton": "Change Owner",
|
||||
"allocatedlicense": "Licencias asignadas",
|
||||
"allocatedLicense": "Licencias asignadas",
|
||||
"search": "Búsqueda"
|
||||
}
|
||||
},
|
||||
|
||||
@ -262,13 +262,13 @@
|
||||
"taskNotEditable": "Le transcripteur ne peut pas être changé car la transcription est déjà en cours ou le fichier n'existe pas. Veuillez actualiser l'écran et vérifier le dernier statut.",
|
||||
"backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.",
|
||||
"cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.",
|
||||
"reopenFailedError": "Le statut n'a pas pu être modifié en Suspendu. Veuillez actualiser l'écran pour voir le statut actuel. Seuls les fichiers dont le statut est Terminé peuvent être traités.",
|
||||
"deleteFailedError": "Échec de la suppression de la tâche. Veuillez actualiser l'écran et vérifier à nouveau.",
|
||||
"licenseNotAssignedError": "La transcription n'est pas possible car aucune licence valide n'a été attribuée. Veuillez demander à votre administrateur d'attribuer une licence valide.",
|
||||
"licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide.",
|
||||
"fileAlreadyDeletedError": "Les informations à l'écran ne sont pas à jour, elles contiennent des fichiers audio qui ont déjà été supprimés. Veuillez actualiser l'écran et sélectionner à nouveau les fichiers à supprimer.",
|
||||
"fileRenameFailedError": "Étant donné que les informations à l'écran ne sont pas à jour, une incohérence s'est produite dans les informations du fichier et il n'a pas été possible de renommer le fichier. Veuillez actualiser l'écran et réessayer.",
|
||||
"fileNameAleadyExistsError": "Ce nom de fichier est déjà enregistré. Veuillez vous inscrire avec un nom de fichier différent.",
|
||||
"reopenFailedError": "Le statut n'a pas pu être modifié en Suspendu. Veuillez actualiser l'écran pour voir le statut actuel. Seuls les fichiers dont le statut est Terminé peuvent être traités."
|
||||
"fileNameAleadyExistsError": "Ce nom de fichier est déjà enregistré. Veuillez vous inscrire avec un nom de fichier différent."
|
||||
},
|
||||
"label": {
|
||||
"title": "Dictées",
|
||||
@ -389,7 +389,7 @@
|
||||
"viewDetails": "Voir les détails",
|
||||
"accounts": "comptes",
|
||||
"changeOwnerButton": "Change Owner",
|
||||
"allocatedlicense": "Licences attribuées",
|
||||
"allocatedLicense": "Licences attribuées",
|
||||
"search": "Recherche"
|
||||
}
|
||||
},
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
-- +migrate Up
|
||||
ALTER TABLE `license_orders` ADD COLUMN `type` VARCHAR(255) DEFAULT "NORMAL" COMMENT 'ライセンス種別' AFTER `issued_at`;
|
||||
|
||||
-- +migrate Down
|
||||
ALTER TABLE `license_orders` DROP COLUMN `type`;
|
||||
16
dictation_server/db/migrations/068-create_task_filters.sql
Normal file
16
dictation_server/db/migrations/068-create_task_filters.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- +migrate Up
|
||||
CREATE TABLE IF NOT EXISTS `task_filters` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'タスク検索条件ID',
|
||||
`user_id` BIGINT UNSIGNED NOT NULL COMMENT 'ユーザーID',
|
||||
`author_id` VARCHAR(255) COMMENT '検索キー:AuthorID',
|
||||
`file_name` VARCHAR(1024) COMMENT '検索キー:ファイル名',
|
||||
`deleted_at` TIMESTAMP COMMENT '削除時刻',
|
||||
`created_by` VARCHAR(255) COMMENT '作成者',
|
||||
`created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻',
|
||||
`updated_by` VARCHAR(255) COMMENT '更新者',
|
||||
`updated_at` TIMESTAMP DEFAULT now() on UPDATE now() COMMENT '更新時刻',
|
||||
INDEX `idx_task_filters_user_id` (`user_id`)
|
||||
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
|
||||
|
||||
-- +migrate Down
|
||||
DROP TABLE `task_filters`;
|
||||
@ -0,0 +1,9 @@
|
||||
-- +migrate Up
|
||||
INSERT INTO task_filters (user_id)
|
||||
SELECT
|
||||
id AS user_id
|
||||
FROM
|
||||
users;
|
||||
|
||||
-- +migrate Down
|
||||
TRUNCATE TABLE task_filters;
|
||||
@ -27,7 +27,9 @@
|
||||
"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:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=local",
|
||||
"migrate:up:test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test"
|
||||
"migrate:up:test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test",
|
||||
"migrate:down:test": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=test",
|
||||
"migrate:status:test": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/identity": "^3.1.3",
|
||||
@ -105,6 +107,7 @@
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"testTimeout": 120000,
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
|
||||
@ -1417,6 +1417,119 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/accounts/partners/search": {
|
||||
"get": {
|
||||
"operationId": "searchPartners",
|
||||
"summary": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "companyName",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "パートナー名",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
{
|
||||
"name": "accountId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "アカウントID",
|
||||
"schema": { "type": "number" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SearchPartnersResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "パラメータ不正",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["accounts"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/accounts/partners/hierarchy": {
|
||||
"get": {
|
||||
"operationId": "getPartnerHierarchy",
|
||||
"summary": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "accountId",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"description": "アカウントID",
|
||||
"schema": { "type": "number" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetPartnerHierarchyResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "パラメータ不正",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["accounts"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/accounts/me/file-delete-setting": {
|
||||
"post": {
|
||||
"operationId": "updateFileDeleteSetting",
|
||||
@ -1968,11 +2081,76 @@
|
||||
"tags": ["users"]
|
||||
}
|
||||
},
|
||||
"/users/confirm/force": {
|
||||
"post": {
|
||||
"operationId": "confirmUserForce",
|
||||
"summary": "",
|
||||
"description": "ユーザーを強制的にメール認証済にする",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ConfirmForceRequest" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ConfirmResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "メール認証済み",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"operationId": "getUsers",
|
||||
"summary": "",
|
||||
"parameters": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userName",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": { "type": "string" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
@ -2184,6 +2362,106 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users/task-filters": {
|
||||
"post": {
|
||||
"operationId": "updateTaskFilter",
|
||||
"summary": "",
|
||||
"description": "ログインしているユーザーの検索条件を更新します",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PostTaskFiltersRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PostTaskFiltersResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "不正なパラメータ",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"security": [{ "bearer": [] }]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "getTaskFilter",
|
||||
"summary": "",
|
||||
"description": "ログインしているユーザーのタスクの検索条件を取得します",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetTaskFiltersResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "不正なパラメータ",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["users"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/users/update": {
|
||||
"post": {
|
||||
"operationId": "updateUser",
|
||||
@ -2996,6 +3274,20 @@
|
||||
"in": "query",
|
||||
"description": "JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
{
|
||||
"name": "authorId",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "タスクの検索キーワード:AuthorID",
|
||||
"schema": { "type": "string" }
|
||||
},
|
||||
{
|
||||
"name": "fileName",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "タスクの検索キーワード:fileName",
|
||||
"schema": { "type": "string" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@ -3779,6 +4071,62 @@
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/licenses/trial": {
|
||||
"post": {
|
||||
"operationId": "issueTrialLicenses",
|
||||
"summary": "",
|
||||
"description": "第五階層アカウントにトライアルライセンスを発行します。",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/IssueTrialLicenseRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "成功時のレスポンス",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/IssueTrialLicenseResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "アカウントやユーザーが見つからないエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "認証エラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "想定外のサーバーエラー",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ErrorResponse" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["licenses"],
|
||||
"security": [{ "bearer": [] }]
|
||||
}
|
||||
},
|
||||
"/licenses/orders/cancel": {
|
||||
"post": {
|
||||
"operationId": "cancelOrder",
|
||||
@ -4533,6 +4881,10 @@
|
||||
"type": "number",
|
||||
"description": "不足数({Stock license} - {Issue Requested})"
|
||||
},
|
||||
"allocatedLicense": {
|
||||
"type": "number",
|
||||
"description": "有効期限内の割り当て済み総ライセンス数"
|
||||
},
|
||||
"issueRequesting": {
|
||||
"type": "number",
|
||||
"description": "未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数)"
|
||||
@ -4545,6 +4897,7 @@
|
||||
"stockLicense",
|
||||
"issuedRequested",
|
||||
"shortage",
|
||||
"allocatedLicense",
|
||||
"issueRequesting"
|
||||
]
|
||||
},
|
||||
@ -4578,9 +4931,10 @@
|
||||
"issueDate": { "type": "string", "description": "発行日付" },
|
||||
"numberOfOrder": { "type": "number", "description": "注文数" },
|
||||
"poNumber": { "type": "string", "description": "POナンバー" },
|
||||
"status": { "type": "string", "description": "注文状態" }
|
||||
"status": { "type": "string", "description": "注文状態" },
|
||||
"type": { "type": "string", "description": "ライセンス種別" }
|
||||
},
|
||||
"required": ["orderDate", "numberOfOrder", "poNumber", "status"]
|
||||
"required": ["orderDate", "numberOfOrder", "poNumber", "status", "type"]
|
||||
},
|
||||
"GetOrderHistoriesResponse": {
|
||||
"type": "object",
|
||||
@ -4793,6 +5147,60 @@
|
||||
},
|
||||
"required": ["total", "partners"]
|
||||
},
|
||||
"SearchPartner": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "会社名" },
|
||||
"tier": { "type": "number", "description": "階層" },
|
||||
"accountId": { "type": "number", "description": "アカウントID" },
|
||||
"country": { "type": "string", "description": "国" },
|
||||
"primaryAdmin": {
|
||||
"type": "string",
|
||||
"description": "プライマリ管理者"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "プライマリ管理者メールアドレス"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"tier",
|
||||
"accountId",
|
||||
"country",
|
||||
"primaryAdmin",
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"SearchPartnersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"searchResult": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/SearchPartner" }
|
||||
}
|
||||
},
|
||||
"required": ["searchResult"]
|
||||
},
|
||||
"PartnerHierarchy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "description": "会社名" },
|
||||
"tier": { "type": "number", "description": "階層" },
|
||||
"accountId": { "type": "number", "description": "アカウントID" }
|
||||
},
|
||||
"required": ["name", "tier", "accountId"]
|
||||
},
|
||||
"GetPartnerHierarchyResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accountHierarchy": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/PartnerHierarchy" }
|
||||
}
|
||||
},
|
||||
"required": ["accountHierarchy"]
|
||||
},
|
||||
"UpdateAccountInfoRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -4958,6 +5366,11 @@
|
||||
"required": ["token"]
|
||||
},
|
||||
"ConfirmResponse": { "type": "object", "properties": {} },
|
||||
"ConfirmForceRequest": {
|
||||
"type": "object",
|
||||
"properties": { "userId": { "type": "number" } },
|
||||
"required": ["userId"]
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -5125,6 +5538,34 @@
|
||||
},
|
||||
"required": ["direction", "paramName"]
|
||||
},
|
||||
"PostTaskFiltersRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filterConditionAuthorId": {
|
||||
"type": "string",
|
||||
"description": "タスクの検索キーワードを更新する:AuthorID"
|
||||
},
|
||||
"filterConditionFileName": {
|
||||
"type": "string",
|
||||
"description": "タスクの検索キーワードを更新する:fileName"
|
||||
}
|
||||
},
|
||||
"required": ["filterConditionAuthorId", "filterConditionFileName"]
|
||||
},
|
||||
"PostTaskFiltersResponse": { "type": "object", "properties": {} },
|
||||
"GetTaskFiltersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authorId": {
|
||||
"type": "string",
|
||||
"description": "タスクの検索キーワードを取得する:AuthorID"
|
||||
},
|
||||
"fileName": {
|
||||
"type": "string",
|
||||
"description": "タスクの検索キーワードを取得する:fileName"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PostUpdateUserRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -5615,6 +6056,12 @@
|
||||
},
|
||||
"required": ["allocatableLicenses"]
|
||||
},
|
||||
"IssueTrialLicenseRequest": {
|
||||
"type": "object",
|
||||
"properties": { "issuedAccount": { "type": "number" } },
|
||||
"required": ["issuedAccount"]
|
||||
},
|
||||
"IssueTrialLicenseResponse": { "type": "object", "properties": {} },
|
||||
"CancelOrderRequest": {
|
||||
"type": "object",
|
||||
"properties": { "poNumber": { "type": "string" } },
|
||||
|
||||
@ -41,6 +41,7 @@ import { LicensesController } from './features/licenses/licenses.controller';
|
||||
import { CheckoutPermissionsRepositoryModule } from './repositories/checkout_permissions/checkout_permissions.repository.module';
|
||||
import { UserGroupsRepositoryModule } from './repositories/user_groups/user_groups.repository.module';
|
||||
import { SortCriteriaRepositoryModule } from './repositories/sort_criteria/sort_criteria.repository.module';
|
||||
import { TaskFiltersRepositoryModule } from './repositories/task_filters/task_filter.repository.module';
|
||||
import { TemplateFilesRepositoryModule } from './repositories/template_files/template_files.repository.module';
|
||||
import { WorktypesRepositoryModule } from './repositories/worktypes/worktypes.repository.module';
|
||||
import { TemplatesService } from './features/templates/templates.service';
|
||||
@ -138,6 +139,7 @@ import { JobNumberRepositoryModule } from './repositories/job_number/job_number.
|
||||
AuthGuardsModule,
|
||||
SystemAccessGuardsModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
TaskFiltersRepositoryModule,
|
||||
WorktypesRepositoryModule,
|
||||
TermsModule,
|
||||
RedisModule,
|
||||
|
||||
8
dictation_server/src/common/repository/utils/utils.ts
Normal file
8
dictation_server/src/common/repository/utils/utils.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* LIKE句で使用する文字列のエスケープ
|
||||
* @param value エスケープする文字列
|
||||
* @returns エスケープ後の文字列
|
||||
*/
|
||||
export function escapeLikeString(value: string): string {
|
||||
return value.replace(/[%_]/g, (match) => `\\${match}`);
|
||||
}
|
||||
@ -23,6 +23,7 @@ import { NotificationhubModule } from '../../gateways/notificationhub/notificati
|
||||
import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module';
|
||||
import { AuthGuardsModule } from '../../common/guards/auth/authguards.module';
|
||||
import { SortCriteriaRepositoryModule } from '../../repositories/sort_criteria/sort_criteria.repository.module';
|
||||
import { TaskFiltersRepositoryModule } from '../../repositories/task_filters/task_filter.repository.module';
|
||||
import { AuthService } from '../../features/auth/auth.service';
|
||||
import { AccountsService } from '../../features/accounts/accounts.service';
|
||||
import { UsersService } from '../../features/users/users.service';
|
||||
@ -78,6 +79,7 @@ export const makeTestingModule = async (
|
||||
BlobstorageModule,
|
||||
AuthGuardsModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
TaskFiltersRepositoryModule,
|
||||
WorktypesRepositoryModule,
|
||||
TermsRepositoryModule,
|
||||
JobNumberRepositoryModule,
|
||||
|
||||
@ -10,6 +10,7 @@ import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.servi
|
||||
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
|
||||
import { Account } from '../../repositories/accounts/entity/account.entity';
|
||||
import { AdB2cUser } from '../../gateways/adb2c/types/types';
|
||||
import { SearchPartnerInfoFromDb } from '../../features/accounts/types/types';
|
||||
|
||||
// ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ###
|
||||
|
||||
@ -279,6 +280,14 @@ export const overrideAccountsRepositoryService = <TService>(
|
||||
) => Promise<{ newAccount: Account; adminUser: User }>;
|
||||
deleteAccount?: (accountId: number, userId: number) => Promise<void>;
|
||||
deleteAccountAndInsertArchives?: (accountId: number) => Promise<User[]>;
|
||||
getAccountsRelatedOwnAccount?: (
|
||||
context: Context,
|
||||
ownAccountId: number,
|
||||
ownAccountTier: number,
|
||||
companyName?: string,
|
||||
targetAccountId?: number,
|
||||
) => Promise<SearchPartnerInfoFromDb[]>;
|
||||
findUserByExternalId?: (context: Context, sub: string) => Promise<User>;
|
||||
},
|
||||
): void => {
|
||||
// テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
|
||||
|
||||
@ -12,6 +12,7 @@ import { AccountArchive } from '../../repositories/accounts/entity/account_archi
|
||||
import { Task } from '../../repositories/tasks/entity/task.entity';
|
||||
import { JobNumber } from '../../repositories/job_number/entity/job_number.entity';
|
||||
import { SortCriteria } from '../../repositories/sort_criteria/entity/sort_criteria.entity';
|
||||
import { TaskFilters } from '../../repositories/task_filters/entity/task_filters.entity';
|
||||
|
||||
type InitialTestDBState = {
|
||||
tier1Accounts: { account: Account; users: User[] }[];
|
||||
@ -243,6 +244,9 @@ export const makeTestAccount = async (
|
||||
// sort_criteriaテーブルにデータを追加
|
||||
await createSortCriteria(datasource, userId, 'JOB_NUMBER', 'ASC');
|
||||
|
||||
// task_filtersテーブルにデータを追加
|
||||
await createTaskFilter(datasource, userId, null, null);
|
||||
|
||||
// job_numberテーブルにデータを追加
|
||||
await createJobNumber(datasource, accountId, '00000000');
|
||||
|
||||
@ -333,6 +337,10 @@ export const makeTestUser = async (
|
||||
}
|
||||
// sort_criteriaテーブルにデータを追加
|
||||
await createSortCriteria(datasource, user.id, 'FILE_LENGTH', 'ASC');
|
||||
|
||||
// task_filtersテーブルにデータを追加
|
||||
await createTaskFilter(datasource, user.id, null, null);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
@ -523,3 +531,46 @@ export const getSortCriteria = async (
|
||||
});
|
||||
return sortCriteria;
|
||||
};
|
||||
|
||||
// task_filterを作成する
|
||||
export const createTaskFilter = async (
|
||||
datasource: DataSource,
|
||||
userId: number,
|
||||
authorId: string | null,
|
||||
fileName: string | null,
|
||||
): Promise<void> => {
|
||||
await datasource.getRepository(TaskFilters).insert({
|
||||
user_id: userId,
|
||||
author_id: authorId,
|
||||
file_name: fileName,
|
||||
});
|
||||
};
|
||||
|
||||
// 指定したユーザーのtask_filterを更新する
|
||||
export const updateTaskFilter = async (
|
||||
datasource: DataSource,
|
||||
userId: number,
|
||||
authorId: string | null,
|
||||
fileName: string | null,
|
||||
): Promise<void> => {
|
||||
await datasource.getRepository(TaskFilters).update(
|
||||
{ user_id: userId },
|
||||
{
|
||||
author_id: authorId,
|
||||
file_name: fileName,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 指定したユーザーのtask_filterを取得する
|
||||
export const getTaskFilter = async (
|
||||
datasource: DataSource,
|
||||
userId: number,
|
||||
): Promise<TaskFilters | null> => {
|
||||
const taskFilter = await datasource.getRepository(TaskFilters).findOne({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
return taskFilter;
|
||||
};
|
||||
|
||||
@ -344,3 +344,9 @@ export const INITIAL_JOB_NUMBER = '00000000';
|
||||
* @const {string}
|
||||
*/
|
||||
export const MAX_JOB_NUMBER = '99999999';
|
||||
|
||||
/**
|
||||
* 上位階層によるトライアルライセンス発行数
|
||||
* @const {number}
|
||||
*/
|
||||
export const ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY = 10;
|
||||
|
||||
@ -85,6 +85,10 @@ import {
|
||||
GetPartnerUsersRequest,
|
||||
UpdatePartnerInfoRequest,
|
||||
UpdatePartnerInfoResponse,
|
||||
SearchPartnersResponse,
|
||||
SearchPartnersRequest,
|
||||
GetPartnerHierarchyRequest,
|
||||
GetPartnerHierarchyResponse,
|
||||
} from './types/types';
|
||||
import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants';
|
||||
import { AuthGuard } from '../../common/guards/auth/authguards';
|
||||
@ -1885,6 +1889,187 @@ export class AccountsController {
|
||||
return response;
|
||||
}
|
||||
|
||||
@Get('/partners/search')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: SearchPartnersResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'パラメータ不正',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({ operationId: 'searchPartners' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4],
|
||||
}),
|
||||
)
|
||||
async searchPartners(
|
||||
@Req() req: Request,
|
||||
@Query() query: SearchPartnersRequest,
|
||||
): Promise<SearchPartnersResponse> {
|
||||
// アカウント名の前方スペースを削除
|
||||
const companyName = query.companyName?.trimStart();
|
||||
const accountId = query.accountId;
|
||||
|
||||
// 両方とも未入力の場合はエラー
|
||||
if (!companyName && !accountId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010001'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// アカウント名が2文字以下の場合はエラー
|
||||
// サロゲートペアを考慮して、スプレッド構文でリスト化してから文字数をカウントする
|
||||
// 絵文字が入力された場合は救えないが、そもそも入力されても検索できないので考慮しない。
|
||||
if (companyName && [...companyName].length <= 2) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010001'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId, tier } = decodedAccessToken as AccessToken;
|
||||
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
const response = await this.accountService.searchPartners(
|
||||
context,
|
||||
userId,
|
||||
tier,
|
||||
companyName,
|
||||
accountId,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Get('/partners/hierarchy')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: GetPartnerHierarchyResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'パラメータ不正',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({ operationId: 'getPartnerHierarchy' })
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3, TIERS.TIER4],
|
||||
}),
|
||||
)
|
||||
async getPartnerHierarchy(
|
||||
@Req() req: Request,
|
||||
@Query() query: GetPartnerHierarchyRequest,
|
||||
): Promise<GetPartnerHierarchyResponse> {
|
||||
const { accountId } = query;
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId, tier } = decodedAccessToken as AccessToken;
|
||||
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
const response = await this.accountService.getPartnerHierarchy(
|
||||
context,
|
||||
userId,
|
||||
tier,
|
||||
accountId,
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Post('/me')
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -38,6 +38,10 @@ import {
|
||||
Partner,
|
||||
GetCompanyNameResponse,
|
||||
PartnerUser,
|
||||
SearchPartnersResponse,
|
||||
GetPartnerHierarchyResponse,
|
||||
SearchPartner,
|
||||
PartnerHierarchy,
|
||||
} from './types/types';
|
||||
import {
|
||||
DateWithZeroTime,
|
||||
@ -981,13 +985,18 @@ export class AccountsService {
|
||||
// 各子アカウントのShortageを算出してreturn用の変数にマージする
|
||||
const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
|
||||
for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
|
||||
const { allocatableLicenseWithMargin, expiringSoonLicense } =
|
||||
childPartnerLicenseFromRepository;
|
||||
const {
|
||||
allocatableLicenseWithMargin,
|
||||
expiringSoonLicense,
|
||||
allocatedLicense,
|
||||
} = childPartnerLicenseFromRepository;
|
||||
|
||||
let childShortage = 0;
|
||||
if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
|
||||
if (
|
||||
allocatableLicenseWithMargin === undefined ||
|
||||
expiringSoonLicense === undefined
|
||||
expiringSoonLicense === undefined ||
|
||||
allocatedLicense === undefined
|
||||
) {
|
||||
throw new Error(
|
||||
`Tier5 account has no allocatableLicenseWithMargin or expiringSoonLicense. accountId: ${accountId}`,
|
||||
@ -1008,6 +1017,9 @@ export class AccountsService {
|
||||
{
|
||||
shortage: childShortage,
|
||||
},
|
||||
{
|
||||
allocatedLicense: allocatedLicense,
|
||||
},
|
||||
);
|
||||
|
||||
childrenPartnerLicenses.push(childPartnerLicense);
|
||||
@ -1072,6 +1084,7 @@ export class AccountsService {
|
||||
orderDate: new Date(licenseOrder.ordered_at).toISOString(),
|
||||
poNumber: licenseOrder.po_number,
|
||||
status: licenseOrder.status,
|
||||
type: licenseOrder.type,
|
||||
};
|
||||
orderHistories.push(returnLicenseOrder);
|
||||
}
|
||||
@ -2159,6 +2172,166 @@ export class AccountsService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* パートナー一覧を検索します。
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param ownTier
|
||||
* @param companyName
|
||||
* @param targetAccountId
|
||||
* @returns SearchPartnersResponse
|
||||
*/
|
||||
async searchPartners(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
ownTier: number,
|
||||
companyName?: string,
|
||||
targetAccountId?: number,
|
||||
): Promise<SearchPartnersResponse> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.searchPartners.name
|
||||
} | params: { ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`ownTier: ${ownTier}, ` +
|
||||
`companyName: ${companyName}, ` +
|
||||
`targetAccountId: ${targetAccountId}, };`,
|
||||
);
|
||||
|
||||
try {
|
||||
const { account_id: accountId } =
|
||||
await this.usersRepository.findUserByExternalId(context, externalId);
|
||||
|
||||
const partnersRecords =
|
||||
await this.accountRepository.getAccountsRelatedOwnAccount(
|
||||
context,
|
||||
accountId,
|
||||
ownTier,
|
||||
companyName,
|
||||
targetAccountId,
|
||||
);
|
||||
|
||||
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
|
||||
let externalIds = partnersRecords.map((x) => x.primaryAccountExternalId);
|
||||
externalIds = externalIds.filter((item) => item !== undefined);
|
||||
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
|
||||
|
||||
// DBから取得した情報とADB2Cから取得した情報をマージ
|
||||
const searchResult = partnersRecords.map(
|
||||
(dbuser): SearchPartner => {
|
||||
const adb2cUser = adb2cUsers.find(
|
||||
(adb2c) => dbuser.primaryAccountExternalId === adb2c.id,
|
||||
);
|
||||
if (!adb2cUser) {
|
||||
throw new Error(
|
||||
`adb2c user not found. externalId: ${dbuser.primaryAccountExternalId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { displayName: primaryAdmin, emailAddress: mail } =
|
||||
getUserNameAndMailAddress(adb2cUser);
|
||||
if (!mail) {
|
||||
throw new Error(
|
||||
`adb2c user mail not found. externalId: ${dbuser.primaryAccountExternalId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: dbuser.name,
|
||||
tier: dbuser.tier,
|
||||
accountId: dbuser.accountId,
|
||||
country: dbuser.country,
|
||||
primaryAdmin: primaryAdmin,
|
||||
email: mail,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return { searchResult };
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.searchPartners.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* パートナーの階層構造を取得します
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param ownTier
|
||||
* @param targetAccountId
|
||||
* @returns GetPartnersResponse
|
||||
*/
|
||||
async getPartnerHierarchy(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
ownTier: number,
|
||||
targetAccountId: number,
|
||||
): Promise<GetPartnerHierarchyResponse> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.getPartnerHierarchy.name
|
||||
} | params: { ` +
|
||||
`externalId: ${externalId}, ` +
|
||||
`ownTier: ${ownTier}, ` +
|
||||
`targetAccountId: ${targetAccountId}, };`,
|
||||
);
|
||||
|
||||
try {
|
||||
// 自身のアカウントIdを取得
|
||||
const { account_id: ownAccountId } =
|
||||
await this.usersRepository.findUserByExternalId(context, externalId);
|
||||
|
||||
// 対象の親アカウントを取得
|
||||
const parentAccountIds = await this.accountRepository.getHierarchyParents(
|
||||
context,
|
||||
targetAccountId,
|
||||
);
|
||||
|
||||
// 親アカウントの中に自身が存在しない場合、エラー
|
||||
if (!parentAccountIds.includes(ownAccountId)) {
|
||||
throw new Error(
|
||||
`parent account not found. targetAccountId=${targetAccountId}, parentAccountIds=${parentAccountIds}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 対象を含むアカウント階層をすべて取得
|
||||
const targetAccountIds = [...parentAccountIds, targetAccountId];
|
||||
const accounts = await this.accountRepository.findAccountsById(
|
||||
context,
|
||||
targetAccountIds,
|
||||
);
|
||||
const accountHierarchy = accounts
|
||||
// 取得する階層を自身の階層までに限定
|
||||
.filter((account) => account.tier >= ownTier)
|
||||
.map((account): PartnerHierarchy => {
|
||||
const { tier, id: accountId, company_name: name } = account;
|
||||
return { tier, accountId, name };
|
||||
})
|
||||
// 上位の階層順になるようにソート
|
||||
.sort((a, b) => a.tier - b.tier);
|
||||
|
||||
return { accountHierarchy };
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.getPartnerHierarchy.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウント情報を設定する
|
||||
* @param context
|
||||
|
||||
@ -463,6 +463,7 @@ export const makeDefaultLicensesRepositoryMockValue =
|
||||
numberOfOrder: 10,
|
||||
poNumber: 'PO001',
|
||||
status: 'Issued',
|
||||
type: 'NORMAL',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
LicenseOrder,
|
||||
} from '../../../repositories/licenses/entity/license.entity';
|
||||
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
|
||||
import { TaskFilters } from '../..//../repositories/task_filters/entity/task_filters.entity';
|
||||
import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity';
|
||||
import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity';
|
||||
import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity';
|
||||
@ -21,6 +22,15 @@ export const getSortCriteriaList = async (dataSource: DataSource) => {
|
||||
return await dataSource.getRepository(SortCriteria).find();
|
||||
};
|
||||
|
||||
/**
|
||||
* テスト ユーティリティ: すべてのTask Filtersを取得する
|
||||
* @param dataSource データソース
|
||||
* @returns 該当ユーザー一覧
|
||||
*/
|
||||
export const getTaskFilterList = async (dataSource: DataSource) => {
|
||||
return await dataSource.getRepository(TaskFilters).find();
|
||||
};
|
||||
|
||||
export const createLicense = async (
|
||||
datasource: DataSource,
|
||||
licenseId: number,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty, OmitType, PickType } from '@nestjs/swagger';
|
||||
import {
|
||||
IsEmail,
|
||||
IsInt,
|
||||
@ -320,6 +320,21 @@ export class GetPartnersRequest {
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export class SearchPartnersRequest {
|
||||
@ApiProperty({ description: 'パートナー名', required: false })
|
||||
@Type(() => String)
|
||||
companyName: string;
|
||||
@ApiProperty({ description: 'アカウントID', required: false })
|
||||
@Type(() => Number)
|
||||
accountId: number;
|
||||
}
|
||||
|
||||
export class GetPartnerHierarchyRequest {
|
||||
@ApiProperty({ description: 'アカウントID' })
|
||||
@Type(() => Number)
|
||||
accountId: number;
|
||||
}
|
||||
|
||||
export class UpdateAccountInfoRequest {
|
||||
@ApiProperty({ description: '親アカウントのID', required: false })
|
||||
@Type(() => Number)
|
||||
@ -592,6 +607,9 @@ export class PartnerLicenseInfo {
|
||||
@ApiProperty({ description: '不足数({Stock license} - {Issue Requested})' })
|
||||
shortage: number;
|
||||
|
||||
@ApiProperty({ description: '有効期限内の割り当て済み総ライセンス数' })
|
||||
allocatedLicense?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数)',
|
||||
@ -618,6 +636,8 @@ export class LicenseOrder {
|
||||
poNumber: string;
|
||||
@ApiProperty({ description: '注文状態' })
|
||||
status: string;
|
||||
@ApiProperty({ description: 'ライセンス種別' })
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class GetOrderHistoriesResponse {
|
||||
@ -706,6 +726,13 @@ export class Partner {
|
||||
dealerManagement: boolean;
|
||||
}
|
||||
|
||||
export class SearchPartner extends OmitType(Partner, ['dealerManagement']) {}
|
||||
export class PartnerHierarchy extends PickType(Partner, [
|
||||
'tier',
|
||||
'name',
|
||||
'accountId',
|
||||
]) {}
|
||||
|
||||
export class GetPartnersResponse {
|
||||
@ApiProperty({ description: '合計件数' })
|
||||
total: number;
|
||||
@ -713,6 +740,16 @@ export class GetPartnersResponse {
|
||||
partners: Partner[];
|
||||
}
|
||||
|
||||
export class SearchPartnersResponse {
|
||||
@ApiProperty({ type: [SearchPartner] })
|
||||
searchResult: SearchPartner[];
|
||||
}
|
||||
|
||||
export class GetPartnerHierarchyResponse {
|
||||
@ApiProperty({ type: [PartnerHierarchy] })
|
||||
accountHierarchy: PartnerHierarchy[];
|
||||
}
|
||||
|
||||
export class UpdateAccountInfoResponse {}
|
||||
|
||||
export class DeleteAccountResponse {}
|
||||
@ -812,3 +849,10 @@ export type PartnerInfoFromDb = {
|
||||
primaryAccountExternalId: string;
|
||||
dealerManagement: boolean;
|
||||
};
|
||||
|
||||
// パートナー検索にて、RepositoryからPartnerLicenseInfoに関する情報を取得する際の型
|
||||
// dealerManagementを除外
|
||||
export type SearchPartnerInfoFromDb = Omit<
|
||||
PartnerInfoFromDb,
|
||||
'dealerManagement'
|
||||
>;
|
||||
|
||||
@ -27,6 +27,8 @@ import {
|
||||
GetAllocatableLicensesResponse,
|
||||
CancelOrderRequest,
|
||||
CancelOrderResponse,
|
||||
IssueTrialLicenseResponse,
|
||||
IssueTrialLicenseRequest,
|
||||
} from './types/types';
|
||||
import { Request } from 'express';
|
||||
import { retrieveAuthorizationToken } from '../../common/http/helper';
|
||||
@ -347,6 +349,90 @@ export class LicensesController {
|
||||
return allocatableLicenses;
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: IssueTrialLicenseResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'アカウントやユーザーが見つからないエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'issueTrialLicenses',
|
||||
description: '第五階層アカウントにトライアルライセンスを発行します。',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER1, TIERS.TIER2],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
@Post('/trial')
|
||||
async issueTrialLicense(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@Req() req: Request,
|
||||
@Body() body: IssueTrialLicenseRequest,
|
||||
): Promise<IssueTrialLicenseResponse> {
|
||||
const { issuedAccount } = body;
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId } = decodedAccessToken as AccessToken;
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
await this.licensesService.issueTrialLicense(
|
||||
context,
|
||||
userId,
|
||||
issuedAccount,
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: CancelOrderResponse,
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { NewAllocatedLicenseExpirationDate } from './types/types';
|
||||
import {
|
||||
NewAllocatedLicenseExpirationDate,
|
||||
NewTrialLicenseExpirationDate,
|
||||
} from './types/types';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { LicensesService } from './licenses.service';
|
||||
@ -15,12 +18,14 @@ import {
|
||||
selectLicenseAllocationHistory,
|
||||
createOrder,
|
||||
selectOrderLicense,
|
||||
selectIssuedLicensesAndLicenseOrders,
|
||||
} from './test/utility';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { Context, makeContext } from '../../common/log';
|
||||
import {
|
||||
ADB2C_SIGN_IN_TYPE,
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
LICENSE_ISSUE_STATUS,
|
||||
LICENSE_TYPE,
|
||||
} from '../../constants';
|
||||
import {
|
||||
@ -36,6 +41,7 @@ import {
|
||||
} from '../../common/test/overrides';
|
||||
import { truncateAllTable } from '../../common/test/init';
|
||||
import { TestLogger } from '../../common/test/logger';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
|
||||
describe('ライセンス注文', () => {
|
||||
let source: DataSource | null = null;
|
||||
@ -103,6 +109,8 @@ describe('ライセンス注文', () => {
|
||||
expect(dbSelectResult.orderLicense?.from_account_id).toEqual(accountId);
|
||||
expect(dbSelectResult.orderLicense?.to_account_id).toEqual(parentAccountId);
|
||||
expect(dbSelectResult.orderLicense?.status).toEqual('Issue Requesting');
|
||||
// ライセンス種別のデフォルト値が埋まっていること
|
||||
expect(dbSelectResult.orderLicense?.type).toEqual(LICENSE_TYPE.NORMAL);
|
||||
});
|
||||
|
||||
it('POナンバー重複時、エラーとなる', async () => {
|
||||
@ -736,7 +744,7 @@ describe('ライセンス割り当て', () => {
|
||||
);
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
let _subject: string = '';
|
||||
let _subject = '';
|
||||
let _url: string | undefined = '';
|
||||
overrideAdB2cService(service, {
|
||||
getUser: async (context, externalId) => {
|
||||
@ -2009,3 +2017,385 @@ describe('割り当て可能なライセンス取得', () => {
|
||||
expect(response.allocatableLicenses[5].licenseId).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('第五階層へのトライアルライセンス発行', () => {
|
||||
let source: DataSource | null = null;
|
||||
beforeAll(async () => {
|
||||
if (source == null) {
|
||||
source = await (async () => {
|
||||
const s = new DataSource({
|
||||
type: 'mysql',
|
||||
host: 'test_mysql_db',
|
||||
port: 3306,
|
||||
username: 'user',
|
||||
password: 'password',
|
||||
database: 'odms',
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
|
||||
logger: new TestLogger('none'),
|
||||
logging: true,
|
||||
});
|
||||
return await s.initialize();
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
if (source) {
|
||||
await truncateAllTable(source);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await source?.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('第一階層が第五階層へのトライアルライセンス発行が完了する', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
// アカウントの階層構造を作成。
|
||||
const { tier1Accounts, tier4Accounts } = await makeHierarchicalAccounts(
|
||||
source,
|
||||
);
|
||||
const tier1AccountId = tier1Accounts[0].account.id;
|
||||
const tier1ExternalId = tier1Accounts[0].users[0].external_id;
|
||||
const tier4AccountId = tier4Accounts[0].account.id;
|
||||
const { id: tier5AccountId } = await makeTestSimpleAccount(source, {
|
||||
tier: 5,
|
||||
parent_account_id: tier4AccountId,
|
||||
});
|
||||
|
||||
await makeTestUser(source, {
|
||||
account_id: tier5AccountId,
|
||||
external_id: 'tier5UserId',
|
||||
role: 'admin',
|
||||
author_id: undefined,
|
||||
});
|
||||
|
||||
const usersService = module.get<UsersService>(UsersService);
|
||||
const licenseService = module.get<LicensesService>(LicensesService);
|
||||
const sendGridService = module.get<SendGridService>(SendGridService);
|
||||
|
||||
// メール送信サービスのモックによって初期化される値たち
|
||||
let _subject = '';
|
||||
let _url: string | undefined = '';
|
||||
let addressToTier5Admin = '';
|
||||
let addressCcDealerList: string[] = [];
|
||||
|
||||
// ユーザー取得処理をモック化
|
||||
overrideAdB2cService(usersService, {
|
||||
getUsers: async (context, externalIds) => {
|
||||
if (externalIds.includes('tier5UserId')) {
|
||||
// 第五階層のユーザーの場合
|
||||
return externalIds.map((x) => ({
|
||||
displayName: `tier5Admin${x}`,
|
||||
id: x,
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'xxxxxx',
|
||||
issuerAssignedId: `tier5Admin+${x}@example.com`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
// 第五階層以外の場合
|
||||
return externalIds.map((x) => ({
|
||||
displayName: 'upperTieradmin',
|
||||
id: x,
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'xxxxxx',
|
||||
issuerAssignedId: `upperTierAdmin+${x}@example.com`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
// メール送信サービスをモック化
|
||||
overrideSendgridService(licenseService, {
|
||||
sendMail: jest.fn(
|
||||
async (
|
||||
context: Context,
|
||||
to: string[],
|
||||
cc: string[],
|
||||
from: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
) => {
|
||||
const urlPattern = /https?:\/\/[^\s]+/g;
|
||||
const urls = text.match(urlPattern);
|
||||
const url = urls?.pop();
|
||||
|
||||
// 件名
|
||||
_subject = subject;
|
||||
// URL
|
||||
_url = url;
|
||||
// 第5階層の宛先
|
||||
addressToTier5Admin = to[0];
|
||||
// ディーラーの宛先
|
||||
addressCcDealerList = cc;
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
await licenseService.issueTrialLicense(
|
||||
context,
|
||||
tier1ExternalId,
|
||||
tier5AccountId,
|
||||
);
|
||||
const dbSelectResult = await selectIssuedLicensesAndLicenseOrders(
|
||||
source,
|
||||
tier5AccountId,
|
||||
tier1AccountId,
|
||||
);
|
||||
if (dbSelectResult === null) fail();
|
||||
|
||||
const { order, licenses } = dbSelectResult;
|
||||
|
||||
// 注文の確認
|
||||
expect(order.from_account_id).toEqual(tier5AccountId);
|
||||
expect(order.to_account_id).toEqual(tier1AccountId);
|
||||
expect(order.po_number).toBeNull();
|
||||
expect(order.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(order.status).toEqual(LICENSE_ISSUE_STATUS.ISSUED);
|
||||
// 10個注文されている
|
||||
expect(order.quantity).toEqual(10);
|
||||
|
||||
// ライセンスの確認
|
||||
// 値のチェックは最初と最後の1つのみ
|
||||
// 10個発行されている
|
||||
expect(licenses.length).toEqual(10);
|
||||
|
||||
// 1レコード目
|
||||
const firstLicense = licenses[0];
|
||||
expect(firstLicense.account_id).toEqual(tier5AccountId);
|
||||
expect(firstLicense.order_id).toEqual(order.id);
|
||||
expect(firstLicense.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(firstLicense.status).toEqual(LICENSE_ALLOCATED_STATUS.UNALLOCATED);
|
||||
expect(firstLicense.expiry_date).toEqual(
|
||||
new NewTrialLicenseExpirationDate(),
|
||||
);
|
||||
|
||||
// 10レコード目
|
||||
const lastLicense = licenses.slice(-1)[0];
|
||||
expect(lastLicense?.account_id).toEqual(tier5AccountId);
|
||||
expect(lastLicense?.order_id).toEqual(order.id);
|
||||
expect(lastLicense?.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(lastLicense?.status).toEqual(LICENSE_ALLOCATED_STATUS.UNALLOCATED);
|
||||
expect(lastLicense?.expiry_date).toEqual(
|
||||
new NewTrialLicenseExpirationDate(),
|
||||
);
|
||||
|
||||
// メールが期待通り送信されていること
|
||||
// 件名
|
||||
expect(_subject).toBe('Issued Trial License Notification [U-125]');
|
||||
// URL
|
||||
expect(_url).toBe('http://localhost:8081/');
|
||||
// 第五階層の宛先
|
||||
expect(addressToTier5Admin).toBeTruthy();
|
||||
// ディーラーの宛先(第一階層宛のみ)
|
||||
expect(addressCcDealerList).toHaveLength(1);
|
||||
// メール送信が呼ばれた回数を検査(第五階層と第一階層に同じメールを送信)
|
||||
expect(sendGridService.sendMail).toBeCalledTimes(1);
|
||||
});
|
||||
it('第二階層が第五階層へのトライアルライセンス発行が完了する', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
// アカウントの階層構造を作成。
|
||||
const { tier2Accounts, tier4Accounts } = await makeHierarchicalAccounts(
|
||||
source,
|
||||
);
|
||||
const tier2AccountId = tier2Accounts[0].account.id;
|
||||
const tier2ExternalId = tier2Accounts[0].users[0].external_id;
|
||||
const tier4AccountId = tier4Accounts[0].account.id;
|
||||
const { id: tier5AccountId } = await makeTestSimpleAccount(source, {
|
||||
tier: 5,
|
||||
parent_account_id: tier4AccountId,
|
||||
});
|
||||
|
||||
await makeTestUser(source, {
|
||||
account_id: tier5AccountId,
|
||||
external_id: 'tier5UserId',
|
||||
role: 'admin',
|
||||
author_id: undefined,
|
||||
});
|
||||
|
||||
const usersService = module.get<UsersService>(UsersService);
|
||||
const licenseService = module.get<LicensesService>(LicensesService);
|
||||
const sendGridService = module.get<SendGridService>(SendGridService);
|
||||
|
||||
// メール送信サービスのモックによって初期化される値たち
|
||||
let _subject = '';
|
||||
let _url: string | undefined = '';
|
||||
let expirationDate: string | undefined = '';
|
||||
let addressToTier5Admin = '';
|
||||
let addressCcDealerList: string[] = [];
|
||||
|
||||
// ユーザー取得処理をモック化
|
||||
overrideAdB2cService(usersService, {
|
||||
getUsers: async (context, externalIds) => {
|
||||
if (externalIds.includes('tier5UserId')) {
|
||||
// 第五階層のユーザーの場合
|
||||
return externalIds.map((x) => ({
|
||||
displayName: `tier5Admin${x}`,
|
||||
id: x,
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'xxxxxx',
|
||||
issuerAssignedId: `tier5Admin+${x}@example.com`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
||||
// 第五階層以外の場合
|
||||
return externalIds.map((x) => ({
|
||||
displayName: 'upperTieradmin',
|
||||
id: x,
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'xxxxxx',
|
||||
issuerAssignedId: `upperTierAdmin+${x}@example.com`,
|
||||
},
|
||||
],
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
// メール送信サービスをモック化
|
||||
overrideSendgridService(licenseService, {
|
||||
sendMail: jest.fn(
|
||||
async (
|
||||
context: Context,
|
||||
to: string[],
|
||||
cc: string[],
|
||||
from: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
) => {
|
||||
const urlPattern = /https?:\/\/[^\s]+/g;
|
||||
const urls = text.match(urlPattern);
|
||||
const url = urls?.pop();
|
||||
|
||||
// 件名
|
||||
_subject = subject;
|
||||
// URL
|
||||
_url = url;
|
||||
// 第5階層の宛先
|
||||
addressToTier5Admin = to[0];
|
||||
// ディーラーの宛先
|
||||
addressCcDealerList = cc;
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
await licenseService.issueTrialLicense(
|
||||
context,
|
||||
tier2ExternalId,
|
||||
tier5AccountId,
|
||||
);
|
||||
const dbSelectResult = await selectIssuedLicensesAndLicenseOrders(
|
||||
source,
|
||||
tier5AccountId,
|
||||
tier2AccountId,
|
||||
);
|
||||
if (dbSelectResult === null) fail();
|
||||
|
||||
const { order, licenses } = dbSelectResult;
|
||||
|
||||
// 注文の確認
|
||||
expect(order.from_account_id).toEqual(tier5AccountId);
|
||||
expect(order.to_account_id).toEqual(tier2AccountId);
|
||||
expect(order.po_number).toBeNull();
|
||||
expect(order.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(order.status).toEqual(LICENSE_ISSUE_STATUS.ISSUED);
|
||||
// 10個注文されている
|
||||
expect(order.quantity).toEqual(10);
|
||||
|
||||
// ライセンスの確認
|
||||
// 値のチェックは最初と最後の1つのみ
|
||||
// 10個発行されている
|
||||
expect(licenses.length).toEqual(10);
|
||||
|
||||
// 1レコード目
|
||||
const firstLicense = licenses[0];
|
||||
expect(firstLicense.account_id).toEqual(tier5AccountId);
|
||||
expect(firstLicense.order_id).toEqual(order.id);
|
||||
expect(firstLicense.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(firstLicense.status).toEqual(LICENSE_ALLOCATED_STATUS.UNALLOCATED);
|
||||
expect(firstLicense.expiry_date).toEqual(
|
||||
new NewTrialLicenseExpirationDate(),
|
||||
);
|
||||
|
||||
// 10レコード目
|
||||
const lastLicense = licenses.slice(-1)[0];
|
||||
expect(lastLicense?.account_id).toEqual(tier5AccountId);
|
||||
expect(lastLicense?.order_id).toEqual(order.id);
|
||||
expect(lastLicense?.type).toEqual(LICENSE_TYPE.TRIAL);
|
||||
expect(lastLicense?.status).toEqual(LICENSE_ALLOCATED_STATUS.UNALLOCATED);
|
||||
expect(lastLicense?.expiry_date).toEqual(
|
||||
new NewTrialLicenseExpirationDate(),
|
||||
);
|
||||
|
||||
// メールが期待通り送信されていること
|
||||
// 件名
|
||||
expect(_subject).toBe('Issued Trial License Notification [U-125]');
|
||||
// URL
|
||||
expect(_url).toBe('http://localhost:8081/');
|
||||
// 第五階層の宛先
|
||||
expect(addressToTier5Admin).toBeTruthy();
|
||||
// ディーラーの宛先(第一階層宛と第二階層宛)
|
||||
expect(addressCcDealerList).toHaveLength(2);
|
||||
// メール送信が呼ばれた回数を検査(第五階層と第一階層に同じメールを送信)
|
||||
expect(sendGridService.sendMail).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('DBアクセスに失敗した場合、500エラーを返却する', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { external_id: externalId } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'userId',
|
||||
role: 'admin',
|
||||
author_id: undefined,
|
||||
});
|
||||
|
||||
const service = module.get<LicensesService>(LicensesService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
//DBアクセスに失敗するようにする
|
||||
const licensesService = module.get<LicensesRepositoryService>(
|
||||
LicensesRepositoryService,
|
||||
);
|
||||
licensesService.issueTrialLicense = jest
|
||||
.fn()
|
||||
.mockRejectedValue('DB failed');
|
||||
|
||||
try {
|
||||
await service.issueTrialLicense(context, externalId, accountId);
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,12 +14,19 @@ import { UserNotFoundError } from '../../repositories/users/errors/types';
|
||||
import {
|
||||
GetAllocatableLicensesResponse,
|
||||
IssueCardLicensesResponse,
|
||||
NewTrialLicenseExpirationDate,
|
||||
} from './types/types';
|
||||
import { Context } from '../../common/log';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
|
||||
import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils';
|
||||
import { LICENSE_ISSUE_STATUS } from '../../constants';
|
||||
import {
|
||||
ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY,
|
||||
LICENSE_ISSUE_STATUS,
|
||||
TIERS,
|
||||
TRIAL_LICENSE_EXPIRATION_DAYS,
|
||||
} from '../../constants';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class LicensesService {
|
||||
@ -452,6 +459,106 @@ export class LicensesService {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* トライアルライセンスを発行する。発行したライセンスに紐づくライセンス注文も作成する。
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param issuedAccountId
|
||||
*/
|
||||
async issueTrialLicense(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
issuedAccountId: number,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.issueTrialLicense.name
|
||||
} | params: { externalId: ${externalId}, issuedAccountId: ${issuedAccountId} };`,
|
||||
);
|
||||
|
||||
let me: User;
|
||||
let ownAccountId: number;
|
||||
|
||||
// ユーザIDからアカウントIDを取得する
|
||||
try {
|
||||
me = await this.usersRepository.findUserByExternalId(context, externalId);
|
||||
ownAccountId = me.account_id;
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// トライアルライセンスを発行
|
||||
const nowDate = new Date();
|
||||
const expired = new NewTrialLicenseExpirationDate(nowDate);
|
||||
try {
|
||||
await this.licensesRepository.issueTrialLicense(
|
||||
context,
|
||||
issuedAccountId,
|
||||
ownAccountId,
|
||||
nowDate,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
this.logger.error(
|
||||
`[${context.getTrackingId()}] issue traial lisences failed`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
try {
|
||||
// 第五階層へメール送信
|
||||
// 第五階層アカウント名と管理者メールアドレスを取得して送信
|
||||
const {
|
||||
adminEmails: tier5AdminMaileAddresses,
|
||||
companyName: tier5ComponyName,
|
||||
} = await this.getAccountInformation(context, issuedAccountId);
|
||||
|
||||
// 自アカウントの管理者にもメール通知
|
||||
const dealerEmails = (
|
||||
await this.getAccountInformation(context, ownAccountId)
|
||||
).adminEmails;
|
||||
|
||||
// 第二階層によるトライアルライセンス発行の場合、第一階層の管理者にも通知する。
|
||||
if (me.account?.tier === TIERS.TIER2 && me.account?.parent_account_id) {
|
||||
const tire1AdminMails = (await this.getAccountInformation(
|
||||
context,
|
||||
me.account.parent_account_id,
|
||||
)).adminEmails;
|
||||
dealerEmails.unshift(...tire1AdminMails);
|
||||
}
|
||||
await this.sendgridService.sendMailWithU125(
|
||||
context,
|
||||
tier5AdminMaileAddresses,
|
||||
tier5ComponyName,
|
||||
ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY,
|
||||
TRIAL_LICENSE_EXPIRATION_DAYS,
|
||||
dealerEmails,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
// メール送信に関する例外はログだけ出して握りつぶす
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.issueTrialLicense.name}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウントIDを指定して、アカウント情報と管理者情報を取得する
|
||||
* @param context
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { DataSource, IsNull } from 'typeorm';
|
||||
import {
|
||||
License,
|
||||
CardLicense,
|
||||
@ -214,3 +214,45 @@ export const getLicenseAllocationHistoryArchive = async (
|
||||
): Promise<LicenseAllocationHistoryArchive[]> => {
|
||||
return await dataSource.getRepository(LicenseAllocationHistoryArchive).find();
|
||||
};
|
||||
|
||||
/**
|
||||
* テストユーティリティ: トライアルライセンス発行数とそれに紐づく注文を取得します。
|
||||
* @param datasource データソース
|
||||
* @param fromAccountId 注文元アカウントID
|
||||
* @param toAccountId 注文先アカウントID
|
||||
* @returns licenses, orders
|
||||
*/
|
||||
export const selectIssuedLicensesAndLicenseOrders = async (
|
||||
datasource: DataSource,
|
||||
fromAccountId: number,
|
||||
toAccountId: number,
|
||||
): Promise<{
|
||||
order: LicenseOrder;
|
||||
licenses: License[];
|
||||
} | null> => {
|
||||
// 注文を取得
|
||||
const order = await datasource.getRepository(LicenseOrder).findOne({
|
||||
where: {
|
||||
from_account_id: fromAccountId,
|
||||
to_account_id: toAccountId,
|
||||
po_number: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!order) return null;
|
||||
|
||||
// 注文に紐づくライセンスを取得
|
||||
const licenses = await datasource.getRepository(License).find({
|
||||
where: {
|
||||
account_id: fromAccountId,
|
||||
order_id: order.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!licenses) return null;
|
||||
|
||||
return {
|
||||
order,
|
||||
licenses,
|
||||
};
|
||||
};
|
||||
|
||||
@ -59,6 +59,16 @@ export class GetAllocatableLicensesResponse {
|
||||
allocatableLicenses: AllocatableLicenseInfo[];
|
||||
}
|
||||
|
||||
export class IssueTrialLicenseRequest {
|
||||
@ApiProperty()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
issuedAccount: number;
|
||||
}
|
||||
|
||||
export class IssueTrialLicenseResponse {}
|
||||
|
||||
export class CancelOrderRequest {
|
||||
@ApiProperty()
|
||||
@Matches(/^[A-Z0-9]+$/)
|
||||
|
||||
@ -132,6 +132,12 @@ export class TasksController {
|
||||
const direction = isSortDirection(body.direction ?? '')
|
||||
? (body.direction as SortDirection)
|
||||
: undefined;
|
||||
const filterConditionAuthorId = body.authorId?.trimStart()
|
||||
? body.authorId.trimStart()
|
||||
: undefined;
|
||||
const filterConditionFileName = body.fileName?.trimStart()
|
||||
? body.fileName.trimStart()
|
||||
: undefined;
|
||||
|
||||
const { tasks, total } = await this.taskService.getTasks(
|
||||
context,
|
||||
@ -143,6 +149,8 @@ export class TasksController {
|
||||
status?.split(','),
|
||||
paramName,
|
||||
direction,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
);
|
||||
return { tasks, total, limit, offset };
|
||||
}
|
||||
@ -861,7 +869,7 @@ export class TasksController {
|
||||
@ApiOperation({
|
||||
operationId: 'reopen',
|
||||
description:
|
||||
'終了した文字起こしタスクを再開します(ステータスをPendingにします)',
|
||||
'完了した文字起こしタスクを再開します(ステータスをPendingにします)',
|
||||
})
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -75,6 +75,8 @@ export class TasksService {
|
||||
status?: string[],
|
||||
paramName?: TaskListSortableAttribute,
|
||||
direction?: SortDirection,
|
||||
filterConditionAuthorId?: string | null,
|
||||
filterConditionFileName?: string | null,
|
||||
): Promise<{ tasks: Task[]; total: number }> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.getTasks.name} | params: { ` +
|
||||
@ -84,7 +86,10 @@ export class TasksService {
|
||||
`limit: ${limit}, ` +
|
||||
`status: ${status}, ` +
|
||||
`paramName: ${paramName}, ` +
|
||||
`direction: ${direction} };`,
|
||||
`direction: ${direction}, ` +
|
||||
`filterConditionAuthorId: ${filterConditionAuthorId},` +
|
||||
`filterConditionFileName: ${filterConditionFileName}
|
||||
};`,
|
||||
);
|
||||
|
||||
// パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える
|
||||
@ -106,6 +111,8 @@ export class TasksService {
|
||||
paramName ?? defaultParamName,
|
||||
direction ?? defaultDirection,
|
||||
status ?? defaultStatus,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
);
|
||||
|
||||
// B2Cからユーザー名を取得する
|
||||
@ -134,6 +141,8 @@ export class TasksService {
|
||||
paramName ?? defaultParamName,
|
||||
direction ?? defaultDirection,
|
||||
status ?? defaultStatus,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
);
|
||||
|
||||
// B2Cからユーザー名を取得する
|
||||
@ -156,6 +165,8 @@ export class TasksService {
|
||||
paramName ?? defaultParamName,
|
||||
direction ?? defaultDirection,
|
||||
status ?? defaultStatus,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
);
|
||||
// B2Cからユーザー名を取得する
|
||||
const b2cUsers = await this.getB2cUsers(
|
||||
|
||||
@ -27,6 +27,7 @@ import { NotificationhubModule } from '../../../gateways/notificationhub/notific
|
||||
import { BlobstorageModule } from '../../../gateways/blobstorage/blobstorage.module';
|
||||
import { AuthGuardsModule } from '../../../common/guards/auth/authguards.module';
|
||||
import { SortCriteriaRepositoryModule } from '../../../repositories/sort_criteria/sort_criteria.repository.module';
|
||||
import { TaskFiltersRepositoryModule } from '../../../repositories/task_filters/task_filter.repository.module';
|
||||
import { AuthService } from '../../../features/auth/auth.service';
|
||||
import { AccountsService } from '../../../features/accounts/accounts.service';
|
||||
import { UsersService } from '../../../features/users/users.service';
|
||||
@ -74,6 +75,7 @@ export const makeTaskTestingModuleWithNotificaiton = async (
|
||||
BlobstorageModule,
|
||||
AuthGuardsModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
TaskFiltersRepositoryModule,
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
@ -110,8 +112,9 @@ export const createTask = async (
|
||||
priority: string,
|
||||
jobNumber: string,
|
||||
status: string,
|
||||
typist_user_id?: number | undefined,
|
||||
is_job_number_enabled?: boolean | undefined,
|
||||
typist_user_id?: number,
|
||||
is_job_number_enabled?: boolean,
|
||||
file_name?: string,
|
||||
): Promise<{ taskId: number; audioFileId: number }> => {
|
||||
const { identifiers: audioFileIdentifiers } = await datasource
|
||||
.getRepository(AudioFile)
|
||||
@ -119,8 +122,8 @@ export const createTask = async (
|
||||
account_id: account_id,
|
||||
owner_user_id: owner_user_id,
|
||||
url: '',
|
||||
file_name: 'x.zip',
|
||||
raw_file_name: 'y.zip',
|
||||
file_name: file_name ?? 'x.zip',
|
||||
raw_file_name: file_name ?? 'y.zip',
|
||||
author_id: author_id,
|
||||
work_type_id: work_type_id,
|
||||
started_at: new Date(),
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
@ -65,6 +66,22 @@ export class TasksRequest {
|
||||
})
|
||||
@IsOptional()
|
||||
paramName?: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: `タスクの検索キーワード:AuthorID`,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
authorId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: `タスクの検索キーワード:fileName`,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
// TODO: RequestでもResponseでも使われているので、Requestに使用される箇所のみバリデータでチェックが行われる状態になっている
|
||||
|
||||
@ -12,6 +12,8 @@ import { LicensesRepositoryService } from '../../../repositories/licenses/licens
|
||||
import { UsersService } from '../users.service';
|
||||
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
|
||||
import { SortCriteriaRepositoryService } from '../../../repositories/sort_criteria/sort_criteria.repository.service';
|
||||
import { TaskFilters } from '../../../repositories/task_filters/entity/task_filters.entity';
|
||||
import { TaskFiltersRepositoryService } from '../../../repositories/task_filters/task_filter.repository.service';
|
||||
import {
|
||||
SortDirection,
|
||||
TaskListSortableAttribute,
|
||||
@ -26,6 +28,11 @@ export type SortCriteriaRepositoryMockValue = {
|
||||
getSortCriteria: SortCriteria | Error;
|
||||
};
|
||||
|
||||
export type TaskFiltersRepositoryMockValue = {
|
||||
updateTaskFilter: TaskFilters | Error;
|
||||
getTaskFilter: TaskFilters | Error;
|
||||
};
|
||||
|
||||
export type UsersRepositoryMockValue = {
|
||||
updateUserVerified: undefined | Error;
|
||||
findUserById: User | Error;
|
||||
@ -63,6 +70,7 @@ export const makeUsersServiceMock = async (
|
||||
sendGridMockValue: SendGridMockValue,
|
||||
configMockValue: ConfigMockValue,
|
||||
sortCriteriaRepositoryMockValue: SortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue: TaskFiltersRepositoryMockValue,
|
||||
): Promise<UsersService> => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UsersService],
|
||||
@ -90,6 +98,8 @@ export const makeUsersServiceMock = async (
|
||||
return makeSortCriteriaRepositoryMock(
|
||||
sortCriteriaRepositoryMockValue,
|
||||
);
|
||||
case TaskFiltersRepositoryService:
|
||||
return makeTaskFiltersRepositoryMock(taskFiltersRepositoryMockValue);
|
||||
case BlobstorageService:
|
||||
return {};
|
||||
}
|
||||
@ -128,6 +138,29 @@ export const makeSortCriteriaRepositoryMock = (
|
||||
};
|
||||
};
|
||||
|
||||
export const makeTaskFiltersRepositoryMock = (
|
||||
value: TaskFiltersRepositoryMockValue,
|
||||
) => {
|
||||
const { updateTaskFilter, getTaskFilter } = value;
|
||||
|
||||
return {
|
||||
updateTaskFilter:
|
||||
updateTaskFilter instanceof Error
|
||||
? jest
|
||||
.fn<Promise<void>, [number, string, string]>()
|
||||
.mockRejectedValue(updateTaskFilter)
|
||||
: jest
|
||||
.fn<Promise<TaskFilters>, [number, string, string]>()
|
||||
.mockResolvedValue(updateTaskFilter),
|
||||
getTaskFilter:
|
||||
getTaskFilter instanceof Error
|
||||
? jest.fn<Promise<void>, [number]>().mockRejectedValue(getTaskFilter)
|
||||
: jest
|
||||
.fn<Promise<TaskFilters>, [number]>()
|
||||
.mockResolvedValue(getTaskFilter),
|
||||
};
|
||||
};
|
||||
|
||||
export const makeSendGridServiceMock = (value: SendGridMockValue) => {
|
||||
const { sendMail } = value;
|
||||
return {
|
||||
@ -291,6 +324,21 @@ export const makeDefaultSortCriteriaRepositoryMockValue =
|
||||
};
|
||||
};
|
||||
|
||||
export const makeDefaultTaskFiltersRepositoryMockValue =
|
||||
(): TaskFiltersRepositoryMockValue => {
|
||||
const taskFilter = new TaskFilters();
|
||||
{
|
||||
taskFilter.id = 1;
|
||||
taskFilter.author_id = null;
|
||||
taskFilter.file_name = null;
|
||||
taskFilter.user_id = 1;
|
||||
}
|
||||
return {
|
||||
updateTaskFilter: taskFilter,
|
||||
getTaskFilter: taskFilter,
|
||||
};
|
||||
};
|
||||
|
||||
export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => {
|
||||
return {
|
||||
getMetaData: {
|
||||
|
||||
@ -35,6 +35,15 @@ export class ConfirmRequest {
|
||||
|
||||
export class ConfirmResponse {}
|
||||
|
||||
export class ConfirmForceRequest {
|
||||
@ApiProperty()
|
||||
@IsInt()
|
||||
userId: number;
|
||||
}
|
||||
|
||||
export class ConfirmForceResponse {}
|
||||
|
||||
|
||||
export class User {
|
||||
@ApiProperty()
|
||||
id: number;
|
||||
@ -84,6 +93,17 @@ export class User {
|
||||
licenseStatus: string;
|
||||
}
|
||||
|
||||
export class GetUsersRequest {
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
userName?: string;
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export class GetUsersResponse {
|
||||
@ApiProperty({ type: [User] })
|
||||
users: User[];
|
||||
@ -223,6 +243,30 @@ export class GetSortCriteriaResponse {
|
||||
paramName: string;
|
||||
}
|
||||
|
||||
export class PostTaskFiltersRequest {
|
||||
@ApiProperty({ description: 'タスクの検索キーワードを更新する:AuthorID' })
|
||||
filterConditionAuthorId: string;
|
||||
|
||||
@ApiProperty({ description: 'タスクの検索キーワードを更新する:fileName' })
|
||||
filterConditionFileName: string;
|
||||
}
|
||||
|
||||
export class PostTaskFiltersResponse {}
|
||||
|
||||
export class GetTaskFiltersRequest {}
|
||||
export class GetTaskFiltersResponse {
|
||||
@ApiProperty({
|
||||
description: 'タスクの検索キーワードを取得する:AuthorID',
|
||||
required: false,
|
||||
})
|
||||
authorId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'タスクの検索キーワードを取得する:fileName',
|
||||
required: false,
|
||||
})
|
||||
fileName?: string;
|
||||
}
|
||||
export class PostUpdateUserRequest {
|
||||
@ApiProperty()
|
||||
@Type(() => Number)
|
||||
|
||||
@ -32,6 +32,10 @@ import {
|
||||
PostSortCriteriaResponse,
|
||||
GetSortCriteriaRequest,
|
||||
GetSortCriteriaResponse,
|
||||
PostTaskFiltersRequest,
|
||||
PostTaskFiltersResponse,
|
||||
GetTaskFiltersRequest,
|
||||
GetTaskFiltersResponse,
|
||||
PostUpdateUserRequest,
|
||||
PostUpdateUserResponse,
|
||||
AllocateLicenseResponse,
|
||||
@ -47,6 +51,9 @@ import {
|
||||
PostMultipleImportsResponse,
|
||||
PostMultipleImportsCompleteRequest,
|
||||
PostMultipleImportsCompleteResponse,
|
||||
ConfirmForceRequest,
|
||||
ConfirmForceResponse,
|
||||
GetUsersRequest,
|
||||
} from './types/types';
|
||||
import { UsersService } from './users.service';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
@ -157,6 +164,85 @@ export class UsersController {
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: ConfirmResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'メール認証済み',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'confirmUserForce',
|
||||
description: 'ユーザーを強制的にメール認証済にする',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [ADMIN_ROLES.ADMIN],
|
||||
tiers: [TIERS.TIER5],
|
||||
delegation: true,
|
||||
}),
|
||||
)
|
||||
@Post('confirm/force')
|
||||
async confirmUserForce(
|
||||
@Body() body: ConfirmForceRequest,
|
||||
@Req() req: Request,
|
||||
): Promise<ConfirmForceResponse> {
|
||||
const { userId } = body;
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId: loginUserId } = decodedAccessToken as AccessToken;
|
||||
const context = makeContext(loginUserId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
await this.usersService.confirmUserForce(context, userId);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: GetUsersResponse,
|
||||
@ -179,7 +265,14 @@ export class UsersController {
|
||||
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
|
||||
)
|
||||
@Get()
|
||||
async getUsers(@Req() req: Request): Promise<GetUsersResponse> {
|
||||
async getUsers(
|
||||
@Req() req: Request,
|
||||
@Query() query: GetUsersRequest,
|
||||
): Promise<GetUsersResponse> {
|
||||
|
||||
const userName = query.userName?.trimStart();
|
||||
const email = query.email?.trimStart();
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
|
||||
if (!accessToken) {
|
||||
@ -216,7 +309,12 @@ export class UsersController {
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
const users = await this.usersService.getUsers(context, userId);
|
||||
const users = await this.usersService.getUsers(
|
||||
context,
|
||||
userId,
|
||||
userName,
|
||||
email,
|
||||
);
|
||||
return { users };
|
||||
}
|
||||
|
||||
@ -535,6 +633,163 @@ export class UsersController {
|
||||
return { direction, paramName };
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: PostTaskFiltersResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: '不正なパラメータ',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'updateTaskFilter',
|
||||
description: 'ログインしているユーザーの検索条件を更新します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@Post('task-filters')
|
||||
async updateTaskFilter(
|
||||
@Body() body: PostTaskFiltersRequest,
|
||||
@Req() req: Request,
|
||||
): Promise<PostTaskFiltersResponse> {
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId } = decodedAccessToken as AccessToken;
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
const filterConditionAuthorId = body.filterConditionAuthorId?.trimStart()
|
||||
? body.filterConditionAuthorId.trimStart()
|
||||
: null;
|
||||
const filterConditionFileName = body.filterConditionFileName?.trimStart()
|
||||
? body.filterConditionFileName.trimStart()
|
||||
: null;
|
||||
|
||||
await this.usersService.updateTaskFilter(
|
||||
context,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
userId,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: GetTaskFiltersResponse,
|
||||
description: '成功時のレスポンス',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: '認証エラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: '不正なパラメータ',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: '想定外のサーバーエラー',
|
||||
type: ErrorResponse,
|
||||
})
|
||||
@ApiOperation({
|
||||
operationId: 'getTaskFilter',
|
||||
description: 'ログインしているユーザーのタスクの検索条件を取得します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('task-filters')
|
||||
async getTaskFilter(
|
||||
@Query() query: GetTaskFiltersRequest,
|
||||
@Req() req: Request,
|
||||
): Promise<GetTaskFiltersResponse> {
|
||||
const {} = query;
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req);
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const ip = retrieveIp(req);
|
||||
if (!ip) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000401'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const requestId = retrieveRequestId(req);
|
||||
if (!requestId) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000501'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId } = decodedAccessToken as AccessToken;
|
||||
const context = makeContext(userId, requestId);
|
||||
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
|
||||
|
||||
const { authorId, fileName } = await this.usersService.getTaskFilter(
|
||||
context,
|
||||
userId,
|
||||
);
|
||||
return { authorId, fileName };
|
||||
}
|
||||
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
type: PostUpdateUserResponse,
|
||||
|
||||
@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
|
||||
import { AdB2cModule } from '../../gateways/adb2c/adb2c.module';
|
||||
import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module';
|
||||
import { SortCriteriaRepositoryModule } from '../../repositories/sort_criteria/sort_criteria.repository.module';
|
||||
import { TaskFiltersRepositoryModule } from '../../repositories/task_filters/task_filter.repository.module';
|
||||
import { UsersRepositoryModule } from '../../repositories/users/users.repository.module';
|
||||
import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module';
|
||||
import { UsersController } from './users.controller';
|
||||
@ -17,6 +18,7 @@ import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module
|
||||
UsersRepositoryModule,
|
||||
LicensesRepositoryModule,
|
||||
SortCriteriaRepositoryModule,
|
||||
TaskFiltersRepositoryModule,
|
||||
AdB2cModule,
|
||||
SendGridModule,
|
||||
ConfigModule,
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
makeDefaultConfigValue,
|
||||
makeDefaultSendGridlValue,
|
||||
makeDefaultSortCriteriaRepositoryMockValue,
|
||||
makeDefaultTaskFiltersRepositoryMockValue,
|
||||
makeDefaultUsersRepositoryMockValue,
|
||||
makeUsersServiceMock,
|
||||
} from './test/users.service.mock';
|
||||
@ -60,6 +61,7 @@ import { createCheckoutPermissions } from '../tasks/test/utility';
|
||||
import { MultipleImportErrors } from './types/types';
|
||||
import { TestLogger } from '../../common/test/logger';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
import { CUSTOMER_NAME } from '../../templates/constants';
|
||||
|
||||
describe('UsersService.confirmUser', () => {
|
||||
let source: DataSource | null = null;
|
||||
@ -123,7 +125,7 @@ describe('UsersService.confirmUser', () => {
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
let _subject: string = '';
|
||||
let _subject = '';
|
||||
let _url: string | undefined = '';
|
||||
overrideSendgridService(service, {
|
||||
sendMail: async (
|
||||
@ -320,7 +322,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
|
||||
};
|
||||
},
|
||||
});
|
||||
let _subject: string = '';
|
||||
let _subject = '';
|
||||
overrideSendgridService(service, {
|
||||
sendMail: async (
|
||||
context: Context,
|
||||
@ -892,7 +894,7 @@ describe('UsersService.createUser', () => {
|
||||
};
|
||||
},
|
||||
});
|
||||
let _subject: string = '';
|
||||
let _subject = '';
|
||||
let _url: string | undefined = '';
|
||||
overrideSendgridService(service, {
|
||||
sendMail: async (
|
||||
@ -2049,6 +2051,331 @@ describe('UsersService.getUsers', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前入力メール未入力検索)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(
|
||||
source,
|
||||
{
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
},
|
||||
);
|
||||
const { id: user2 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id2',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID2',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
const { id: user3 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id3',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID3',
|
||||
auto_renew: false,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, 'test1', undefined);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].name).toBe('test1');
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前未入力メール入力検索)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(
|
||||
source,
|
||||
{
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
},
|
||||
);
|
||||
const { id: user2 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id2',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID2',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
const { id: user3 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id3',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID3',
|
||||
auto_renew: false,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, undefined, 'test2@mail.com');
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].email).toBe('test2@mail.com');
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前/メール入力検索)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(
|
||||
source,
|
||||
{
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
},
|
||||
);
|
||||
const { id: user2 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id2',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID2',
|
||||
auto_renew: true,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
const { id: user3 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id3',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID3',
|
||||
auto_renew: false,
|
||||
encryption: false,
|
||||
encryption_password: undefined,
|
||||
prompt: false,
|
||||
});
|
||||
|
||||
const expectedUser = {
|
||||
id: user3,
|
||||
name: 'test3',
|
||||
role: 'author',
|
||||
authorId: 'AUTHOR_ID3',
|
||||
typistGroupName: [],
|
||||
email: 'test3@mail.com',
|
||||
emailVerified: true,
|
||||
autoRenew: false,
|
||||
notification: true,
|
||||
encryption: false,
|
||||
prompt: false,
|
||||
expiration: undefined,
|
||||
remaining: undefined,
|
||||
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
|
||||
}
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, '3', 'test3@mail');
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toEqual(expectedUser);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前入力メール未入力検索で0件)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, 'nonexistent', undefined);
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前未入力メール入力検索で0件)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, undefined, 'wrongemail@example.com');
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前/メール入力で0件)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, 'test1', 'wrongemail@example.com');
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前/メール入力で0件)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1, 'wronguser', 'test1@mail.com');
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('ユーザーを取得できること(名前メール未入力)', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModuleWithAdb2c(source, adb2cParam);
|
||||
if (!module) fail();
|
||||
|
||||
const { id: accountId } = await makeTestSimpleAccount(source);
|
||||
const { id: user1, external_id: external_id1 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id1',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID1',
|
||||
});
|
||||
const { id: user2 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id2',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID2',
|
||||
});
|
||||
const { id: user3 } = await makeTestUser(source, {
|
||||
account_id: accountId,
|
||||
external_id: 'external_id3',
|
||||
role: 'author',
|
||||
author_id: 'AUTHOR_ID3',
|
||||
});
|
||||
|
||||
const expectedUsers = [
|
||||
{
|
||||
id: user1,
|
||||
name: 'test1',
|
||||
role: 'author',
|
||||
authorId: 'AUTHOR_ID1',
|
||||
typistGroupName: [],
|
||||
email: 'test1@mail.com',
|
||||
emailVerified: true,
|
||||
autoRenew: true,
|
||||
notification: true,
|
||||
encryption: true,
|
||||
prompt: true,
|
||||
expiration: undefined,
|
||||
remaining: undefined,
|
||||
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
|
||||
},
|
||||
{
|
||||
id: user2,
|
||||
name: 'test2',
|
||||
role: 'author',
|
||||
authorId: 'AUTHOR_ID2',
|
||||
typistGroupName: [],
|
||||
email: 'test2@mail.com',
|
||||
emailVerified: true,
|
||||
autoRenew: true,
|
||||
notification: true,
|
||||
encryption: true,
|
||||
prompt: true,
|
||||
expiration: undefined,
|
||||
remaining: undefined,
|
||||
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
|
||||
},
|
||||
{
|
||||
id: user3,
|
||||
name: 'test3',
|
||||
role: 'author',
|
||||
authorId: 'AUTHOR_ID3',
|
||||
typistGroupName: [],
|
||||
email: 'test3@mail.com',
|
||||
emailVerified: true,
|
||||
autoRenew: true,
|
||||
notification: true,
|
||||
encryption: true,
|
||||
prompt: true,
|
||||
expiration: undefined,
|
||||
remaining: undefined,
|
||||
licenseStatus: USER_LICENSE_EXPIRY_STATUS.NO_LICENSE,
|
||||
},
|
||||
]
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
const result = await service.getUsers(context, external_id1);
|
||||
expect(result.length).toBe(3);
|
||||
expect(result).toEqual(expectedUsers)
|
||||
});
|
||||
|
||||
it('DBからのユーザーの取得に失敗した場合、エラーとなる', async () => {
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
if (!source) fail();
|
||||
@ -2112,6 +2439,8 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
@ -2119,6 +2448,7 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2140,6 +2470,8 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
|
||||
usersRepositoryMockValue.findUserByExternalId = new Error('user not found');
|
||||
|
||||
@ -2150,6 +2482,7 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2171,6 +2504,8 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
sortCriteriaRepositoryMockValue.updateSortCriteria = new Error(
|
||||
'sort criteria not found',
|
||||
);
|
||||
@ -2182,6 +2517,7 @@ describe('UsersService.updateSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2205,6 +2541,8 @@ describe('UsersService.getSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
@ -2212,6 +2550,7 @@ describe('UsersService.getSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2229,6 +2568,8 @@ describe('UsersService.getSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
|
||||
sortCriteriaRepositoryMockValue.getSortCriteria = new Error(
|
||||
'sort criteria not found',
|
||||
@ -2241,6 +2582,7 @@ describe('UsersService.getSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2262,6 +2604,8 @@ describe('UsersService.getSortCriteria', () => {
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
sortCriteriaRepositoryMockValue.getSortCriteria = {
|
||||
id: 1,
|
||||
direction: 'AAA',
|
||||
@ -2276,6 +2620,7 @@ describe('UsersService.getSortCriteria', () => {
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
@ -2290,6 +2635,182 @@ describe('UsersService.getSortCriteria', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('UsersService.updateTaskFilter', () => {
|
||||
it('タスク検索条件を変更できる', async () => {
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const licensesRepositoryMockValue = null;
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendgridMockValue = makeDefaultSendGridlValue();
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
expect(
|
||||
await service.updateTaskFilter(
|
||||
context,
|
||||
'AUTHOR_ID',
|
||||
'FILE_NAME',
|
||||
'external_id',
|
||||
),
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('ユーザー情報が存在せず、タスク検索条件を変更できない', async () => {
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const licensesRepositoryMockValue = null;
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendgridMockValue = makeDefaultSendGridlValue();
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
|
||||
usersRepositoryMockValue.findUserByExternalId = new Error('user not found');
|
||||
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
await expect(
|
||||
service.updateTaskFilter(
|
||||
context,
|
||||
'AUTHOR_ID',
|
||||
'FILE_NAME',
|
||||
'external_id',
|
||||
),
|
||||
).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('タスク検索条件が存在せず、タスク検索条件を変更できない', async () => {
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const licensesRepositoryMockValue = null;
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendgridMockValue = makeDefaultSendGridlValue();
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
taskFiltersRepositoryMockValue.updateTaskFilter = new Error(
|
||||
'task filters not found',
|
||||
);
|
||||
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
await expect(
|
||||
service.updateTaskFilter(
|
||||
context,
|
||||
'AUTHOR_ID',
|
||||
'FILE_NAME',
|
||||
'external_id',
|
||||
),
|
||||
).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UsersService.getTaskFilter', () => {
|
||||
it('タスク検索条件を取得できる', async () => {
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const licensesRepositoryMockValue = null;
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendgridMockValue = makeDefaultSendGridlValue();
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
console.log(await service.getTaskFilter(context, 'external_id'));
|
||||
expect(await service.getTaskFilter(context, 'external_id')).toEqual({
|
||||
authorId: undefined,
|
||||
fileName: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('タスク検索条件が存在せず、タスク検索条件を取得できない', async () => {
|
||||
const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue();
|
||||
const licensesRepositoryMockValue = null;
|
||||
const adb2cParam = makeDefaultAdB2cMockValue();
|
||||
const sendgridMockValue = makeDefaultSendGridlValue();
|
||||
const configMockValue = makeDefaultConfigValue();
|
||||
const sortCriteriaRepositoryMockValue =
|
||||
makeDefaultSortCriteriaRepositoryMockValue();
|
||||
const taskFiltersRepositoryMockValue =
|
||||
makeDefaultTaskFiltersRepositoryMockValue();
|
||||
|
||||
taskFiltersRepositoryMockValue.getTaskFilter = new Error(
|
||||
'task filters not found',
|
||||
);
|
||||
|
||||
const service = await makeUsersServiceMock(
|
||||
usersRepositoryMockValue,
|
||||
licensesRepositoryMockValue,
|
||||
adb2cParam,
|
||||
sendgridMockValue,
|
||||
configMockValue,
|
||||
sortCriteriaRepositoryMockValue,
|
||||
taskFiltersRepositoryMockValue,
|
||||
);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
await expect(service.getTaskFilter(context, 'external_id')).rejects.toEqual(
|
||||
new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UsersService.updateUser', () => {
|
||||
let source: DataSource | null = null;
|
||||
beforeAll(async () => {
|
||||
@ -5144,3 +5665,295 @@ describe('UsersService.multipleImportsComplate', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UsersService.confirmUserForce', () => {
|
||||
let source: DataSource | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (source == null) {
|
||||
source = await (async () => {
|
||||
const s = new DataSource({
|
||||
type: 'mysql',
|
||||
host: 'test_mysql_db',
|
||||
port: 3306,
|
||||
username: 'user',
|
||||
password: 'password',
|
||||
database: 'odms',
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: false, // trueにすると自動的にmigrationが行われるため注意
|
||||
logger: new TestLogger('none'),
|
||||
logging: true,
|
||||
});
|
||||
return await s.initialize();
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
if (source) {
|
||||
await truncateAllTable(source);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await source?.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('第五階層の管理者がメール認証済みではないユーザーを強制認証できる', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
const { account, admin } = await makeTestAccount(source, {
|
||||
tier: 5,
|
||||
});
|
||||
const { id: user1, external_id } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
author_id: 'AUTHOR_1',
|
||||
email_verified: false,
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
getUsers: async () => {
|
||||
return [
|
||||
{
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'admin@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: external_id,
|
||||
displayName: 'user1',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
getUser: async () => {
|
||||
return {
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
let mailSubject: string | undefined;
|
||||
let mailText: string | undefined;
|
||||
let mailTextUrl: string | undefined;
|
||||
let mailHtml: string | undefined;
|
||||
let mailHtmlUrl: string | undefined;
|
||||
let _to: string[] | undefined;
|
||||
let _cc: string[] | undefined;
|
||||
overrideSendgridService(service, {
|
||||
sendMail: jest.fn(
|
||||
async (
|
||||
context: Context,
|
||||
to: string[],
|
||||
cc: string[],
|
||||
from: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
) => {
|
||||
const urlPattern = /https?:\/\/[^\s]+/g;
|
||||
const mailTextUrls = text.match(urlPattern);
|
||||
const mailHtmlUrls = html.match(urlPattern);
|
||||
|
||||
mailSubject = subject;
|
||||
mailText = text;
|
||||
mailTextUrl = mailTextUrls?.pop();
|
||||
mailHtml = html;
|
||||
mailHtmlUrl = mailHtmlUrls?.pop();
|
||||
_to = to;
|
||||
_cc = cc;
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
// 強制認証を実行
|
||||
await service.confirmUserForce(context, user1);
|
||||
|
||||
// ユーザーのメール認証済みに変更されたことを確認
|
||||
{
|
||||
const user = await getUser(source, user1);
|
||||
if (!user) fail();
|
||||
expect(user.email_verified).toBe(true);
|
||||
}
|
||||
// メールの検証
|
||||
expect(mailSubject).toBe('Forced Email Verification Notification [U-126]');
|
||||
expect(mailText?.includes('admin')).toBe(true);
|
||||
expect(mailHtml?.includes('admin')).toBe(true);
|
||||
expect(_to).toEqual(['user1@example.com']);
|
||||
// ユーザー取得をモック化しているため、値の比較ではなく有り無しで確認
|
||||
expect(mailText?.includes(CUSTOMER_NAME)).toBe(false);
|
||||
expect(mailTextUrl).toBe('http://localhost:8081/');
|
||||
expect(_cc).not.toBeUndefined();
|
||||
expect(mailHtml?.includes(CUSTOMER_NAME)).toBe(false);
|
||||
expect(mailHtmlUrl).toBe('http://localhost:8081/');
|
||||
});
|
||||
it('存在しないユーザは強制認証できない', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
const { account, admin } = await makeTestAccount(source, {
|
||||
tier: 5,
|
||||
});
|
||||
const { external_id } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
getUsers: async () => {
|
||||
return [
|
||||
{
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'admin@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: external_id,
|
||||
displayName: 'user1',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
getUser: async () => {
|
||||
return {
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
overrideSendgridService(service, {});
|
||||
try {
|
||||
await service.confirmUserForce(context, 100);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E009999'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
it('既に認証済みのユーザは強制認証できない', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
|
||||
const { account, admin } = await makeTestAccount(source, {
|
||||
tier: 5,
|
||||
});
|
||||
const { external_id } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
role: USER_ROLES.AUTHOR,
|
||||
// 認証済みユーザー
|
||||
email_verified: true,
|
||||
});
|
||||
|
||||
const service = module.get<UsersService>(UsersService);
|
||||
const context = makeContext(`uuidv4`, 'requestId');
|
||||
|
||||
overrideAdB2cService(service, {
|
||||
getUsers: async () => {
|
||||
return [
|
||||
{
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'admin@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: external_id,
|
||||
displayName: 'user1',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
getUser: async () => {
|
||||
return {
|
||||
id: admin.external_id,
|
||||
displayName: 'admin',
|
||||
identities: [
|
||||
{
|
||||
signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS,
|
||||
issuer: 'issuer',
|
||||
issuerAssignedId: 'user1@example.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
overrideSendgridService(service, {});
|
||||
|
||||
try {
|
||||
await service.confirmUserForce(context, admin.id);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010202'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
} from '../../gateways/adb2c/adb2c.service';
|
||||
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
|
||||
import { SortCriteriaRepositoryService } from '../../repositories/sort_criteria/sort_criteria.repository.service';
|
||||
import { TaskFiltersRepositoryService } from '../../repositories/task_filters/task_filter.repository.service';
|
||||
import {
|
||||
User as EntityUser,
|
||||
newUser,
|
||||
@ -26,6 +27,7 @@ import { LicensesRepositoryService } from '../../repositories/licenses/licenses.
|
||||
import {
|
||||
MultipleImportUser,
|
||||
GetRelationsResponse,
|
||||
GetTaskFiltersResponse,
|
||||
MultipleImportErrors,
|
||||
User,
|
||||
} from './types/types';
|
||||
@ -73,6 +75,7 @@ export class UsersService {
|
||||
private readonly usersRepository: UsersRepositoryService,
|
||||
private readonly licensesRepository: LicensesRepositoryService,
|
||||
private readonly sortCriteriaRepository: SortCriteriaRepositoryService,
|
||||
private readonly taskFiltersRepository: TaskFiltersRepositoryService,
|
||||
private readonly adB2cService: AdB2cService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly sendgridService: SendGridService,
|
||||
@ -603,10 +606,18 @@ export class UsersService {
|
||||
|
||||
/**
|
||||
* Get Users
|
||||
* @param accessToken
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param userInputUserName
|
||||
* @param userInputEmail
|
||||
* @returns users
|
||||
*/
|
||||
async getUsers(context: Context, externalId: string): Promise<User[]> {
|
||||
async getUsers(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
userInputUserName?: string,
|
||||
userInputEmail?: string,
|
||||
): Promise<User[]> {
|
||||
this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`);
|
||||
|
||||
try {
|
||||
@ -617,7 +628,7 @@ export class UsersService {
|
||||
);
|
||||
|
||||
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
|
||||
const externalIds = dbUsers.map((x) => x.external_id);
|
||||
const externalIds = dbUsers.map((user) => user.external_id);
|
||||
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
|
||||
|
||||
// DBから取得した各ユーザーをもとにADB2C情報をマージしライセンス情報を算出
|
||||
@ -703,7 +714,22 @@ export class UsersService {
|
||||
};
|
||||
});
|
||||
|
||||
// 検索条件(ユーザ名とメールアドレス)が入力されていない場合は全ユーザーを返す
|
||||
if (!userInputUserName && !userInputEmail) {
|
||||
return users;
|
||||
}
|
||||
|
||||
// 検索条件が入力されている場合、部分一致するユーザーだけを残す
|
||||
const matchedUsers = users.filter(
|
||||
(user) =>
|
||||
(!userInputUserName ||
|
||||
user.name
|
||||
.toLowerCase()
|
||||
.includes(userInputUserName.toLowerCase())) &&
|
||||
(!userInputEmail ||
|
||||
user.email.toLowerCase().includes(userInputEmail.toLowerCase())),
|
||||
);
|
||||
return matchedUsers;
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
@ -831,6 +857,116 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates task filters
|
||||
* @param authorId
|
||||
* @param fileName
|
||||
* @param token
|
||||
* @returns task filters
|
||||
*/
|
||||
async updateTaskFilter(
|
||||
context: Context,
|
||||
filterConditionAuthorId: string | null,
|
||||
filterConditionFileName: string | null,
|
||||
externalId: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.updateTaskFilter.name
|
||||
} | params: { filterConditionAuthorId: ${filterConditionAuthorId}, filterConditionFileName: ${filterConditionFileName}, externalId: ${externalId} };`,
|
||||
);
|
||||
let user: EntityUser;
|
||||
try {
|
||||
// ユーザー情報を取得
|
||||
user = await this.usersRepository.findUserByExternalId(
|
||||
context,
|
||||
externalId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// ユーザーの検索条件を更新
|
||||
await this.taskFiltersRepository.updateTaskFilter(
|
||||
user.id,
|
||||
filterConditionAuthorId,
|
||||
filterConditionFileName,
|
||||
context,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.updateTaskFilter.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets task filters
|
||||
* @param token
|
||||
* @returns task filters
|
||||
*/
|
||||
async getTaskFilter(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
): Promise<GetTaskFiltersResponse> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${
|
||||
this.getTaskFilter.name
|
||||
} | params: { externalId: ${externalId} };`,
|
||||
);
|
||||
let user: EntityUser;
|
||||
try {
|
||||
// ユーザー情報を取得
|
||||
user = await this.usersRepository.findUserByExternalId(
|
||||
context,
|
||||
externalId,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// ユーザーのタスク検索条件を取得
|
||||
const taskFilters = await this.taskFiltersRepository.getTaskFilter(
|
||||
user.id,
|
||||
context,
|
||||
);
|
||||
const { author_id: authorId, file_name: fileName } = taskFilters;
|
||||
const result = {
|
||||
authorId: authorId ?? undefined,
|
||||
fileName: fileName ?? undefined,
|
||||
};
|
||||
return result;
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.getTaskFilter.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定したユーザーの文字起こし業務に関連する情報を取得します
|
||||
* @param userId
|
||||
@ -1740,6 +1876,99 @@ export class UsersService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ユーザーの強制認証
|
||||
* @param userId ユーザId
|
||||
*/
|
||||
async confirmUserForce(context: Context, userId: number): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.confirmUserForce.name}`,
|
||||
);
|
||||
try {
|
||||
// ユーザーをメール認証済みにする。
|
||||
await this.usersRepository.updateUserVerified(context, userId);
|
||||
|
||||
// 通知先のユーザーを取得
|
||||
const { external_id, account_id } =
|
||||
await this.usersRepository.findUserById(context, userId);
|
||||
|
||||
const adb2cUser = await this.adB2cService.getUser(context, external_id);
|
||||
const { displayName, emailAddress } =
|
||||
getUserNameAndMailAddress(adb2cUser);
|
||||
|
||||
// メールアドレスが無いことはありえないが、プログラム上はあり得るためユーザーが見つからないエラーとして返す。
|
||||
if (!emailAddress) {
|
||||
throw new UserNotFoundError(
|
||||
`emailAddress is null. externalId=${external_id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// プライマリアカウント管理者を取得する
|
||||
const { primary_admin_user_id } =
|
||||
await this.accountsRepository.findAccountById(context, account_id);
|
||||
|
||||
if (primary_admin_user_id === null) {
|
||||
throw new UserNotFoundError(
|
||||
`primary_admin_user_id is null. account_id=${account_id}`,
|
||||
);
|
||||
}
|
||||
const { external_id: primaryUserExtarnalId } =
|
||||
await this.usersRepository.findUserById(context, primary_admin_user_id);
|
||||
|
||||
const primaryAdmimAdb2cUser = await this.adB2cService.getUser(
|
||||
context,
|
||||
primaryUserExtarnalId,
|
||||
);
|
||||
const {
|
||||
emailAddress: primaryAdminMailAdress,
|
||||
} = getUserNameAndMailAddress(primaryAdmimAdb2cUser);
|
||||
|
||||
// メールアドレスが無いことはありえないが、プログラム上はあり得るためユーザーが見つからないエラーとして返す。
|
||||
if (!primaryAdminMailAdress) {
|
||||
throw new UserNotFoundError(
|
||||
`primary admin emailAddress is null. externalId=${primaryUserExtarnalId}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
// アカウント認証が完了した旨をメール送信する
|
||||
await this.sendgridService.sendMailWithU126(
|
||||
context,
|
||||
emailAddress,
|
||||
displayName,
|
||||
primaryAdminMailAdress,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
// メール送信に関する例外はログだけ出して握りつぶす
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case EmailAlreadyVerifiedError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010202'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.confirmUserForce.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* アカウントIDを指定して、アカウント情報と管理者情報を取得する
|
||||
* @param context
|
||||
|
||||
@ -33,6 +33,8 @@ import {
|
||||
NO_ERROR_MESSAGE_EN,
|
||||
NO_ERROR_MESSAGE_DE,
|
||||
NO_ERROR_MESSAGE_FR,
|
||||
ISSUER_CUSTOMER_NAME,
|
||||
EXPIRATION_DATE,
|
||||
} from '../../templates/constants';
|
||||
import { URL } from 'node:url';
|
||||
|
||||
@ -100,6 +102,10 @@ export class SendGridService {
|
||||
private readonly templateU123Text: string;
|
||||
private readonly templateU124Html: string;
|
||||
private readonly templateU124Text: string;
|
||||
private readonly templateU125Html: string;
|
||||
private readonly templateU125Text: string;
|
||||
private readonly templateU126Html: string;
|
||||
private readonly templateU126Text: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.appDomain = this.configService.getOrThrow<string>('APP_DOMAIN');
|
||||
@ -357,6 +363,22 @@ export class SendGridService {
|
||||
path.resolve(__dirname, `../../templates/template_U_124.txt`),
|
||||
'utf-8',
|
||||
);
|
||||
this.templateU125Html = readFileSync(
|
||||
path.resolve(__dirname, `../../templates/template_U_125.html`),
|
||||
'utf-8',
|
||||
);
|
||||
this.templateU125Text = readFileSync(
|
||||
path.resolve(__dirname, `../../templates/template_U_125.txt`),
|
||||
'utf-8',
|
||||
);
|
||||
this.templateU126Html = readFileSync(
|
||||
path.resolve(__dirname, `../../templates/template_U_126.html`),
|
||||
'utf-8',
|
||||
);
|
||||
this.templateU126Text = readFileSync(
|
||||
path.resolve(__dirname, `../../templates/template_U_126.txt`),
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1599,6 +1621,104 @@ export class SendGridService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* U-125のテンプレートを使用したメールを送信する
|
||||
* @param context
|
||||
* @param adminMailaddresses
|
||||
* @param accountName
|
||||
* @param licenseQuantity
|
||||
* @param expirationDay
|
||||
* @param dealerEmails
|
||||
* @returns mail with U125
|
||||
*/
|
||||
async sendMailWithU125(
|
||||
context: Context,
|
||||
adminMailaddresses: string[],
|
||||
accountName: string,
|
||||
licenseQuantity: number,
|
||||
expirationDay: number,
|
||||
dealerEmails: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.sendMailWithU125.name}`,
|
||||
);
|
||||
try {
|
||||
const subject = 'Issued Trial License Notification [U-125]';
|
||||
const url = new URL(this.appDomain).href;
|
||||
|
||||
const html = this.templateU125Html
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(accountName))
|
||||
.replaceAll(LICENSE_QUANTITY, escapeDollar(String(licenseQuantity)))
|
||||
.replaceAll(EXPIRATION_DATE, escapeDollar(String(expirationDay)))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
const text = this.templateU125Text
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(accountName))
|
||||
.replaceAll(LICENSE_QUANTITY, escapeDollar(String(licenseQuantity)))
|
||||
.replaceAll(EXPIRATION_DATE, escapeDollar(String(expirationDay)))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
context,
|
||||
adminMailaddresses,
|
||||
dealerEmails,
|
||||
this.mailFrom,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.sendMailWithU125.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* U-126のテンプレートを使用したメールを送信する
|
||||
* @param context
|
||||
* @param mailaddress
|
||||
* @param userName
|
||||
* @param adminMailaddress
|
||||
* @param adminUserName
|
||||
* @returns mail with U126
|
||||
*/
|
||||
async sendMailWithU126(
|
||||
context: Context,
|
||||
mailaddress: string,
|
||||
userName: string,
|
||||
adminMailaddress: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.getTrackingId()}] ${this.sendMailWithU126.name}`,
|
||||
);
|
||||
try {
|
||||
const subject = 'Forced Email Verification Notification [U-126]';
|
||||
const url = new URL(this.appDomain).href;
|
||||
|
||||
const html = this.templateU126Html
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(userName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
const text = this.templateU126Text
|
||||
.replaceAll(CUSTOMER_NAME, escapeDollar(userName))
|
||||
.replaceAll(TOP_URL, escapeDollar(url));
|
||||
// メールを送信する
|
||||
await this.sendMail(
|
||||
context,
|
||||
[mailaddress],
|
||||
[adminMailaddress],
|
||||
this.mailFrom,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.getTrackingId()}] ${this.sendMailWithU126.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* メールを送信する
|
||||
* @param context
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import assert from 'node:assert';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
Between,
|
||||
@ -9,6 +10,8 @@ import {
|
||||
Not,
|
||||
UpdateResult,
|
||||
EntityManager,
|
||||
FindOptionsWhere,
|
||||
Like,
|
||||
} from 'typeorm';
|
||||
import { User, UserArchive } from '../users/entity/user.entity';
|
||||
import { Account } from './entity/account.entity';
|
||||
@ -21,6 +24,8 @@ import {
|
||||
LicenseOrder,
|
||||
} from '../licenses/entity/license.entity';
|
||||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||||
import { TaskFilters } from '../task_filters/entity/task_filters.entity';
|
||||
import { escapeLikeString } from '../../common/repository/utils/utils';
|
||||
import {
|
||||
getDirection,
|
||||
getTaskListSortableAttribute,
|
||||
@ -66,6 +71,7 @@ import {
|
||||
LicenseSummaryInfo,
|
||||
PartnerInfoFromDb,
|
||||
PartnerLicenseInfoForRepository,
|
||||
SearchPartnerInfoFromDb,
|
||||
} from '../../features/accounts/types/types';
|
||||
import { AccountArchive } from './entity/account_archive.entity';
|
||||
import { JobNumber } from '../job_number/entity/job_number.entity';
|
||||
@ -242,6 +248,21 @@ export class AccountsRepositoryService {
|
||||
context,
|
||||
);
|
||||
|
||||
// ユーザーのタスク検索条件を作成
|
||||
const taskFilters = new TaskFilters();
|
||||
{
|
||||
taskFilters.user_id = persistedUser.id;
|
||||
}
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
const newTaskFilters = taskFiltersRepo.create(taskFilters);
|
||||
await insertEntity(
|
||||
TaskFilters,
|
||||
taskFiltersRepo,
|
||||
newTaskFilters,
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
return { newAccount: persistedAccount, adminUser: persistedUser };
|
||||
});
|
||||
}
|
||||
@ -260,6 +281,7 @@ export class AccountsRepositoryService {
|
||||
const accountsRepo = entityManager.getRepository(Account);
|
||||
const usersRepo = entityManager.getRepository(User);
|
||||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
const jobNumberRepo = entityManager.getRepository(JobNumber);
|
||||
// JobNumberを削除
|
||||
await deleteEntity(
|
||||
@ -275,6 +297,13 @@ export class AccountsRepositoryService {
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
// タスク検索条件を削除
|
||||
await deleteEntity(
|
||||
taskFiltersRepo,
|
||||
{ user_id: userId },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
// プライマリ管理者を削除
|
||||
await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context);
|
||||
// アカウントを削除
|
||||
@ -411,6 +440,35 @@ export class AccountsRepositoryService {
|
||||
return expiringSoonLicense;
|
||||
}
|
||||
|
||||
/**
|
||||
* ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
|
||||
* 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
|
||||
* @param entityManager
|
||||
* @param id
|
||||
* @param currentDate
|
||||
* @returns expiringSoonLicense
|
||||
*/
|
||||
private async getAllocatedLicense(
|
||||
context: Context,
|
||||
entityManager: EntityManager,
|
||||
id: number,
|
||||
currentDate: Date,
|
||||
): Promise<number> {
|
||||
const license = entityManager.getRepository(License);
|
||||
|
||||
// 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
|
||||
const allocatedLicense = await license.count({
|
||||
where: {
|
||||
account_id: id,
|
||||
expiry_date: MoreThanOrEqual(currentDate),
|
||||
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
return allocatedLicense;
|
||||
}
|
||||
|
||||
/**
|
||||
* ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定
|
||||
* 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う
|
||||
@ -497,14 +555,12 @@ export class AccountsRepositoryService {
|
||||
});
|
||||
|
||||
// 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
|
||||
const allocatedLicense = await license.count({
|
||||
where: {
|
||||
account_id: id,
|
||||
expiry_date: MoreThanOrEqual(currentDate),
|
||||
status: LICENSE_ALLOCATED_STATUS.ALLOCATED,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
const allocatedLicense = await this.getAllocatedLicense(
|
||||
context,
|
||||
entityManager,
|
||||
id,
|
||||
currentDate,
|
||||
);
|
||||
|
||||
// 総ライセンス数のうち、ユーザーに割り当てたことがあるが、現在は割り当て解除され誰にも割り当たっていないライセンス数を取得する
|
||||
const reusableLicense = await license.count({
|
||||
@ -749,6 +805,7 @@ export class AccountsRepositoryService {
|
||||
// 第五の不足数を算出するためのライセンス数情報を取得する
|
||||
let expiringSoonLicense: number = 0; // eslint-disable-line
|
||||
let allocatableLicenseWithMargin: number = 0; // eslint-disable-line
|
||||
let allocatedLicense: number | undefined = undefined; // eslint-disable-line
|
||||
if (childAccount.tier === TIERS.TIER5) {
|
||||
expiringSoonLicense = await this.getExpiringSoonLicense(
|
||||
context,
|
||||
@ -764,6 +821,12 @@ export class AccountsRepositoryService {
|
||||
childAccount.id,
|
||||
expiringSoonDate,
|
||||
);
|
||||
allocatedLicense = await this.getAllocatedLicense(
|
||||
context,
|
||||
entityManager,
|
||||
childAccount.id,
|
||||
currentDate,
|
||||
);
|
||||
}
|
||||
|
||||
// 戻り値用の値を設定
|
||||
@ -777,6 +840,7 @@ export class AccountsRepositoryService {
|
||||
issueRequesting: childLicenseOrderStatus.issueRequesting,
|
||||
expiringSoonLicense: expiringSoonLicense,
|
||||
allocatableLicenseWithMargin: allocatableLicenseWithMargin,
|
||||
allocatedLicense: allocatedLicense,
|
||||
};
|
||||
|
||||
childPartnerLicensesFromRepository.push(
|
||||
@ -842,10 +906,15 @@ export class AccountsRepositoryService {
|
||||
if (!account) {
|
||||
break;
|
||||
}
|
||||
if (!account.parent_account_id) {
|
||||
throw new Error("Parent account doesn't exist.");
|
||||
// 第一階層はアカウント階層の最上位であるため、到達したらループを抜ける
|
||||
if (account.tier === TIERS.TIER1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 第二〜第五のアカウントには親アカウントIDがかならず設定されている
|
||||
assert(
|
||||
account.parent_account_id,
|
||||
new Error("Parent account doesn't exist."),
|
||||
);
|
||||
parentAccountIds.push(account.parent_account_id);
|
||||
currentAccountId = account.parent_account_id;
|
||||
}
|
||||
@ -1441,6 +1510,16 @@ export class AccountsRepositoryService {
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
// タスク検索条件のテーブルのレコードを削除する
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
await deleteEntity(
|
||||
taskFiltersRepo,
|
||||
{ user_id: In(users.map((user) => user.id)) },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
return users;
|
||||
});
|
||||
}
|
||||
@ -1657,6 +1736,15 @@ export class AccountsRepositoryService {
|
||||
context,
|
||||
);
|
||||
|
||||
// タスク検索条件のレコードを削除する
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
await deleteEntity(
|
||||
taskFiltersRepo,
|
||||
{ user_id: In(users.map((user) => user.id)) },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
// JobNumberのテーブルのレコードを削除する
|
||||
const jobNumberRepo = entityManager.getRepository(JobNumber);
|
||||
await deleteEntity(
|
||||
@ -1881,4 +1969,209 @@ export class AccountsRepositoryService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 自身のアカウントに関連するアカウントを取得する
|
||||
* @param context
|
||||
* @param ownAccountId
|
||||
* @param ownAccountTier
|
||||
* @param primaryAdminUserId
|
||||
* @param companyName
|
||||
* @param targetAccountId
|
||||
* @returns partner info
|
||||
*/
|
||||
async getAccountsRelatedOwnAccount(
|
||||
context: Context,
|
||||
ownAccountId: number,
|
||||
ownAccountTier: number,
|
||||
companyName?: string,
|
||||
targetAccountId?: number,
|
||||
): Promise<SearchPartnerInfoFromDb[]> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const whereClause: FindOptionsWhere<Account> = {
|
||||
// 共通の条件として、自分の階層より下に限定する
|
||||
tier: MoreThan(ownAccountTier)
|
||||
};
|
||||
|
||||
// 検索条件に入力があればAND条件で追加
|
||||
if (targetAccountId) {
|
||||
whereClause.id = targetAccountId;
|
||||
}
|
||||
if (companyName) {
|
||||
whereClause.company_name = Like(`%${escapeLikeString(companyName)}%`);
|
||||
}
|
||||
|
||||
const accountsRepository = entityManager.getRepository(Account);
|
||||
const filterdAccounts = await accountsRepository.find({
|
||||
where: whereClause,
|
||||
order: {
|
||||
tier: 'DESC',
|
||||
company_name: 'ASC'
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
// 親アカウントIDが無いアカウントは検索結果から弾く
|
||||
const parentAccountIds = filterdAccounts.filter(
|
||||
(
|
||||
account,
|
||||
): account is NonNullable<
|
||||
typeof account & { parent_account_id: number }
|
||||
> => account.parent_account_id !== null,
|
||||
);
|
||||
|
||||
// 親子関係にあるアカウントのみを検索対象にする
|
||||
// 階層構造を辿っていく回数を少なくするため、親子関係有り無しのキャッシュセットを作成しておく
|
||||
let relatedAccountCache: Set<number> = new Set();
|
||||
let notRelatedAccountCache: Set<number> = new Set();
|
||||
|
||||
// 親子関係にあるアカウントかどうかをチェックする
|
||||
const relatedAccounts = await Promise.all(
|
||||
parentAccountIds.map((account) => {
|
||||
return this.checkRelatedAccount(
|
||||
context,
|
||||
ownAccountId,
|
||||
ownAccountTier,
|
||||
account,
|
||||
account.parent_account_id,
|
||||
account.tier,
|
||||
relatedAccountCache,
|
||||
notRelatedAccountCache,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// アカウント階層構造のキャッシュをクリアする。
|
||||
// nullに書き換えるために強制的にunkown型に変換。
|
||||
relatedAccountCache.clear();
|
||||
(relatedAccountCache as unknown) = null;
|
||||
notRelatedAccountCache.clear();
|
||||
(notRelatedAccountCache as unknown) = null;
|
||||
|
||||
// 親子関係ではないアカウントを取り除き、階層順でソート
|
||||
const relatedAccountsNonNull = relatedAccounts
|
||||
.filter(
|
||||
(account): account is NonNullable<typeof account> => account !== null,
|
||||
)
|
||||
.sort((a, b) => a.tier - b.tier);
|
||||
|
||||
// 検索できていなければ、空の検索結果を返す
|
||||
if (relatedAccountsNonNull.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// ADB2Cから情報を取得するための外部ユーザIDを取得する(念のためプライマリ管理者IDが存在しない場合を考慮)
|
||||
const primaryUserIds = relatedAccountsNonNull.flatMap((x) => {
|
||||
if (x.primary_admin_user_id) {
|
||||
return [x.primary_admin_user_id];
|
||||
} else if (x.secondary_admin_user_id) {
|
||||
return [x.secondary_admin_user_id];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const primaryUsers = await userRepo.find({
|
||||
where: {
|
||||
id: In(primaryUserIds),
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
// アカウント情報とプライマリ管理者の外部ユーザIDをマージ
|
||||
const partners = relatedAccountsNonNull.map((account) => {
|
||||
const primaryUser = primaryUsers.find(
|
||||
(user) =>
|
||||
// Raw SQLで取得できた管理者ユーザーIDは文字列になっているので、厳密等価で比較しない
|
||||
user.id == account.primary_admin_user_id ||
|
||||
user.id == account.secondary_admin_user_id,
|
||||
);
|
||||
if (!primaryUser) {
|
||||
throw new AdminUserNotFoundError(
|
||||
`Primary admin user is not found. id: ${account.primary_admin_user_id}, account_id: ${account.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
name: account.company_name,
|
||||
tier: account.tier,
|
||||
accountId: account.id,
|
||||
country: account.country,
|
||||
primaryAccountExternalId: primaryUser.external_id,
|
||||
};
|
||||
});
|
||||
|
||||
return partners;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* targetAccountが自身のアカウントと親子関係になっているかをチェックし、親子間家になっていればtargetAccountを返す
|
||||
* @param context
|
||||
* @param ownAccountId
|
||||
* @param ownTier
|
||||
* @param targetAccount
|
||||
* @param parentAccountId
|
||||
* @param tier
|
||||
* @param relatedAccountCache
|
||||
* @param notRelatedAccountCache
|
||||
* @returns
|
||||
*/
|
||||
private async checkRelatedAccount(
|
||||
context: Context,
|
||||
ownAccountId: number,
|
||||
ownTier: number,
|
||||
targetAccount: Account,
|
||||
parentAccountId: number,
|
||||
tier: number,
|
||||
relatedAccountCache: Set<number>,
|
||||
notRelatedAccountCache: Set<number>,
|
||||
): Promise<Account | null> {
|
||||
// 親子関係のアカウントの場合、DBにアクセスせずに検索結果に含める
|
||||
if (Array.from(relatedAccountCache).includes(parentAccountId)) {
|
||||
return targetAccount;
|
||||
}
|
||||
|
||||
// 親子関係のアカウントではない場合、DBにアクセスせずに検索結果から除外する
|
||||
if (Array.from(notRelatedAccountCache).includes(parentAccountId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parentAccount = await this.getOneUpperTierAccount(
|
||||
context,
|
||||
parentAccountId,
|
||||
tier,
|
||||
);
|
||||
|
||||
// 通常ありえないが、親を持たないアカウントの場合は検索対象としない。
|
||||
if (!parentAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parentAccount.tier !== ownTier) {
|
||||
// 検索者の階層と一致しない場合は、上位の階層を検索する。
|
||||
return this.checkRelatedAccount(
|
||||
context,
|
||||
ownAccountId,
|
||||
ownTier,
|
||||
targetAccount,
|
||||
parentAccount.parent_account_id!,
|
||||
parentAccount.tier,
|
||||
relatedAccountCache,
|
||||
notRelatedAccountCache,
|
||||
);
|
||||
}
|
||||
|
||||
if (parentAccount.id !== ownAccountId) {
|
||||
// 検索者の階層と一致しているかつ、アカウントIDが一致しない場合は、検索対象としない。
|
||||
// 親子関係ではないリストに情報をキャッシュ
|
||||
notRelatedAccountCache.add(parentAccount.id);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 検索者の階層と一致しているかつ、アカウントIDが一致する場合は、検索対象に含める。
|
||||
// 親子関係リストに情報をキャッシュ
|
||||
relatedAccountCache.add(parentAccount.id);
|
||||
return targetAccount;
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +35,9 @@ export class LicenseOrder {
|
||||
@Column({ nullable: true, type: 'datetime' })
|
||||
issued_at: Date | null;
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column()
|
||||
quantity: number;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource, In, IsNull, MoreThanOrEqual, Not } from 'typeorm';
|
||||
import { DataSource, In, MoreThanOrEqual } from 'typeorm';
|
||||
import {
|
||||
LicenseOrder,
|
||||
License,
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from './entity/license.entity';
|
||||
import {
|
||||
CARD_LICENSE_LENGTH,
|
||||
ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY,
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
LICENSE_ISSUE_STATUS,
|
||||
LICENSE_TYPE,
|
||||
@ -32,6 +33,7 @@ import {
|
||||
import {
|
||||
AllocatableLicenseInfo,
|
||||
DateWithZeroTime,
|
||||
NewTrialLicenseExpirationDate,
|
||||
} from '../../features/licenses/types/types';
|
||||
import { NewAllocatedLicenseExpirationDate } from '../../features/licenses/types/types';
|
||||
import {
|
||||
@ -514,6 +516,66 @@ export class LicensesRepositoryService {
|
||||
return { issuedOrderId: issuingOrder.id };
|
||||
});
|
||||
}
|
||||
/**
|
||||
* トライアルライセンスを発行する。ライセンスに紐づくライセンス注文も作成する。
|
||||
* @context Context
|
||||
* @param issuedAccountId
|
||||
* @param toAccountId
|
||||
* @param nowDate
|
||||
*/
|
||||
async issueTrialLicense(
|
||||
context: Context,
|
||||
issuedAccountId: number,
|
||||
toAccountId: number,
|
||||
nowDate: Date,
|
||||
): Promise<void> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
// 注文のレコードを作成する
|
||||
const licenseOrderRepo = entityManager.getRepository(LicenseOrder);
|
||||
const trialLicenseOrder = new LicenseOrder();
|
||||
// PONumberは設定しないので初期値を使用する
|
||||
trialLicenseOrder.from_account_id = issuedAccountId;
|
||||
trialLicenseOrder.to_account_id = toAccountId;
|
||||
trialLicenseOrder.quantity = ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY;
|
||||
trialLicenseOrder.status = LICENSE_ISSUE_STATUS.ISSUED;
|
||||
trialLicenseOrder.ordered_at = nowDate;
|
||||
trialLicenseOrder.issued_at = nowDate;
|
||||
trialLicenseOrder.type = LICENSE_TYPE.TRIAL;
|
||||
await insertEntity(
|
||||
LicenseOrder,
|
||||
licenseOrderRepo,
|
||||
trialLicenseOrder,
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
// トライアルライセンスを発行する。
|
||||
const licenseRepo = entityManager.getRepository(License);
|
||||
// トライアルライセンスの有効期限は今日を起算日として30日後の日付が変わるまで
|
||||
const expiryDate = new NewTrialLicenseExpirationDate(nowDate);
|
||||
// ライセンステーブルのレコードを作成する
|
||||
const newTrialLicenses = Array.from(
|
||||
{ length: trialLicenseOrder.quantity },
|
||||
() => {
|
||||
const license = new License();
|
||||
license.expiry_date = expiryDate;
|
||||
license.account_id = issuedAccountId;
|
||||
license.type = LICENSE_TYPE.TRIAL;
|
||||
license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
|
||||
license.order_id = trialLicenseOrder.id;
|
||||
return license;
|
||||
},
|
||||
);
|
||||
|
||||
// ライセンステーブルを登録(注文元)
|
||||
await insertEntities(
|
||||
License,
|
||||
licenseRepo,
|
||||
newTrialLicenses,
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 対象のアカウントの割り当て可能なライセンスを取得する
|
||||
* @context Context
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'task_filters' })
|
||||
export class TaskFilters {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
user_id: number;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
author_id: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
file_name: string | null;
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TaskFiltersRepositoryService } from './task_filter.repository.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TaskFilters } from './entity/task_filters.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([TaskFilters])],
|
||||
providers: [TaskFiltersRepositoryService],
|
||||
exports: [TaskFiltersRepositoryService],
|
||||
})
|
||||
export class TaskFiltersRepositoryModule {}
|
||||
@ -0,0 +1,129 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
import { TaskFilters } from './entity/task_filters.entity';
|
||||
import { insertEntity, updateEntity } from '../../common/repository';
|
||||
import { Context } from '../../common/log';
|
||||
|
||||
@Injectable()
|
||||
export class TaskFiltersRepositoryService {
|
||||
// クエリログにコメントを出力するかどうか
|
||||
private readonly isCommentOut = process.env.STAGE !== 'local';
|
||||
|
||||
constructor(private dataSource: DataSource) {}
|
||||
private readonly logger = new Logger(TaskFiltersRepositoryService.name);
|
||||
|
||||
/**
|
||||
* Create task filter
|
||||
* @param userId
|
||||
* @param authorId
|
||||
* @param fileName
|
||||
* @param context
|
||||
* @returns task filter
|
||||
*/
|
||||
async createTaskFilter(
|
||||
userId: number,
|
||||
authorId: string | null,
|
||||
fileName: string | null,
|
||||
context: Context,
|
||||
): Promise<TaskFilters> {
|
||||
this.logger.log(` ${this.createTaskFilter.name}; userId:${userId}`);
|
||||
|
||||
// 新しいTaskFilterエンティティの作成
|
||||
const taskFilter = new TaskFilters();
|
||||
taskFilter.user_id = userId;
|
||||
|
||||
// authorIdとfileNameがスペースだけの場合はNULLを設定
|
||||
taskFilter.author_id = authorId && authorId.trim() !== '' ? authorId : null;
|
||||
taskFilter.file_name = fileName && fileName.trim() !== '' ? fileName : null;
|
||||
|
||||
// リポジトリの取得
|
||||
const repo = this.dataSource.getRepository(TaskFilters);
|
||||
|
||||
// エンティティの挿入
|
||||
const createdTaskFilter = await insertEntity(
|
||||
TaskFilters,
|
||||
repo,
|
||||
taskFilter,
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
return createdTaskFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates task filter
|
||||
* @param userId
|
||||
* @param authorId
|
||||
* @param fileName
|
||||
* @returns task filter
|
||||
*/
|
||||
async updateTaskFilter(
|
||||
userId: number,
|
||||
authorId: string | null,
|
||||
fileName: string | null,
|
||||
context: Context,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
` ${this.updateTaskFilter.name}; author_id:${authorId}, file_name:${fileName}`,
|
||||
);
|
||||
try {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const repo = entityManager.getRepository(TaskFilters);
|
||||
const targetTaskFilter = await repo.findOne({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
// 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!targetTaskFilter) {
|
||||
throw new Error('task filter not found.');
|
||||
}
|
||||
|
||||
// authorIdとfileNameがスペースだけの場合はNULLを設定
|
||||
targetTaskFilter.author_id = authorId && authorId.trim() !== '' ? authorId : null;
|
||||
targetTaskFilter.file_name = fileName && fileName.trim() !== '' ? fileName : null;
|
||||
|
||||
await updateEntity(
|
||||
repo,
|
||||
{ id: targetTaskFilter.id },
|
||||
targetTaskFilter,
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to update task filter for userId: ${userId}`,
|
||||
error.stack,
|
||||
);
|
||||
throw new Error('Failed to update task filter');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets task filter
|
||||
* @param userId
|
||||
* @returns task filter
|
||||
*/
|
||||
async getTaskFilter(userId: number, context: Context): Promise<TaskFilters> {
|
||||
this.logger.log(` ${this.getTaskFilter.name}; userId:${userId}`);
|
||||
|
||||
const repo = this.dataSource.getRepository(TaskFilters);
|
||||
const taskFilter = await repo.findOne({
|
||||
where: {
|
||||
user_id: userId,
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
// 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!taskFilter) {
|
||||
throw new Error('Failed to get task filters.');
|
||||
}
|
||||
|
||||
return taskFilter;
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import {
|
||||
FindOptionsOrderValue,
|
||||
In,
|
||||
IsNull,
|
||||
Like,
|
||||
Not,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
@ -21,6 +22,7 @@ import { AudioOptionItem as ParamOptionItem } from '../../features/files/types/t
|
||||
import { AudioFile } from '../audio_files/entity/audio_file.entity';
|
||||
import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity';
|
||||
import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity';
|
||||
import { escapeLikeString } from '../../common/repository/utils/utils';
|
||||
import {
|
||||
SortDirection,
|
||||
TaskListSortableAttribute,
|
||||
@ -45,6 +47,7 @@ import {
|
||||
import { Roles } from '../../common/types/role';
|
||||
import { TaskStatus, isTaskStatus } from '../../common/types/taskStatus';
|
||||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||||
import { TaskFilters } from '../task_filters/entity/task_filters.entity';
|
||||
import { Workflow } from '../workflows/entity/workflow.entity';
|
||||
import { Worktype } from '../worktypes/entity/worktype.entity';
|
||||
import {
|
||||
@ -629,6 +632,8 @@ export class TasksRepositoryService {
|
||||
* @param sort_criteria
|
||||
* @param direction
|
||||
* @param status
|
||||
* @param user_input_author_id
|
||||
* @param user_input_file_name
|
||||
* @returns tasks: タスク情報 / permissions:タスクに紐づくチェックアウト権限情報 / count: offset|limitを行わなかった場合の該当タスクの合計
|
||||
*/
|
||||
async getTasksFromAccountId(
|
||||
@ -639,6 +644,8 @@ export class TasksRepositoryService {
|
||||
sort_criteria: TaskListSortableAttribute,
|
||||
direction: SortDirection,
|
||||
status: string[],
|
||||
user_input_author_id?: string | null,
|
||||
user_input_file_name?: string | null,
|
||||
): Promise<{
|
||||
tasks: Task[];
|
||||
permissions: CheckoutPermission[];
|
||||
@ -654,6 +661,14 @@ export class TasksRepositoryService {
|
||||
where: {
|
||||
account_id: account_id,
|
||||
status: In(status),
|
||||
file: {
|
||||
author_id: user_input_author_id
|
||||
? Like(`%${escapeLikeString(user_input_author_id)}%`)
|
||||
: undefined,
|
||||
file_name: user_input_file_name
|
||||
? Like(`%${escapeLikeString(user_input_file_name)}%`)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
@ -668,6 +683,14 @@ export class TasksRepositoryService {
|
||||
where: {
|
||||
account_id: account_id,
|
||||
status: In(status),
|
||||
file: {
|
||||
author_id: user_input_author_id
|
||||
? Like(`%${escapeLikeString(user_input_author_id)}%`)
|
||||
: undefined,
|
||||
file_name: user_input_file_name
|
||||
? Like(`%${escapeLikeString(user_input_file_name)}%`)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
order: order, // 引数によってOrderに使用するパラメータを変更
|
||||
take: limit,
|
||||
@ -703,6 +726,8 @@ export class TasksRepositoryService {
|
||||
* @param sort_criteria
|
||||
* @param direction
|
||||
* @param status
|
||||
* @param user_input_author_id
|
||||
* @param user_input_file_name
|
||||
* @returns tasks: タスク情報 / permissions:タスクに紐づくチェックアウト権限情報 / count: offset|limitを行わなかった場合の該当タスクの合計
|
||||
*/
|
||||
async getTasksFromAuthorIdAndAccountId(
|
||||
@ -714,6 +739,8 @@ export class TasksRepositoryService {
|
||||
sort_criteria: TaskListSortableAttribute,
|
||||
direction: SortDirection,
|
||||
status: string[],
|
||||
user_input_author_id?: string | null,
|
||||
user_input_file_name?: string | null,
|
||||
): Promise<{
|
||||
tasks: Task[];
|
||||
permissions: CheckoutPermission[];
|
||||
@ -722,31 +749,75 @@ export class TasksRepositoryService {
|
||||
const order = makeOrder(sort_criteria, direction);
|
||||
const value = await this.dataSource.transaction(async (entityManager) => {
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
const count = await taskRepo.count({
|
||||
where: {
|
||||
account_id: account_id,
|
||||
status: In(status),
|
||||
file: { author_id: author_id },
|
||||
},
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
const countQueryBuilder = await taskRepo
|
||||
.createQueryBuilder('task')
|
||||
.innerJoin('task.file', 'file')
|
||||
.where('task.account_id = :accountId', { accountId: account_id })
|
||||
.andWhere('task.status IN (:...status)', { status })
|
||||
.andWhere('file.author_id = :authorId', { authorId: author_id });
|
||||
|
||||
// AUTHOR ID条件の設定
|
||||
if (user_input_author_id) {
|
||||
countQueryBuilder.andWhere('file.author_id LIKE :userInputAuthorId', {
|
||||
userInputAuthorId: `%${escapeLikeString(user_input_author_id)}%`,
|
||||
});
|
||||
}
|
||||
|
||||
// ファイル名の条件を設定
|
||||
if (user_input_file_name) {
|
||||
countQueryBuilder.andWhere('file.file_name LIKE :userInputFileName', {
|
||||
userInputFileName: `%${escapeLikeString(user_input_file_name)}%`,
|
||||
});
|
||||
}
|
||||
|
||||
countQueryBuilder.comment(
|
||||
`${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
);
|
||||
const count = await countQueryBuilder.getCount();
|
||||
|
||||
const tasksQueryBuilder = taskRepo
|
||||
.createQueryBuilder('task')
|
||||
.leftJoinAndSelect('task.file', 'file')
|
||||
.leftJoinAndSelect('task.option_items', 'option_items')
|
||||
.leftJoinAndSelect('task.typist_user', 'typist_user')
|
||||
.where('task.account_id = :accountId', { accountId: account_id })
|
||||
.andWhere('task.status IN (:...status)', { status })
|
||||
.andWhere('file.author_id = :authorId', { authorId: author_id })
|
||||
.comment(`${context.getTrackingId()}_${new Date().toUTCString()}`);
|
||||
|
||||
// AUTHOR ID条件の設定
|
||||
if (user_input_author_id) {
|
||||
tasksQueryBuilder.andWhere('file.author_id LIKE :userInputAuthorId', {
|
||||
userInputAuthorId: `%${escapeLikeString(user_input_author_id)}%`,
|
||||
});
|
||||
}
|
||||
|
||||
// ファイル名の条件を設定
|
||||
if (user_input_file_name) {
|
||||
tasksQueryBuilder.andWhere('file.file_name LIKE :userInputFileName', {
|
||||
userInputFileName: `%${escapeLikeString(user_input_file_name)}%`,
|
||||
});
|
||||
}
|
||||
|
||||
// ソート条件を追加
|
||||
Object.entries(order).forEach(([key, value]) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const relationKey = key;
|
||||
Object.entries(value).forEach(([key, value]) => {
|
||||
tasksQueryBuilder.addOrderBy(
|
||||
`${relationKey}.${key}`,
|
||||
value as 'ASC' | 'DESC',
|
||||
);
|
||||
});
|
||||
} else {
|
||||
tasksQueryBuilder.addOrderBy(`task.${key}`, value as 'ASC' | 'DESC');
|
||||
}
|
||||
});
|
||||
|
||||
const tasks = await taskRepo.find({
|
||||
relations: {
|
||||
file: true,
|
||||
option_items: true,
|
||||
typist_user: true,
|
||||
},
|
||||
where: {
|
||||
account_id: account_id,
|
||||
status: In(status),
|
||||
file: { author_id: author_id },
|
||||
},
|
||||
order: order, // 引数によってOrderに使用するパラメータを変更
|
||||
take: limit,
|
||||
skip: offset,
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
tasksQueryBuilder.comment(
|
||||
`${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
);
|
||||
const tasks = await tasksQueryBuilder.take(limit).skip(offset).getMany();
|
||||
|
||||
const checkoutRepo = entityManager.getRepository(CheckoutPermission);
|
||||
const taskIds = tasks.map((x) => x.id);
|
||||
@ -773,6 +844,8 @@ export class TasksRepositoryService {
|
||||
sort_criteria: TaskListSortableAttribute,
|
||||
direction: SortDirection,
|
||||
status: string[],
|
||||
user_input_author_id?: string | null,
|
||||
user_input_file_name?: string | null,
|
||||
): Promise<{
|
||||
tasks: Task[];
|
||||
permissions: CheckoutPermission[];
|
||||
@ -837,11 +910,27 @@ export class TasksRepositoryService {
|
||||
external_id: external_user_id,
|
||||
},
|
||||
status: In(status),
|
||||
file: {
|
||||
author_id: user_input_author_id
|
||||
? Like(`%${escapeLikeString(user_input_author_id)}%`)
|
||||
: undefined,
|
||||
file_name: user_input_file_name
|
||||
? Like(`%${escapeLikeString(user_input_file_name)}%`)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
// TypistまたはTypistが所属するユーザーグループが割り当て可能になっているTaskを取得
|
||||
id: In(relatedTaskIds),
|
||||
status: In(status),
|
||||
file: {
|
||||
author_id: user_input_author_id
|
||||
? Like(`%${escapeLikeString(user_input_author_id)}%`)
|
||||
: undefined,
|
||||
file_name: user_input_file_name
|
||||
? Like(`%${escapeLikeString(user_input_file_name)}%`)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
@ -861,11 +950,27 @@ export class TasksRepositoryService {
|
||||
external_id: external_user_id,
|
||||
},
|
||||
status: In(status),
|
||||
file: {
|
||||
author_id: user_input_author_id
|
||||
? Like(`%${escapeLikeString(user_input_author_id)}%`)
|
||||
: undefined,
|
||||
file_name: user_input_file_name
|
||||
? Like(`%${escapeLikeString(user_input_file_name)}%`)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
// TypistまたはTypistが所属するユーザーグループが割り当て可能になっているTaskを取得
|
||||
id: In(relatedTaskIds),
|
||||
status: In(status),
|
||||
file: {
|
||||
author_id: user_input_author_id
|
||||
? Like(`%${escapeLikeString(user_input_author_id)}%`)
|
||||
: undefined,
|
||||
file_name: user_input_file_name
|
||||
? Like(`%${escapeLikeString(user_input_file_name)}%`)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
order: order, // 引数によってOrderに使用するパラメータを変更
|
||||
@ -1177,7 +1282,9 @@ export class TasksRepositoryService {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
const sortRepo = entityManager.getRepository(SortCriteria);
|
||||
const taskfileterRepo = entityManager.getRepository(TaskFilters);
|
||||
|
||||
// ユーザーのソート条件を取得する。
|
||||
const sort = await sortRepo.findOne({
|
||||
where: { user_id: userId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
@ -1188,6 +1295,17 @@ export class TasksRepositoryService {
|
||||
throw new Error(`sort criteria not found. userId: ${userId}`);
|
||||
}
|
||||
|
||||
// ユーザーのタスク検索条件を取得する。
|
||||
const taskFilter = await taskfileterRepo.findOne({
|
||||
where: { user_id: userId },
|
||||
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
|
||||
});
|
||||
|
||||
// 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!taskFilter) {
|
||||
throw new Error(`task filter not found. userId: ${userId}`);
|
||||
}
|
||||
|
||||
const { direction, parameter } = sort;
|
||||
//型チェック
|
||||
if (
|
||||
@ -1199,6 +1317,14 @@ export class TasksRepositoryService {
|
||||
);
|
||||
}
|
||||
|
||||
const filterConditionAuthorId = taskFilter.author_id
|
||||
? taskFilter.author_id.trimStart()
|
||||
: undefined;
|
||||
|
||||
const filterConditionFileName = taskFilter.file_name
|
||||
? taskFilter.file_name.trimStart()
|
||||
: undefined;
|
||||
|
||||
// 指定した音声ファイルIDのタスクを取得
|
||||
const targetTask = await taskRepo.findOne({
|
||||
where: {
|
||||
@ -1247,6 +1373,9 @@ export class TasksRepositoryService {
|
||||
|
||||
// 引数の音声ファイルIDで指定したタスクとユーザが着手可能なタスクの一覧を取得
|
||||
const tasks = await taskRepo.find({
|
||||
relations: {
|
||||
file: true,
|
||||
},
|
||||
where: [
|
||||
{
|
||||
account_id: accountId,
|
||||
@ -1257,6 +1386,14 @@ export class TasksRepositoryService {
|
||||
status: In([TASK_STATUS.UPLOADED, TASK_STATUS.PENDING]),
|
||||
// TypistまたはTypistが所属するユーザーグループが割り当て可能になっているTaskを取得
|
||||
id: In(relatedTaskIds),
|
||||
file: {
|
||||
author_id: filterConditionAuthorId
|
||||
? Like(`%${escapeLikeString(filterConditionAuthorId)}%`)
|
||||
: undefined,
|
||||
file_name: filterConditionFileName
|
||||
? Like(`%${escapeLikeString(filterConditionFileName)}%`)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
order: order,
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
UpdateResult,
|
||||
} from 'typeorm';
|
||||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||||
import { TaskFilters } from '../task_filters/entity/task_filters.entity';
|
||||
import {
|
||||
getDirection,
|
||||
getTaskListSortableAttribute,
|
||||
@ -135,6 +136,21 @@ export class UsersRepositoryService {
|
||||
context,
|
||||
);
|
||||
|
||||
// ユーザーのタスク検索条件を作成
|
||||
const taskFilters = new TaskFilters();
|
||||
{
|
||||
taskFilters.user_id = persisted.id;
|
||||
}
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
const newTaskFilters = taskFiltersRepo.create(taskFilters);
|
||||
await insertEntity(
|
||||
TaskFilters,
|
||||
taskFiltersRepo,
|
||||
newTaskFilters,
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
return persisted;
|
||||
},
|
||||
);
|
||||
@ -836,6 +852,7 @@ export class UsersRepositoryService {
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
// 期限切れライセンスが割り当てられていた場合、ユーザーを削除する前にライセンスを割り当て解除する
|
||||
// ※この処理時点で有効期限外ライセンスであることは確定であるため、期限切れ判定をここでは行わない
|
||||
if (allocatedLicense != null) {
|
||||
@ -869,6 +886,7 @@ export class UsersRepositoryService {
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
// ユーザテーブルのレコードを削除する
|
||||
await deleteEntity(
|
||||
userRepo,
|
||||
@ -876,6 +894,7 @@ export class UsersRepositoryService {
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
// ソート条件のテーブルのレコードを削除する
|
||||
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
|
||||
await deleteEntity(
|
||||
@ -884,6 +903,16 @@ export class UsersRepositoryService {
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
// タスク検索条件テーブルのレコードを削除する
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
await deleteEntity(
|
||||
taskFiltersRepo,
|
||||
{ user_id: target.id },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
return { isSuccess: true };
|
||||
});
|
||||
}
|
||||
@ -904,6 +933,16 @@ export class UsersRepositoryService {
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
// タスク検索条件テーブルのレコードを削除する
|
||||
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
|
||||
await deleteEntity(
|
||||
taskFiltersRepo,
|
||||
{ user_id: userId },
|
||||
this.isCommentOut,
|
||||
context,
|
||||
);
|
||||
|
||||
// プライマリ管理者を削除
|
||||
await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context);
|
||||
});
|
||||
|
||||
@ -12,6 +12,9 @@ export const FILE_NAME = '$FILE_NAME$';
|
||||
export const TYPIST_NAME = '$TYPIST_NAME$';
|
||||
export const TEMPORARY_PASSWORD = '$TEMPORARY_PASSWORD$';
|
||||
export const REQUEST_TIME = '$REQUEST_TIME$';
|
||||
export const ISSUER_CUSTOMER_NAME = '$ISSUER_CUSTOMER_NAME$';
|
||||
export const EXPIRATION_DATE = '$EXPIRATION_DATE$';
|
||||
|
||||
// 言語ごとに変更
|
||||
export const EMAIL_DUPLICATION_EN = `$EMAIL_DUPLICATION_EN$`;
|
||||
export const AUTHOR_ID_DUPLICATION_EN = `$AUTHOR_ID_DUPLICATION_EN$`;
|
||||
|
||||
108
dictation_server/src/templates/template_U_125.html
Normal file
108
dictation_server/src/templates/template_U_125.html
Normal file
@ -0,0 +1,108 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Issued Trial License Notification [U-125]</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h3><English></h3>
|
||||
<p>Dear $CUSTOMER_NAME$,</p>
|
||||
<p>
|
||||
Thank you for choosing ODMS Cloud.
|
||||
</p>
|
||||
<p>
|
||||
We have granted [$LICENSE_QUANTITY$] trial licenses to your account which is valid for
|
||||
$EXPIRATION_DATE$ days. During the trial, you can try all the features of ODMS Cloud.
|
||||
</p>
|
||||
<p>
|
||||
If you wish to continue using ODMS Cloud after the trial period has expired, please contact an authorized OM
|
||||
SYSTEM audio dealer to purchase annual licenses. Various settings including dealer selection can be configured within
|
||||
ODMS Cloud under the Account tab.
|
||||
</p>
|
||||
<p>
|
||||
Please log in to ODMS Cloud to configure your user setting and and verify the license expiration date.<br />
|
||||
URL: $TOP_URL$
|
||||
</p>
|
||||
<p>
|
||||
After you have selected a dealer, to request the number of licenses please select Subscription tab. Licenses
|
||||
issued by dealers will be stored in your license Inventory.
|
||||
</p>
|
||||
<p>
|
||||
If you need assistance with ODMS Cloud, please contact your selected approved OM SYSTEM audio dealer directly.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have received this e-mail in error, please delete this e-mail from your system.<br />
|
||||
This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3><Deutsch></h3>
|
||||
<p>Sehr geehrte(r) $CUSTOMER_NAME$,</p>
|
||||
<p>
|
||||
Vielen Dank, dass Sie sich für ODMS Cloud entschieden haben.
|
||||
</p>
|
||||
<p>
|
||||
Wir haben Ihrem Konto [$LICENSE_QUANTITY$] Testlizenzen hinzugefügt, die $EXPIRATION_DATE$ Tage gültig sind.
|
||||
Während der Testversion können Sie alle Funktionen von ODMS Cloud ausprobieren.
|
||||
</p>
|
||||
<p>
|
||||
Wenn Sie ODMS Cloud nach Ablauf des Testzeitraums weiterhin nutzen möchten, wenden Sie sich bitte an einen
|
||||
autorisierten OM SYSTEM-Audiohändler, um Jahreslizenzen zu erwerben. Verschiedene Einstellungen, einschließlich der
|
||||
Händlerauswahl, können in der ODMS Cloud auf der Registerkarte „Konto“ konfiguriert werden.
|
||||
</p>
|
||||
<p>
|
||||
Bitte melden Sie sich bei ODMS Cloud an, um Ihre Benutzereinstellungen zu konfigurieren und das Ablaufdatum der
|
||||
Lizenz zu überprüfen.<br />
|
||||
URL: $TOP_URL$
|
||||
</p>
|
||||
<p>
|
||||
Nachdem Sie einen Händler ausgewählt haben, wählen Sie bitte die Registerkarte „Abonnement“ aus, um die Anzahl der
|
||||
Lizenzen anzufordern. Von Händlern ausgestellte Lizenzen werden in Ihrem Lizenzbestand gespeichert.
|
||||
</p>
|
||||
<p>
|
||||
Wenn Sie Hilfe mit ODMS Cloud benötigen, wenden Sie sich bitte direkt an Ihren ausgewählten zugelassenen OM
|
||||
SYSTEM-Audiohändler.
|
||||
</p>
|
||||
<p>
|
||||
Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.<br />
|
||||
Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3><Français></h3>
|
||||
<p>Chère/Cher $CUSTOMER_NAME$,</p>
|
||||
<p>
|
||||
Merci d'avoir choisi ODMS Cloud.
|
||||
</p>
|
||||
<p>
|
||||
Nous avons accordé [$LICENSE_QUANTITY$] licences d'essai à votre compte, valables $EXPIRATION_DATE$
|
||||
jours. Pendant la période d'essai, vous pouvez essayer toutes les fonctionnalités d'ODMS Cloud.
|
||||
</p>
|
||||
<p>
|
||||
Si vous souhaitez continuer à utiliser ODMS Cloud après l'expiration de la période d'essai, veuillez contacter un
|
||||
concessionnaire audio agréé OM SYSTEM pour acheter des licences annuelles. Divers paramètres, y compris la
|
||||
sélection du concessionnaire, peuvent être configurés dans ODMS Cloud sous l'onglet Compte.
|
||||
</p>
|
||||
<p>
|
||||
Veuillez vous connecter à ODMS Cloud pour configurer vos paramètres utilisateur et vérifier la date d'expiration
|
||||
de la licence.<br />
|
||||
URL: $TOP_URL$
|
||||
</p>
|
||||
<p>
|
||||
Après avoir sélectionné un concessionnaire, pour demander le nombre de licences, veuillez sélectionner l'onglet
|
||||
Abonnement. Les licences délivrées par les concessionnaires seront stockées dans votre inventaire de licences.
|
||||
</p>
|
||||
<p>
|
||||
Si vous avez besoin d'aide avec ODMS Cloud, veuillez contacter directement votre concessionnaire audio OM SYSTEM
|
||||
agréé sélectionné.
|
||||
</p>
|
||||
<p>
|
||||
Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.<br />
|
||||
Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
59
dictation_server/src/templates/template_U_125.txt
Normal file
59
dictation_server/src/templates/template_U_125.txt
Normal file
@ -0,0 +1,59 @@
|
||||
<English>
|
||||
|
||||
Dear $CUSTOMER_NAME$,
|
||||
|
||||
Thank you for choosing ODMS Cloud.
|
||||
|
||||
We have granted [$LICENSE_QUANTITY$] trial licenses to your account which is valid for $EXPIRATION_DATE$ days. During the trial, you can try all the features of ODMS Cloud.
|
||||
|
||||
If you wish to continue using ODMS Cloud after the trial period has expired, please contact an authorized OM SYSTEM audio dealer to purchase annual licenses. Various settings including dealer selection can be configured within ODMS Cloud under the Account tab.
|
||||
|
||||
Please log in to ODMS Cloud to configure your user setting and and verify the license expiration date.
|
||||
URL: $TOP_URL$
|
||||
|
||||
After you have selected a dealer, to request the number of licenses please select Subscription tab. Licenses issued by dealers will be stored in your license Inventory.
|
||||
|
||||
If you need assistance with ODMS Cloud, please contact your selected approved OM SYSTEM audio dealer directly.
|
||||
|
||||
If you have received this e-mail in error, please delete this e-mail from your system.
|
||||
This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply.
|
||||
|
||||
<Deutsch>
|
||||
|
||||
Sehr geehrte(r) $CUSTOMER_NAME$,
|
||||
|
||||
Vielen Dank, dass Sie sich für ODMS Cloud entschieden haben.
|
||||
|
||||
Wir haben Ihrem Konto [$LICENSE_QUANTITY$] Testlizenzen hinzugefügt, die $EXPIRATION_DATE$ Tage gültig sind. Während der Testversion können Sie alle Funktionen von ODMS Cloud ausprobieren.
|
||||
|
||||
Wenn Sie ODMS Cloud nach Ablauf des Testzeitraums weiterhin nutzen möchten, wenden Sie sich bitte an einen autorisierten OM SYSTEM-Audiohändler, um Jahreslizenzen zu erwerben. Verschiedene Einstellungen, einschließlich der Händlerauswahl, können in der ODMS Cloud auf der Registerkarte „Konto“ konfiguriert werden.
|
||||
|
||||
Bitte melden Sie sich bei ODMS Cloud an, um Ihre Benutzereinstellungen zu konfigurieren und das Ablaufdatum der Lizenz zu überprüfen.
|
||||
URL: $TOP_URL$
|
||||
|
||||
Nachdem Sie einen Händler ausgewählt haben, wählen Sie bitte die Registerkarte „Abonnement“ aus, um die Anzahl der Lizenzen anzufordern. Von Händlern ausgestellte Lizenzen werden in Ihrem Lizenzbestand gespeichert.
|
||||
|
||||
Wenn Sie Hilfe mit ODMS Cloud benötigen, wenden Sie sich bitte direkt an Ihren ausgewählten zugelassenen OM SYSTEM-Audiohändler.
|
||||
|
||||
Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
|
||||
Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht.
|
||||
|
||||
<Français>
|
||||
|
||||
Chère/Cher $CUSTOMER_NAME$,
|
||||
|
||||
Merci d'avoir choisi ODMS Cloud.
|
||||
|
||||
Nous avons accordé [$LICENSE_QUANTITY$] licences d'essai à votre compte, valables $EXPIRATION_DATE$ jours. Pendant la période d'essai, vous pouvez essayer toutes les fonctionnalités d'ODMS Cloud.
|
||||
|
||||
Si vous souhaitez continuer à utiliser ODMS Cloud après l'expiration de la période d'essai, veuillez contacter un concessionnaire audio agréé OM SYSTEM pour acheter des licences annuelles. Divers paramètres, y compris la sélection du concessionnaire, peuvent être configurés dans ODMS Cloud sous l'onglet Compte.
|
||||
|
||||
Veuillez vous connecter à ODMS Cloud pour configurer vos paramètres utilisateur et vérifier la date d'expiration de la licence.
|
||||
URL: $TOP_URL$
|
||||
|
||||
Après avoir sélectionné un concessionnaire, pour demander le nombre de licences, veuillez sélectionner l'onglet Abonnement. Les licences délivrées par les concessionnaires seront stockées dans votre inventaire de licences.
|
||||
|
||||
Si vous avez besoin d'aide avec ODMS Cloud, veuillez contacter directement votre concessionnaire audio OM SYSTEM agréé sélectionné.
|
||||
|
||||
Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
|
||||
Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre.
|
||||
92
dictation_server/src/templates/template_U_126.html
Normal file
92
dictation_server/src/templates/template_U_126.html
Normal file
@ -0,0 +1,92 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Forced Email Verification Notification [U-126]</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h3><English></h3>
|
||||
<p>Dear $CUSTOMER_NAME$,</p>
|
||||
<p>
|
||||
Your user information has been registered in the ODMS Cloud. To complete the user registration, your email address
|
||||
must be verified. Since you were not able to complete the verification, your administrator has forcibly verified
|
||||
your email address on your behalf.
|
||||
</p>
|
||||
<p>
|
||||
To sign into the ODMS Cloud, please select "Forgot your password?" from the ODMS Cloud sign-in screen and follow
|
||||
the instructions to set your password. Once your password has been set, you will be able to log into your ODMS
|
||||
Cloud account
|
||||
</p>
|
||||
<p>
|
||||
Please be aware that you might not be able to properly receive email notifications from the ODMS Cloud since you may
|
||||
not have received the initial verification email. This may be due to restrictions placed on your system. Please
|
||||
consult your company's IT administrator to resolve any problems receiving email notifications from the ODMS Cloud.
|
||||
</p>
|
||||
<p>
|
||||
ODMS Cloud Top page<br />
|
||||
URL: $TOP_URL$
|
||||
</p>
|
||||
<p>
|
||||
If you have received this e-mail in error, please delete this e-mail from your system.<br />
|
||||
This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3><Deutsch></h3>
|
||||
<p>Sehr geehrte(r) $CUSTOMER_NAME$,</p>
|
||||
<p>
|
||||
Ihre Benutzerinformationen wurden in der ODMS Cloud registriert. Um die Benutzerregistrierung abzuschließen, muss Ihre
|
||||
E-Mail-Adresse verifiziert werden. Da Sie die Verifizierung nicht abschließen konnten, hat Ihr Administrator Ihre
|
||||
E-Mail-Adresse in Ihrem Namen zwangsweise verifiziert.
|
||||
</p>
|
||||
<p>
|
||||
Um sich bei der ODMS Cloud anzumelden, wählen Sie auf dem ODMS Cloud-Anmeldebildschirm „Passwort vergessen?“ und folgen
|
||||
Sie den Anweisungen zum Festlegen Ihres Passworts. Sobald Ihr Passwort festgelegt wurde, können Sie sich bei Ihrem ODMS
|
||||
Cloud-Konto anmelden.
|
||||
</p>
|
||||
<p>
|
||||
Bitte beachten Sie, dass Sie möglicherweise keine E-Mail-Benachrichtigungen von der ODMS Cloud ordnungsgemäß empfangen
|
||||
können, da Sie möglicherweise die erste Verifizierungs-E-Mail nicht erhalten haben. Dies kann auf Einschränkungen Ihres
|
||||
Systems zurückzuführen sein. Wenden Sie sich bitte an den IT-Administrator Ihres Unternehmens, um etwaige Probleme beim
|
||||
Empfang von E-Mail-Benachrichtigungen von der ODMS Cloud zu beheben.
|
||||
</p>
|
||||
<p>
|
||||
ODMS Cloud Startseite<br />
|
||||
URL: $TOP_URL$
|
||||
</p>
|
||||
<p>
|
||||
Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.<br />
|
||||
Es handelt sich um eine automatisch generierte E-Mail, und dieses Postfach wird nicht überwacht. Bitte antworten Sie
|
||||
nicht.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3><Français></h3>
|
||||
<p>Chère/Cher $CUSTOMER_NAME$,</p>
|
||||
<p>
|
||||
Vos informations d'utilisateur ont été enregistrées dans le Cloud ODMS. Pour terminer l'enregistrement de l'utilisateur,
|
||||
votre adresse e-mail doit être vérifiée. Comme vous n'avez pas pu terminer la vérification, votre administrateur a vérifié
|
||||
de force votre adresse e-mail en votre nom.
|
||||
</p>
|
||||
<p>
|
||||
Pour vous connecter au Cloud ODMS, veuillez sélectionner « Mot de passe oublié ? » dans l'écran de connexion au Cloud ODMS
|
||||
et suivez les instructions pour définir votre mot de passe. Une fois votre mot de passe défini, vous pourrez vous connecter
|
||||
à votre compte Cloud ODMS.
|
||||
</p>
|
||||
<p>
|
||||
Veuillez noter que vous ne pourrez peut-être pas recevoir correctement les notifications par e-mail du Cloud ODMS, car
|
||||
vous n'avez peut-être pas reçu l'e-mail de vérification initial. Cela peut être dû à des restrictions imposées à votre
|
||||
système. Veuillez consulter l'administrateur informatique de votre entreprise pour résoudre tout problème de réception
|
||||
de notifications par e-mail du Cloud ODMS.
|
||||
</p>
|
||||
<p>
|
||||
Page d'accueil du Cloud ODMS<br />
|
||||
URL: $TOP_URL$
|
||||
</p>
|
||||
<p>
|
||||
Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.<br />
|
||||
Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
47
dictation_server/src/templates/template_U_126.txt
Normal file
47
dictation_server/src/templates/template_U_126.txt
Normal file
@ -0,0 +1,47 @@
|
||||
<English>
|
||||
|
||||
Dear $CUSTOMER_NAME$,
|
||||
|
||||
Your user information has been registered in the ODMS Cloud. To complete the user registration, your email address must be verified. Since you were not able to complete the verification, your administrator has forcibly verified your email address on your behalf.
|
||||
|
||||
To sign into the ODMS Cloud, please select "Forgot your password?" from the ODMS Cloud sign-in screen and follow the instructions to set your password. Once your password has been set, you will be able to log into your ODMS Cloud account
|
||||
|
||||
Please be aware that you might not be able to properly receive email notifications from the ODMS Cloud since you may not have received the initial verification email. This may be due to restrictions placed on your system. Please consult your company's IT administrator to resolve any problems receiving email notifications from the ODMS Cloud.
|
||||
|
||||
ODMS Cloud Top page
|
||||
URL: $TOP_URL$
|
||||
|
||||
If you have received this e-mail in error, please delete this e-mail from your system.
|
||||
This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply.
|
||||
|
||||
<Deutsch>
|
||||
|
||||
Sehr geehrte(r) $CUSTOMER_NAME$,
|
||||
|
||||
Ihre Benutzerinformationen wurden in der ODMS Cloud registriert. Um die Benutzerregistrierung abzuschließen, muss Ihre E-Mail-Adresse verifiziert werden. Da Sie die Verifizierung nicht abschließen konnten, hat Ihr Administrator Ihre E-Mail-Adresse in Ihrem Namen zwangsweise verifiziert.
|
||||
|
||||
Um sich bei der ODMS Cloud anzumelden, wählen Sie auf dem ODMS Cloud-Anmeldebildschirm „Passwort vergessen?“ und folgen Sie den Anweisungen zum Festlegen Ihres Passworts. Sobald Ihr Passwort festgelegt wurde, können Sie sich bei Ihrem ODMS Cloud-Konto anmelden.
|
||||
|
||||
Bitte beachten Sie, dass Sie möglicherweise keine E-Mail-Benachrichtigungen von der ODMS Cloud ordnungsgemäß empfangen können, da Sie möglicherweise die erste Verifizierungs-E-Mail nicht erhalten haben. Dies kann auf Einschränkungen Ihres Systems zurückzuführen sein. Wenden Sie sich bitte an den IT-Administrator Ihres Unternehmens, um etwaige Probleme beim Empfang von E-Mail-Benachrichtigungen von der ODMS Cloud zu beheben.
|
||||
|
||||
ODMS Cloud Startseite
|
||||
URL: $TOP_URL$
|
||||
|
||||
Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System.
|
||||
Es handelt sich um eine automatisch generierte E-Mail, und dieses Postfach wird nicht überwacht. Bitte antworten Sie nicht.
|
||||
|
||||
<Français>
|
||||
|
||||
Chère/Cher $CUSTOMER_NAME$,
|
||||
|
||||
Vos informations d'utilisateur ont été enregistrées dans le Cloud ODMS. Pour terminer l'enregistrement de l'utilisateur, votre adresse e-mail doit être vérifiée. Comme vous n'avez pas pu terminer la vérification, votre administrateur a vérifié de force votre adresse e-mail en votre nom.
|
||||
|
||||
Pour vous connecter au Cloud ODMS, veuillez sélectionner « Mot de passe oublié ? » dans l'écran de connexion au Cloud ODMS et suivez les instructions pour définir votre mot de passe. Une fois votre mot de passe défini, vous pourrez vous connecter à votre compte Cloud ODMS.
|
||||
|
||||
Veuillez noter que vous ne pourrez peut-être pas recevoir correctement les notifications par e-mail du Cloud ODMS, car vous n'avez peut-être pas reçu l'e-mail de vérification initial. Cela peut être dû à des restrictions imposées à votre système. Veuillez consulter l'administrateur informatique de votre entreprise pour résoudre tout problème de réception de notifications par e-mail du Cloud ODMS.
|
||||
|
||||
Page d'accueil du Cloud ODMS
|
||||
URL: $TOP_URL$
|
||||
|
||||
Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système.
|
||||
Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre.
|
||||
88
environment_building_tools/README.md
Executable file
88
environment_building_tools/README.md
Executable file
@ -0,0 +1,88 @@
|
||||
# 開発環境構築ツール
|
||||
|
||||
このディレクトリには、ローカル開発環境を構築するためのスクリプト、クライアントのビルドスクリプト、サーバー起動スクリプトが含まれています。
|
||||
|
||||
## 前提
|
||||
1. `dictation_client` と `dictation_server` のローカル環境ファイル `.env.local` 設定されたこと
|
||||
|
||||
2. Docker インストール済み
|
||||
|
||||
[Docker](https://www.docker.com/)
|
||||
|
||||
3. DevContainer CLI インストール済み
|
||||
|
||||
```bash
|
||||
npm install -g @devcontainers/cli
|
||||
```
|
||||
|
||||
## ファイル一覧
|
||||
|
||||
- **environment-build-local.sh**
|
||||
初期ローカル開発環境をセットアップするためのスクリプトです。依存関係のチェック、MySQL データベースの設定、Dev Container のインストールを確認し、ローカル構築をします。
|
||||
|
||||
- **buildclient.sh**
|
||||
`dictation_client` コンテナの起動、クライアントサイドの依存関係をインストールし、サーバー環境のファイルをビルドします。
|
||||
|
||||
- **runserver.sh**
|
||||
`dictation_server` コンテナの起動、依存関係のインストールし、サーバーの起動を行います。
|
||||
|
||||
- **killserver.sh**
|
||||
現在稼働しているコンテナプロセスを停止するためのスクリプトです。
|
||||
|
||||
- **logfile.log**
|
||||
各スクリプトの実行中に発生するログを記録するファイルです。
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. **依存関係の確認**
|
||||
`environment-build-local.sh` を実行すると、Docker、Dev Container CLI の依存関係を確認、`.env.local`ファイル確認し、環境構築します。
|
||||
|
||||
```bash
|
||||
sh environment-build-local.sh
|
||||
```
|
||||
|
||||
2. **クライアントのセットアップ**
|
||||
`buildclient.sh` を使用して `dictation_client` をセットアップします。
|
||||
|
||||
```bash
|
||||
sh buildclient.sh
|
||||
```
|
||||
|
||||
3. サーバーの起動
|
||||
`runserver.sh` を実行して `dictation_server` を起動します。
|
||||
|
||||
```bash
|
||||
sh runserver.sh
|
||||
```
|
||||
|
||||
4. サーバーの停止
|
||||
サーバーを停止したい場合は `killserver.sh` を実行します。
|
||||
|
||||
```bash
|
||||
sh killserver.sh
|
||||
```
|
||||
|
||||
## ログ
|
||||
|
||||
各スクリプトの実行中のメッセージは `logfile.log` に記録されます。エラーメッセージとステータスを確認したい場合は、このファイルを参照してください。
|
||||
|
||||
## 手動でのクライアントビルド手順
|
||||
|
||||
1. `dictation_client` フォルダを VSCode で開く。
|
||||
2. DevContainer で開くか確認するダイアログが表示されたら、「DevContainer で開く」を選択。
|
||||
3. ターミナルで以下のコマンドを実行:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build:local
|
||||
```
|
||||
|
||||
## 手動でのサーバー起動手順
|
||||
1. `dictation_server` フォルダを VSCode で開く。
|
||||
2. DevContainer で開くか確認するダイアログが表示されたら、「DevContainer で開く」を選択。
|
||||
3. ターミナルで以下のコマンドを実行:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run start
|
||||
```
|
||||
77
environment_building_tools/buildclient.sh
Executable file
77
environment_building_tools/buildclient.sh
Executable file
@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ファイルフルパス
|
||||
INITIAL_DIR=$(pwd)
|
||||
LOG_FILE="$INITIAL_DIR/logfile.log"
|
||||
ODMS_CLOUD_DIR="../"
|
||||
|
||||
# ログ関数
|
||||
log_message() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# 1: ODMS Cloudにディレクトリ変更docker-compose up実行
|
||||
cd $ODMS_CLOUD_DIR 2>>"$LOG_FILE"
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Failed to navigate to /ODMS%Cloud/"
|
||||
exit 1
|
||||
else
|
||||
log_message "Navigated to /ODMS%Cloud/"
|
||||
fi
|
||||
|
||||
log_message "Running docker-compose up -d"
|
||||
docker-compose up -d 2>>"$LOG_FILE"
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Failed to run docker-compose up -d"
|
||||
exit 1
|
||||
else
|
||||
log_message "Successfully ran docker-compose up -d"
|
||||
fi
|
||||
|
||||
# 2: dictation_clientにDev Container起動する
|
||||
log_message "Starting Dev Container in dictation_client/"
|
||||
devcontainer up --workspace-folder "dictation_client/" 2>>"$LOG_FILE"
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Failed to start Dev Container in dictation_client/"
|
||||
exit 1
|
||||
else
|
||||
log_message "Dev Container started in dictation_client/"
|
||||
fi
|
||||
|
||||
# DevContainerの起動待ち
|
||||
log_message "Waiting for Dev Container to be ready..."
|
||||
sleep 5
|
||||
|
||||
# 3: コンテナーID・名取得
|
||||
log_message "Retrieving the container ID of the Dev Container"
|
||||
CONTAINER_ID=$(docker ps --filter "label=devcontainer.local_folder=$(pwd)/dictation_client" --format "{{.ID}}")
|
||||
|
||||
if [ -z "$CONTAINER_ID" ]; then
|
||||
log_message "ERROR: Could not find the Dev Container for dictation_client/"
|
||||
exit 1
|
||||
else
|
||||
log_message "Found Dev Container with ID: $CONTAINER_ID"
|
||||
fi
|
||||
|
||||
# 4: docker execでDev Containerに'npm install'実行
|
||||
COMMAND="npm install"
|
||||
|
||||
log_message "Executing command inside Dev Container: $COMMAND"
|
||||
docker exec --user vscode "$CONTAINER_ID" $COMMAND >>"$LOG_FILE" 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Command execution failed inside Dev Container"
|
||||
exit 1
|
||||
else
|
||||
log_message "Command executed successfully inside Dev Container"
|
||||
fi
|
||||
|
||||
# 5: 'npm run build:local'実行
|
||||
COMMAND="npm run build:local"
|
||||
log_message "Executing command inside Dev Container: $COMMAND"
|
||||
docker exec --user vscode "$CONTAINER_ID" $COMMAND >>"$LOG_FILE" 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Command execution failed inside Dev Container"
|
||||
exit 1
|
||||
else
|
||||
log_message "Command executed successfully inside Dev Container"
|
||||
fi
|
||||
91
environment_building_tools/environment-build-local.sh
Executable file
91
environment_building_tools/environment-build-local.sh
Executable file
@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ファイルフルパス
|
||||
INITIAL_DIR=$(pwd)
|
||||
LOG_FILE="$INITIAL_DIR/logfile.log"
|
||||
ODMS_CLOUD_DIR="../"
|
||||
ENV_FILE_PATH="../.env"
|
||||
CLIENT_ENV_FILE="../dictation_client/.env.local"
|
||||
SERVER_ENV_FILE="../dictation_server/.env.local"
|
||||
|
||||
# ログ関数
|
||||
log_message() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# コマンド存在確認
|
||||
check_command() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Docker確認
|
||||
check_docker_version() {
|
||||
DOCKER_VERSION=$(docker --version 2>/dev/null | grep -o '[0-9.]*' | head -1)
|
||||
if [[ -z "$DOCKER_VERSION" ]]; then
|
||||
log_message "ERROR: Docker is not installed or not available in PATH."
|
||||
return 1
|
||||
else
|
||||
log_message "Docker version $DOCKER_VERSION is available."
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Dev Container CLI 確認
|
||||
check_devcontainer_cli() {
|
||||
if check_command "devcontainer"; then
|
||||
log_message "Dev Container CLI is available."
|
||||
return 0
|
||||
else
|
||||
log_message "ERROR: Dev Container CLI is not installed."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# logfile.log が存在しないとき作成
|
||||
touch logfile.log
|
||||
# スクリプト実行に必要なパッケージ確認
|
||||
log_message "Starting dependency check..."
|
||||
|
||||
if ! check_docker_version; then
|
||||
log_message "Dependency check failed: Docker is required."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! check_devcontainer_cli; then
|
||||
log_message "Dependency check failed: Dev Container CLI is required."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_message "All dependencies are met. Proceeding with the script..."
|
||||
|
||||
# .envファイル確認
|
||||
if [ -f "$ENV_FILE_PATH" ]; then
|
||||
log_message ".env file found at $ENV_FILE_PATH"
|
||||
else
|
||||
log_message "ERROR: .env file not found at $ENV_FILE_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# clientの.env.localファイル確認
|
||||
if [ -f "$CLIENT_ENV_FILE" ]; then
|
||||
log_message ".env.local file found in dictation_client."
|
||||
else
|
||||
log_message "ERROR: .env.local file missing in dictation_client."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# serverの.env.localファイル確認
|
||||
if [ -f "$SERVER_ENV_FILE" ]; then
|
||||
log_message ".env.local file found in dictation_server."
|
||||
else
|
||||
log_message "ERROR: .env.local file missing in dictation_server."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_message "All required .env files are present."
|
||||
|
||||
# 1: dictation_client に Dev Container 起動, ローカル用サイトビルドする
|
||||
sh ./buildclient.sh
|
||||
|
||||
# 2: dictation_server に Dev Container 起動, ローカル用のサーバー起動する
|
||||
sh ./runserver.sh
|
||||
32
environment_building_tools/killserver.sh
Executable file
32
environment_building_tools/killserver.sh
Executable file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# ファイルフルパス
|
||||
INITIAL_DIR=$(pwd)
|
||||
LOG_FILE="$INITIAL_DIR/logfile.log"
|
||||
|
||||
log_message() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
# Docker dictation_client コンテナと dictation_server コンテナを停止
|
||||
CONTAINER_EXISTS=$(docker ps -a --filter "name=dictation_server_dev_container" --format "{{.ID}}")
|
||||
if [ -n "$CONTAINER_EXISTS" ]; then
|
||||
log_message "Stopping existing Dev Container: $CONTAINER_EXIST..."
|
||||
docker stop "$CONTAINER_EXISTS" >>"$LOG_FILE" 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Failed to remove the Dev Container: $CONTAINER_EXIST."
|
||||
exit 1
|
||||
else
|
||||
log_message "Dev Container: dictation_server removed successfully."
|
||||
fi
|
||||
fi
|
||||
|
||||
CONTAINER_EXISTS=$(docker ps -a --filter "name=dictation_client_devcontainer" --format "{{.ID}}")
|
||||
if [ -n "$CONTAINER_EXISTS" ]; then
|
||||
log_message "Stopping existing Dev Container: $CONTAINER_EXIST..."
|
||||
docker stop "$CONTAINER_EXISTS" >>"$LOG_FILE" 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Failed to remove the Dev Container: $CONTAINER_EXIST."
|
||||
exit 1
|
||||
else
|
||||
log_message "Dev Container: dictation_client removed successfully."
|
||||
fi
|
||||
fi
|
||||
97
environment_building_tools/runserver.sh
Executable file
97
environment_building_tools/runserver.sh
Executable file
@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ファイルフルパス
|
||||
INITIAL_DIR=$(pwd)
|
||||
LOG_FILE="$INITIAL_DIR/logfile.log"
|
||||
ODMS_CLOUD_DIR="../"
|
||||
|
||||
# ログ関数
|
||||
log_message() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# 1: ODMS Cloudにディレクトリ変更docker-compose up実行
|
||||
cd $ODMS_CLOUD_DIR 2>>"$LOG_FILE"
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Failed to navigate to /ODMS%Cloud/"
|
||||
exit 1
|
||||
else
|
||||
log_message "Navigated to /ODMS%Cloud/"
|
||||
fi
|
||||
|
||||
log_message "Running docker-compose up -d"
|
||||
docker-compose up -d 2>>"$LOG_FILE"
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Failed to run docker-compose up -d"
|
||||
exit 1
|
||||
else
|
||||
log_message "Successfully ran docker-compose up -d"
|
||||
fi
|
||||
|
||||
# 2: dictation_serverコンテナプロセス存在確認、停止
|
||||
CONTAINER_EXISTS=$(docker ps -a --filter "name=dictation_server_dev_container" --format "{{.ID}}")
|
||||
if [ -n "$CONTAINER_EXISTS" ]; then
|
||||
log_message "Stopping existing Dev Container..."
|
||||
docker stop "$CONTAINER_EXISTS" >>"$LOG_FILE" 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Failed to remove the Dev Container."
|
||||
exit 1
|
||||
else
|
||||
log_message "Dev Container removed successfully."
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3: dictation_serverをDevContainerに起動
|
||||
log_message "Starting Dev Container in dictation_server/"
|
||||
devcontainer up --workspace-folder "dictation_server/" 2>>"$LOG_FILE"
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Failed to start Dev Container in dictation_server/"
|
||||
exit 1
|
||||
else
|
||||
log_message "Dev Container started in dictation_server/"
|
||||
fi
|
||||
|
||||
# DevContainerの起動待ち
|
||||
log_message "Waiting for Dev Container to be ready..."
|
||||
sleep 5
|
||||
|
||||
# 4: コンテナーID・名取得
|
||||
log_message "Retrieving the container ID of the Dev Container"
|
||||
CONTAINER_ID=$(docker ps --filter "label=devcontainer.local_folder=$(pwd)/dictation_server" --format "{{.ID}}")
|
||||
|
||||
if [ -z "$CONTAINER_ID" ]; then
|
||||
log_message "ERROR: Could not find the Dev Container for dictation_server/"
|
||||
exit 1
|
||||
else
|
||||
log_message "Found Dev Container with ID: $CONTAINER_ID"
|
||||
fi
|
||||
|
||||
# 5: docker execでDev Containerに'npm install'実行
|
||||
COMMAND="npm install"
|
||||
|
||||
log_message "Executing command inside Dev Container: $COMMAND"
|
||||
docker exec --user vscode "$CONTAINER_ID" $COMMAND >>"$LOG_FILE" 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Command execution failed inside Dev Container"
|
||||
exit 1
|
||||
else
|
||||
log_message "Command executed successfully inside Dev Container"
|
||||
fi
|
||||
|
||||
# 6: 'npm run migrate:up' 実行
|
||||
COMMAND="npm run migrate:up"
|
||||
log_message "Executing command inside Dev Container: $COMMAND"
|
||||
|
||||
docker exec --user vscode "$CONTAINER_ID" $COMMAND >>"$LOG_FILE" 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
log_message "ERROR: Command execution failed inside Dev Container"
|
||||
exit 1
|
||||
else
|
||||
log_message "Command executed successfully inside Dev Container"
|
||||
fi
|
||||
|
||||
# 7: 'npm run start'実行
|
||||
COMMAND="npm run start"
|
||||
|
||||
log_message "Executing command inside Dev Container: $COMMAND"
|
||||
docker exec --user vscode "$CONTAINER_ID" $COMMAND
|
||||
Loading…
x
Reference in New Issue
Block a user