Compare commits

..

No commits in common. "main" and "release-2024-11-12-1" have entirely different histories.

105 changed files with 866 additions and 12175 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
environment_building_tools/logfile.log

View File

@ -17,6 +17,10 @@ RUN bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "$
&& apt-get install default-jre -y \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
# Update NPM
RUN npm install -g npm
# Install mob
RUN curl -sL install.mob.sh | sh

View File

@ -27,7 +27,6 @@ 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

View File

@ -10,8 +10,6 @@ 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";
@ -37,8 +35,6 @@ export const store = configureStore({
licenseSummary,
licenseOrderHistory,
partnerLicense,
licenseTrialIssue,
searchPartners,
dictation,
partner,
typistGroup,

View File

@ -1,7 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -47,7 +47,6 @@ export const KEYS_TO_PRESERVE = [
"accessToken",
"refreshToken",
"displayInfo",
"filterCriteria",
"sortCriteria",
];

View File

@ -39,7 +39,7 @@ export const getAccountRelationsAsync = createAsyncThunk<
headers: { authorization: `Bearer ${accessToken}` },
});
const dealers = await accountsApi.getDealers();
const users = await usersApi.getUsers(undefined, undefined, {
const users = await usersApi.getUsers({
headers: { authorization: `Bearer ${accessToken}` },
});
return {

View File

@ -43,8 +43,6 @@ const initialState: DictationState = {
direction: DIRECTION.ASC,
paramName: SORTABLE_COLUMN.JobNumber,
selectedTask: undefined,
authorId: "",
fileName: "",
assignee: {
selected: [],
pool: [],
@ -80,14 +78,6 @@ 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;
@ -256,8 +246,6 @@ export const {
changeDisplayInfo,
changeDirection,
changeParamName,
changeAuthorId,
changeFileName,
changeSelectedTask,
changeAssignee,
changeBackupTaskChecked,

View File

@ -35,8 +35,6 @@ export const listTasksAsync = createAsyncThunk<
filter?: string;
direction: DirectionType;
paramName: SortableColumnType;
authorId?: string;
fileName?: string;
},
{
// rejectした時の返却値の型
@ -45,8 +43,7 @@ export const listTasksAsync = createAsyncThunk<
};
}
>("dictations/listTasksAsync", async (args, thunkApi) => {
const { limit, offset, filter, direction, paramName, authorId, fileName } =
args;
const { limit, offset, filter, direction, paramName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
@ -63,8 +60,6 @@ export const listTasksAsync = createAsyncThunk<
filter,
direction,
paramName,
authorId,
fileName,
{
headers: { authorization: `Bearer ${accessToken}` },
}
@ -85,136 +80,6 @@ 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;
@ -415,8 +280,6 @@ export const playbackAsync = createAsyncThunk<
direction: DirectionType;
paramName: SortableColumnType;
audioFileId: number;
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
@ -425,13 +288,7 @@ export const playbackAsync = createAsyncThunk<
};
}
>("dictations/playbackAsync", async (args, thunkApi) => {
const {
audioFileId,
direction,
paramName,
filterConditionAuthorId,
filterConditionFileName,
} = args;
const { audioFileId, direction, paramName } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
@ -448,12 +305,6 @@ 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}` },
});
@ -536,8 +387,6 @@ export const cancelAsync = createAsyncThunk<
paramName: SortableColumnType;
audioFileId: number;
isTypist: boolean;
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
@ -546,14 +395,7 @@ export const cancelAsync = createAsyncThunk<
};
}
>("dictations/cancelAsync", async (args, thunkApi) => {
const {
audioFileId,
direction,
paramName,
isTypist,
filterConditionAuthorId,
filterConditionFileName,
} = args;
const { audioFileId, direction, paramName, isTypist } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
@ -564,25 +406,15 @@ export const cancelAsync = createAsyncThunk<
const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config);
try {
// ユーザーがタイピストである場合に、ソート条件と検索条件を保存する
// ユーザーがタイピストである場合に、ソート条件を保存する
if (isTypist) {
await usersApi.updateSortCriteria(
{
direction,
paramName,
},
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
await usersApi.updateTaskFilter(
{ filterConditionAuthorId, filterConditionFileName },
{ direction, paramName },
{
headers: { authorization: `Bearer ${accessToken}` },
}
);
}
await tasksApi.cancel(audioFileId, {
headers: { authorization: `Bearer ${accessToken}` },
});
@ -627,8 +459,6 @@ export const reopenAsync = createAsyncThunk<
paramName: SortableColumnType;
audioFileId: number;
isTypist: boolean;
filterConditionAuthorId: string;
filterConditionFileName: string;
},
{
// rejectした時の返却値の型
@ -637,14 +467,7 @@ export const reopenAsync = createAsyncThunk<
};
}
>("dictations/reopenAsync", async (args, thunkApi) => {
const {
audioFileId,
direction,
paramName,
isTypist,
filterConditionAuthorId,
filterConditionFileName,
} = args;
const { audioFileId, direction, paramName, isTypist } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
@ -655,7 +478,7 @@ export const reopenAsync = createAsyncThunk<
const tasksApi = new TasksApi(config);
const usersApi = new UsersApi(config);
try {
// ユーザーがタイピストである場合に、ソート条件と検索条件を保存する
// ユーザーがタイピストである場合に、ソート条件を保存する
if (isTypist) {
await usersApi.updateSortCriteria(
{ direction, paramName },
@ -663,12 +486,6 @@ 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}` },
@ -735,8 +552,6 @@ export const listBackupPopupTasksAsync = createAsyncThunk<
BACKUP_POPUP_LIST_STATUS.join(","), // ステータスはFinished,Backupのみ
DIRECTION.DESC,
SORTABLE_COLUMN.Status,
undefined, // backupポップアップ表示時には検索条件は未指定
undefined, // backupポップアップ表示時には検索条件は未指定
{
headers: { authorization: `Bearer ${accessToken}` },
}

View File

@ -72,12 +72,6 @@ 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;

View File

@ -25,8 +25,6 @@ export interface Apps {
displayInfo: DisplayInfoType;
direction: DirectionType;
paramName: SortableColumnType;
authorId: string;
fileName: string;
selectedTask?: Task;
selectedFileTask?: Task;
assignee: {

View File

@ -7,8 +7,3 @@ 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;

View File

@ -3,12 +3,7 @@ import type { RootState } from "app/store";
import { getTranslationID } from "translation";
import { openSnackbar } from "features/ui/uiSlice";
import { getAccessToken } from "features/auth";
import {
AccountsApi,
LicensesApi,
SearchPartner,
PartnerLicenseInfo,
} from "../../../api/api";
import { AccountsApi, LicensesApi } from "../../../api/api";
import { Configuration } from "../../../api/configuration";
import { ErrorObject, createErrorObject } from "../../../common/errors";
import { OrderHistoryView } from "./types";
@ -20,7 +15,6 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
// パラメータ
limit: number;
offset: number;
selectedRow?: PartnerLicenseInfo | SearchPartner;
},
{
// rejectした時の返却値の型
@ -29,7 +23,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
};
}
>("licenses/licenseOrderHisotyAsync", async (args, thunkApi) => {
const { limit, offset, selectedRow } = args;
const { limit, offset } = args;
// apiのConfigurationを取得する
const { getState } = thunkApi;
const state = getState() as RootState;
@ -39,6 +33,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
const accountsApi = new AccountsApi(config);
try {
const { selectedRow } = state.partnerLicense.apps;
let accountId = 0;
let companyName = "";
// 他の画面から指定されていない場合はログインアカウントのidを取得する
@ -51,9 +46,7 @@ export const getLicenseOrderHistoriesAsync = createAsyncThunk<
companyName = getMyAccountResponse.data.account.companyName;
} else {
accountId = selectedRow.accountId;
// パートナーライセンスとパートナー検索で型が異なるため、型ガードで推論させる
if ("companyName" in selectedRow) companyName = selectedRow.companyName;
if ("name" in selectedRow) companyName = selectedRow.name;
companyName = selectedRow.companyName;
}
const res = await accountsApi.getOrderHistories(

View File

@ -7,7 +7,6 @@ import {
AccountsApi,
GetCompanyNameResponse,
GetLicenseSummaryResponse,
SearchPartner,
PartnerLicenseInfo,
UpdateRestrictionStatusRequest,
} from "../../../api/api";
@ -18,7 +17,7 @@ export const getLicenseSummaryAsync = createAsyncThunk<
// 正常時の戻り値の型
GetLicenseSummaryResponse,
// 引数
{ selectedRow?: PartnerLicenseInfo | SearchPartner },
{ selectedRow?: PartnerLicenseInfo },
{
// rejectした時の返却値の型
rejectValue: {
@ -74,7 +73,7 @@ export const getCompanyNameAsync = createAsyncThunk<
// 正常時の戻り値の型
GetCompanyNameResponse,
// 引数
{ selectedRow?: PartnerLicenseInfo | SearchPartner },
{ selectedRow?: PartnerLicenseInfo },
{
// rejectした時の返却値の型
rejectValue: {

View File

@ -1,2 +0,0 @@
export const ISSUED_TRIAL_LICENSE_QUANTITY = 10;
export const TRIAL_LICENSE_EXPIRATION_DAY = 30;

View File

@ -1,5 +0,0 @@
export * from "./state";
export * from "./operations";
export * from "./selectors";
export * from "./licenseTrialIssueSlice";
export * from "./constants";

View File

@ -1,60 +0,0 @@
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;

View File

@ -1,84 +0,0 @@
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 });
}
});

View File

@ -1,10 +0,0 @@
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;

View File

@ -1,9 +0,0 @@
export interface LicenseTrialIssueState {
apps: Apps;
}
export interface Apps {
isLoading: boolean;
expirationDate: string;
quantity: number;
}

View File

@ -25,7 +25,6 @@ const initialState: PartnerLicensesState = {
tier: 0,
companyName: "",
stockLicense: 0,
allocatedLicense: 0,
issuedRequested: 0,
shortage: 0,
issueRequesting: 0,
@ -40,9 +39,6 @@ const initialState: PartnerLicensesState = {
hierarchicalElements: [],
isLoading: true,
selectedRow: undefined,
isLicenseOrderHistoryOpen: false,
isViewDetailsOpen: false,
isSearchPopupOpen: false,
},
};
@ -92,24 +88,6 @@ 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) => {
@ -153,9 +131,6 @@ export const {
clearHierarchicalElement,
changeSelectedRow,
savePageInfo,
setIsLicenseOrderHistoryOpen,
setIsViewDetailsOpen,
setIsSearchPopupOpen,
} = partnerLicenseSlice.actions;
export default partnerLicenseSlice.reducer;

View File

@ -30,10 +30,3 @@ 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;

View File

@ -20,9 +20,6 @@ export interface Apps {
hierarchicalElements: HierarchicalElement[];
isLoading: boolean;
selectedRow?: PartnerLicenseInfo;
isLicenseOrderHistoryOpen: boolean;
isViewDetailsOpen: boolean;
isSearchPopupOpen: boolean;
}
export interface HierarchicalElement {

View File

@ -1,4 +0,0 @@
export * from "./state";
export * from "./operations";
export * from "./selectors";
export * from "./searchPartnerSlice";

View File

@ -1,96 +0,0 @@
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 });
}
});

View File

@ -1,80 +0,0 @@
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;

View File

@ -1,14 +0,0 @@
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;

View File

@ -1,18 +0,0 @@
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;
}

View File

@ -18,7 +18,7 @@ export const listUsersAsync = createAsyncThunk<
// 正常時の戻り値の型
GetUsersResponse,
// 引数
undefined | { userInputUserName?: string; userInputEmail?: string },
void,
{
// rejectした時の返却値の型
rejectValue: {
@ -33,11 +33,9 @@ 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(userInputUserName, userInputEmail, {
const res = await usersApi.getUsers({
headers: { authorization: `Bearer ${accessToken}` },
});
@ -502,72 +500,6 @@ 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<
// 正常時の戻り値の型
{

View File

@ -22,8 +22,6 @@ import {
selectDirection,
changeParamName,
changeDirection,
changeAuthorId,
changeFileName,
changeSelectedTask,
openFilePropertyInfo,
SortableColumnType,
@ -39,11 +37,6 @@ import {
deleteTaskAsync,
isSortableColumnType,
isDirectionType,
getTaskFiltersAsync,
selectAuthorId,
selectFilename,
updateTaskFiltersAsync,
updateSortColumnAsync,
} from "features/dictation";
import { getTranslationID } from "translation";
import { Task } from "api/api";
@ -62,7 +55,6 @@ 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();
@ -109,18 +101,10 @@ 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);
@ -144,8 +128,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
dispatch(listTypistsAsync());
@ -159,8 +141,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup,
sortDirection,
sortableParamName,
authorId,
fileName,
]);
const getLastPage = useCallback(() => {
@ -179,8 +159,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
dispatch(listTypistsAsync());
@ -195,8 +173,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup,
sortDirection,
sortableParamName,
authorId,
fileName,
]);
const getPrevPage = useCallback(() => {
@ -215,8 +191,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
dispatch(listTypistsAsync());
@ -231,8 +205,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup,
sortDirection,
sortableParamName,
authorId,
fileName,
]);
const getNextPage = useCallback(() => {
@ -251,8 +223,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
dispatch(listTypistsAsync());
@ -267,8 +237,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup,
sortDirection,
sortableParamName,
authorId,
fileName,
]);
const updateSortColumn = useCallback(
@ -301,8 +269,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: currentDirection,
paramName,
authorId,
fileName,
})
);
dispatch(listTypistsAsync());
@ -317,8 +283,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filterPending,
filterFinished,
filterBackup,
authorId,
fileName,
]
);
@ -368,19 +332,6 @@ 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,
@ -388,14 +339,12 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
dispatch(listTypistsAsync());
dispatch(listTypistGroupsAsync());
},
[dispatch, sortDirection, sortableParamName, authorId, fileName]
[dispatch, sortDirection, sortableParamName]
);
const onPlayBack = useCallback(
@ -411,8 +360,6 @@ const DictationPage: React.FC = (): JSX.Element => {
audioFileId,
direction: sortDirection,
paramName: sortableParamName,
filterConditionAuthorId: authorId,
filterConditionFileName: fileName,
})
);
if (meta.requestStatus === "fulfilled") {
@ -432,15 +379,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
}:playback?audioId=${audioFileId}`;
const url = `${
import.meta.env.VITE_DESK_TOP_APP_SCHEME
}:playback?audioId=${audioFileId}`;
const a = document.createElement("a");
a.href = url;
document.body.appendChild(a);
@ -457,8 +403,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filterUploaded,
sortDirection,
sortableParamName,
authorId,
fileName,
t,
]
);
@ -480,8 +424,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
}
@ -496,8 +438,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filterBackup,
sortDirection,
sortableParamName,
authorId,
fileName,
]
);
@ -515,8 +455,6 @@ const DictationPage: React.FC = (): JSX.Element => {
direction: sortDirection,
paramName: sortableParamName,
isTypist,
filterConditionAuthorId: authorId,
filterConditionFileName: fileName,
})
);
if (meta.requestStatus === "fulfilled") {
@ -534,8 +472,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
dispatch(listTypistsAsync());
@ -552,8 +488,6 @@ const DictationPage: React.FC = (): JSX.Element => {
isTypist,
sortDirection,
sortableParamName,
authorId,
fileName,
t,
]
);
@ -573,11 +507,8 @@ const DictationPage: React.FC = (): JSX.Element => {
direction: sortDirection,
paramName: sortableParamName,
isTypist,
filterConditionAuthorId: authorId,
filterConditionFileName: fileName,
})
);
if (meta.requestStatus === "fulfilled") {
const filter = getFilter(
filterUploaded,
@ -593,8 +524,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
dispatch(listTypistsAsync());
@ -611,8 +540,6 @@ const DictationPage: React.FC = (): JSX.Element => {
isTypist,
sortDirection,
sortableParamName,
authorId,
fileName,
t,
]
);
@ -642,8 +569,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
}
@ -658,28 +583,9 @@ 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,
@ -722,8 +628,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction: sortDirection,
paramName: sortableParamName,
authorId,
fileName,
})
);
dispatch(listTypistsAsync());
@ -739,68 +643,10 @@ 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 () => {
@ -816,73 +662,21 @@ const DictationPage: React.FC = (): JSX.Element => {
dispatch(changeDisplayInfo({ column: displayInfo }));
// フィルター状態をローカルストレージから取得する
const filterValue = localStorage.getItem("filterCriteria");
const filter = getFilter(true, true, true, true, false);
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;
const { meta, payload } = await dispatch(getSortColumnAsync());
if (
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)
meta.requestStatus === "fulfilled" &&
payload &&
!("error" in payload)
) {
// ソート情報をローカルストレージから取得する
const sortColumnValue = localStorage.getItem("sortCriteria") ?? "";
let direction: DirectionType;
let paramName: SortableColumnType;
if (sortColumnValue === "") {
direction = sortCriteriaPayload.direction;
paramName = sortCriteriaPayload.paramName;
direction = payload.direction;
paramName = payload.paramName;
} else {
// ソート情報をDirectionとParamNameに分割する
const sortColumn = sortColumnValue?.split(",");
@ -893,18 +687,15 @@ const DictationPage: React.FC = (): JSX.Element => {
// 正常なソート情報がローカルストレージに存在する場合はローカルストレージの情報を使用する
direction = isDirectionType(localStorageDirection)
? localStorageDirection
: sortCriteriaPayload.direction;
: payload.direction;
paramName = isSortableColumnType(localStorageParamName)
? localStorageParamName
: sortCriteriaPayload.paramName;
: payload.paramName;
dispatch(changeDirection({ direction }));
dispatch(changeParamName({ paramName }));
}
}
// タスク一覧を取得する
if (isDirectionType(direction) && isSortableColumnType(paramName)) {
dispatch(
listTasksAsync({
limit: LIMIT_TASK_NUM,
@ -912,8 +703,6 @@ const DictationPage: React.FC = (): JSX.Element => {
filter,
direction,
paramName,
authorId: payloadAuthorId,
fileName: payloadFileName,
})
);
dispatch(listTypistsAsync());
@ -946,171 +735,125 @@ const DictationPage: React.FC = (): JSX.Element => {
<section className={styles.dictation}>
<div>
<DisPlayInfo />
<ul className={styles.menuAction}>
<ul className={styles.tableFilter}>
<li>{t(getTranslationID("dictationPage.label.filter"))}:</li>
<li>
<ul className={styles.tableFilter}>
<li>
{t(getTranslationID("dictationPage.label.filter"))}:
</li>
<li>
<label htmlFor="uploaded">
<input
id="uploaded"
type="checkbox"
value="flUploaded"
className={styles.formCheck}
checked={filterUploaded}
disabled={isLoading}
onChange={(e) => {
setFilterUploaded(e.target.checked);
updateFilter(
e.target.checked,
filterInProgress,
filterPending,
filterFinished,
filterBackup
);
}}
/>
{t(getTranslationID("dictationPage.label.uploaded"))}
</label>
</li>
<li>
<label htmlFor="inProgress">
<input
id="inProgress"
type="checkbox"
value="flInProgress"
className={styles.formCheck}
checked={filterInProgress}
disabled={isLoading}
onChange={(e) => {
setFilterInProgress(e.target.checked);
updateFilter(
filterUploaded,
e.target.checked,
filterPending,
filterFinished,
filterBackup
);
}}
/>
{t(
getTranslationID("dictationPage.label.inProgress")
)}
</label>
</li>
<li>
<label htmlFor="pending">
<input
id="pending"
type="checkbox"
value="flPending"
className={styles.formCheck}
checked={filterPending}
disabled={isLoading}
onChange={(e) => {
setFilterPending(e.target.checked);
updateFilter(
filterUploaded,
filterInProgress,
e.target.checked,
filterFinished,
filterBackup
);
}}
/>
{t(getTranslationID("dictationPage.label.pending"))}
</label>
</li>
<li>
<label htmlFor="finished">
<input
id="finished"
type="checkbox"
value="flFinished"
className={styles.formCheck}
checked={filterFinished}
disabled={isLoading}
onChange={(e) => {
setFilterFinished(e.target.checked);
updateFilter(
filterUploaded,
filterInProgress,
filterPending,
e.target.checked,
filterBackup
);
}}
/>
{t(getTranslationID("dictationPage.label.finished"))}
</label>
</li>
<li>
<label htmlFor="backup">
<input
id="backup"
type="checkbox"
value="flBackup"
className={styles.formCheck}
checked={filterBackup}
disabled={isLoading}
onChange={(e) => {
setFilterBackup(e.target.checked);
updateFilter(
filterUploaded,
filterInProgress,
filterPending,
filterFinished,
e.target.checked
);
}}
/>
{t(getTranslationID("dictationPage.label.backup"))}
</label>
</li>
</ul>
<label htmlFor="uploaded">
<input
id="uploaded"
type="checkbox"
value="flUploaded"
className={styles.formCheck}
checked={filterUploaded}
disabled={isLoading}
onChange={(e) => {
setFilterUploaded(e.target.checked);
updateFilter(
e.target.checked,
filterInProgress,
filterPending,
filterFinished,
filterBackup
);
}}
/>
{t(getTranslationID("dictationPage.label.uploaded"))}
</label>
</li>
<li className={styles.floatRight}>
<form
className={styles.searchBar}
onSubmit={(e) => requestSearch(e)}
>
<li>
<label htmlFor="inProgress">
<input
type="text"
placeholder={t(
getTranslationID("dictationPage.label.fileName")
)}
value={filterConditionFileName}
onChange={(e) => onChangeFilterConditionFileName(e)}
className={styles.searchInput}
id="inProgress"
type="checkbox"
value="flInProgress"
className={styles.formCheck}
checked={filterInProgress}
disabled={isLoading}
onChange={(e) => {
setFilterInProgress(e.target.checked);
updateFilter(
filterUploaded,
e.target.checked,
filterPending,
filterFinished,
filterBackup
);
}}
/>
{t(getTranslationID("dictationPage.label.inProgress"))}
</label>
</li>
<li>
<label htmlFor="pending">
<input
type="text"
placeholder={t(
getTranslationID("dictationPage.label.authorId")
)}
value={filterConditionAuthorId}
onChange={(e) => onChangeFilterConditionAuthorId(e)}
className={styles.searchInput}
id="pending"
type="checkbox"
value="flPending"
className={styles.formCheck}
checked={filterPending}
disabled={isLoading}
onChange={(e) => {
setFilterPending(e.target.checked);
updateFilter(
filterUploaded,
filterInProgress,
e.target.checked,
filterFinished,
filterBackup
);
}}
/>
{/* 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>
{t(getTranslationID("dictationPage.label.pending"))}
</label>
</li>
<li>
<label htmlFor="finished">
<input
id="finished"
type="checkbox"
value="flFinished"
className={styles.formCheck}
checked={filterFinished}
disabled={isLoading}
onChange={(e) => {
setFilterFinished(e.target.checked);
updateFilter(
filterUploaded,
filterInProgress,
filterPending,
e.target.checked,
filterBackup
);
}}
/>
{t(getTranslationID("dictationPage.label.finished"))}
</label>
</li>
<li>
<label htmlFor="backup">
<input
id="backup"
type="checkbox"
value="flBackup"
className={styles.formCheck}
checked={filterBackup}
disabled={isLoading}
onChange={(e) => {
setFilterBackup(e.target.checked);
updateFilter(
filterUploaded,
filterInProgress,
filterPending,
filterFinished,
e.target.checked
);
}}
/>
{t(getTranslationID("dictationPage.label.backup"))}
</label>
</li>
</ul>
<div className={styles.tableWrap}>
<table className={`${styles.table} ${styles.dictation}`}>
<tr className={styles.tableHeader}>
@ -1537,7 +1280,7 @@ const DictationPage: React.FC = (): JSX.Element => {
<a
className={
x.status !== STATUS.UPLOADED ||
!(isAdmin || isAuthor)
!(isAdmin || isAuthor)
? styles.isDisable
: ""
}
@ -1558,7 +1301,7 @@ const DictationPage: React.FC = (): JSX.Element => {
className={
(x.status === STATUS.INPROGRESS ||
x.status === STATUS.PENDING) &&
(isAdmin || isTypist)
(isAdmin || isTypist)
? ""
: styles.isDisable
}
@ -1579,7 +1322,7 @@ const DictationPage: React.FC = (): JSX.Element => {
<a
className={
x.status === STATUS.FINISHED &&
(isAdmin || isTypist)
(isAdmin || isTypist)
? ""
: styles.isDisable
}
@ -1600,8 +1343,8 @@ const DictationPage: React.FC = (): JSX.Element => {
// タスクのステータスがInprogressまたはPending以外の場合、削除ボタンを活性化する
className={
isDeletableRole &&
x.status !== STATUS.INPROGRESS &&
x.status !== STATUS.PENDING
x.status !== STATUS.INPROGRESS &&
x.status !== STATUS.PENDING
? ""
: styles.isDisable
}
@ -1797,16 +1540,18 @@ 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}
>
«
</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}
>
@ -1814,20 +1559,22 @@ 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
: ""
}`}
}`}
onClick={getNextPage}
>
</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
: ""
}`}
}`}
onClick={getLastPage}
>
»
@ -1855,8 +1602,9 @@ 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} />
{t(getTranslationID("dictationPage.label.fileBackup"))}

View File

@ -12,7 +12,6 @@ import { useDispatch, useSelector } from "react-redux";
import {
LIMIT_ORDER_HISORY_NUM,
STATUS,
LICENSE_TYPE,
getLicenseOrderHistoriesAsync,
selectCurrentPage,
selectIsLoading,
@ -26,21 +25,20 @@ 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, selectedRow } = props;
const { onReturn } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const total = useSelector(selectTotal);
@ -48,6 +46,7 @@ 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);
@ -65,7 +64,6 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
getLicenseOrderHistoriesAsync({
limit: LIMIT_ORDER_HISORY_NUM,
offset,
selectedRow,
})
);
};
@ -153,15 +151,11 @@ 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 : ""}`}
@ -214,11 +208,6 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
getTranslationID("orderHistoriesPage.label.issueDate")
)}
</th>
<th>
{t(
getTranslationID("orderHistoriesPage.label.licenseType")
)}
</th>
<th>
{t(
getTranslationID(
@ -240,10 +229,9 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
// eslint-disable-next-line react/jsx-key
<tr>
<td>{x.orderDate}</td>
<td>{x.issueDate ?? "-"}</td>
<td>{x.type}</td>
<td>{x.issueDate ? x.issueDate : "-"}</td>
<td>{x.numberOfOrder}</td>
<td>{x.poNumber ?? "-"}</td>
<td>{x.poNumber}</td>
<td>
{(() => {
switch (x.status) {
@ -271,7 +259,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
})()}
</td>
<td>
{!selectedRow && isNotTrialLicense(x) && (
{!selectedRow && (
<ul
className={`${styles.menuAction} ${styles.inTable}`}
>
@ -296,7 +284,7 @@ export const LicenseOrderHistory: React.FC<LicenseOrderHistoryProps> = (
</li>
</ul>
)}
{selectedRow && isNotTrialLicense(x) && (
{selectedRow && (
<ul
className={`${styles.menuAction} ${styles.inTable}`}
>

View File

@ -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,20 +27,19 @@ 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, selectedRow } = props;
const { onReturn } = props;
const dispatch: AppDispatch = useDispatch();
const [t] = useTranslation();
const selectedRow = useSelector(selectSelectedRow);
// 代行操作用のトークンを取得する
const delegationAccessToken = useSelector(selectDelegationAccessToken);
@ -50,8 +49,6 @@ 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);
@ -61,10 +58,6 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
setIsCardLicenseActivatePopupOpen(true);
}, [setIsCardLicenseActivatePopupOpen]);
const onTrialLicenseIssueOpen = useCallback(() => {
setIsTrialLicenseIssuePopupOpen(true);
}, [setIsTrialLicenseIssuePopupOpen]);
// 呼び出し画面制御関係
const [islicenseOrderHistoryOpen, setIsLicenseOrderHistoryOpen] =
useState(false);
@ -78,7 +71,6 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
const companyName = useSelector(selectCompanyName);
const isTier1 = isApproveTier([TIERS.TIER1]);
const isTier2 = isApproveTier([TIERS.TIER2]);
const isAdmin = isAdminUser();
useEffect(() => {
@ -141,21 +133,11 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
}}
/>
)}
{isTrialLicenseIssuePopupOpen && (
<TrialLicenseIssuePopup
onClose={() => {
setIsTrialLicenseIssuePopupOpen(false);
dispatch(getLicenseSummaryAsync({ selectedRow }));
}}
selectedRow={selectedRow}
/>
)}
{islicenseOrderHistoryOpen && (
<LicenseOrderHistory
onReturn={() => {
setIsLicenseOrderHistoryOpen(false);
}}
selectedRow={selectedRow}
/>
)}
{!islicenseOrderHistoryOpen && (
@ -246,30 +228,6 @@ 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

View File

@ -1,57 +1,43 @@
import { PartnerLicenseInfo } from "api";
import { AppDispatch } from "app/store";
import React, { useCallback, useState, useEffect } from "react";
import Footer from "components/footer";
import Header from "components/header";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import styles from "styles/app.module.scss";
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch } from "app/store";
import { getTranslationID } from "translation";
import changeOwnerIcon from "../../assets/images/change_circle.svg";
import history from "../../assets/images/history.svg";
import { useTranslation } from "react-i18next";
import { PartnerLicenseInfo } from "api";
import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup";
import postAdd from "../../assets/images/post_add.svg";
import progress_activit from "../../assets/images/progress_activit.svg";
import history from "../../assets/images/history.svg";
import returnLabel from "../../assets/images/undo.svg";
import searchIcon from "../../assets/images/search.svg";
import { TIERS } from "../../components/auth/constants";
import changeOwnerIcon from "../../assets/images/change_circle.svg";
import { isApproveTier } from "../../features/auth/utils";
import { TIERS } from "../../components/auth/constants";
import {
ACCOUNTS_VIEW_LIMIT,
changeSelectedRow,
getMyAccountAsync,
getPartnerLicenseAsync,
popHierarchicalElement,
ACCOUNTS_VIEW_LIMIT,
selectMyAccountInfo,
selectTotal,
selectOwnPartnerLicense,
selectChildrenPartnerLicenses,
selectHierarchicalElements,
selectTotalPage,
selectIsLoading,
selectOffset,
selectCurrentPage,
pushHierarchicalElement,
popHierarchicalElement,
spliceHierarchicalElement,
savePageInfo,
setIsLicenseOrderHistoryOpen,
setIsViewDetailsOpen,
selectChildrenPartnerLicenses,
selectCurrentPage,
selectHierarchicalElements,
selectIsLoading,
selectMyAccountInfo,
selectOffset,
selectOwnPartnerLicense,
selectTotal,
selectTotalPage,
selectSelectedRow,
selectIsLicenseOrderHistoryOpen,
selectIsViewDetailsOpen,
setIsSearchPopupOpen,
selectIsSearchPopupOpen,
getMyAccountAsync,
changeSelectedRow,
} from "../../features/license/partnerLicense";
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 { LicenseOrderHistory } from "./licenseOrderHistory";
import { LicenseSummary } from "./licenseSummary";
import { SearchPartnerPopup } from "./searchPartnerAccountPopup";
import progress_activit from "../../assets/images/progress_activit.svg";
import ChangeOwnerPopup from "./changeOwnerPopup";
const PartnerLicense: React.FC = (): JSX.Element => {
const dispatch: AppDispatch = useDispatch();
@ -61,21 +47,9 @@ const PartnerLicense: React.FC = (): JSX.Element => {
const [isCardLicenseIssuePopupOpen, setIsCardLicenseIssuePopupOpen] =
useState(false);
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = 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 [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] =
useState(false);
const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false);
const [isChangeOwnerPopupOpen, setIsChangeOwnerPopupOpen] = useState(false);
// 階層表示用
@ -110,7 +84,6 @@ const PartnerLicense: React.FC = (): JSX.Element => {
);
const hierarchicalElements = useSelector(selectHierarchicalElements);
const isLoading = useSelector(selectIsLoading);
const selectedRow = useSelector(selectSelectedRow) as PartnerLicenseInfo;
// ページネーション制御用
const currentPage = useSelector(selectCurrentPage);
@ -163,18 +136,18 @@ const PartnerLicense: React.FC = (): JSX.Element => {
const onClickViewDetails = useCallback(
(value?: PartnerLicenseInfo) => {
dispatch(changeSelectedRow({ value }));
dispatch(setIsViewDetailsOpen({ value: true }));
setIsViewDetailsOpen(true);
},
[dispatch]
[dispatch, setIsViewDetailsOpen]
);
// orderHistoryボタン押下時
const onClickOrderHistory = useCallback(
(value?: PartnerLicenseInfo) => {
dispatch(changeSelectedRow({ value }));
dispatch(setIsLicenseOrderHistoryOpen({ value: true }));
setIslicenseOrderHistoryOpen(true);
},
[dispatch]
[dispatch, setIslicenseOrderHistoryOpen]
);
// changeOwnerボタン押下時
@ -182,10 +155,6 @@ const PartnerLicense: React.FC = (): JSX.Element => {
setIsChangeOwnerPopupOpen(true);
}, [setIsChangeOwnerPopupOpen]);
const onOpenSearchPopup = useCallback(() => {
dispatch(setIsSearchPopupOpen({ value: true }));
}, [dispatch]);
// マウント時のみ実行
useEffect(() => {
dispatch(getMyAccountAsync());
@ -207,8 +176,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
}, [myAccountInfo]);
// 現在の表示階層に合わせたボタン制御用
const [showOrderHistoryButton, setShowOrderHistoryButton] = useState(false);
const [showViewDetailsButton, setShowViewDetailsButton] = useState(false);
const [buttonLabel, setButtonLabel] = useState("");
// パンくずリスト用stateに自アカウントを追加
useEffect(() => {
@ -226,17 +194,15 @@ const PartnerLicense: React.FC = (): JSX.Element => {
);
}
// 表内のボタン表示判定
if (ownPartnerLicenseInfo.tier !== 4) {
setShowOrderHistoryButton(true);
setShowViewDetailsButton(false);
if (hierarchicalElements.length === 1 && ownPartnerLicenseInfo.tier !== 4) {
setButtonLabel(
t(getTranslationID("partnerLicense.label.orderHistoryButton"))
);
} else if (ownPartnerLicenseInfo.tier === 4) {
setShowOrderHistoryButton(true);
setShowViewDetailsButton(true);
setButtonLabel(t(getTranslationID("partnerLicense.label.viewDetails")));
} else {
setShowOrderHistoryButton(false);
setShowViewDetailsButton(false);
setButtonLabel("");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ownPartnerLicenseInfo]);
@ -255,21 +221,6 @@ 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を生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */}
@ -287,20 +238,18 @@ const PartnerLicense: React.FC = (): JSX.Element => {
}}
/>
)}
{isLicenseOrderHistoryOpen && (
{islicenseOrderHistoryOpen && (
<LicenseOrderHistory
onReturn={() => {
dispatch(setIsLicenseOrderHistoryOpen({ value: false }));
setIslicenseOrderHistoryOpen(false);
}}
selectedRow={selectedRow}
/>
)}
{isViewDetailsOpen && (
<LicenseSummary
onReturn={() => {
dispatch(setIsViewDetailsOpen({ value: false }));
setIsViewDetailsOpen(false);
}}
selectedRow={selectedRow}
/>
)}
{isChangeOwnerPopupOpen && (
@ -310,12 +259,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
}}
/>
)}
{isVisiblePartnerSearch() && isSearchPopupOpen && (
<SearchPartnerPopup
onClose={() => dispatch(setIsSearchPopupOpen({ value: true }))}
/>
)}
{isVisiblePartnerLicensePage && (
{!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
<div className={styles.wrap}>
<Header />
<main className={styles.main}>
@ -418,22 +362,6 @@ 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) => (
@ -460,13 +388,6 @@ 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")
@ -492,13 +413,12 @@ const PartnerLicense: React.FC = (): JSX.Element => {
? ownPartnerLicenseInfo.stockLicense
: "-"}
</td>
<td>-</td>
<td>{ownPartnerLicenseInfo.issuedRequested}</td>
<td>
<span
className={
ownPartnerLicenseInfo.shortage > 0 &&
ownPartnerLicenseInfo.tier !== 1
ownPartnerLicenseInfo.tier !== 1
? styles.isAlert
: ""
}
@ -536,7 +456,6 @@ 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
@ -553,38 +472,20 @@ 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} ${showOrderHistoryButton ? styles.isActive : ""
}`}
className={`${styles.menuLink} ${
buttonLabel ? styles.isActive : ""
}`}
onClick={() => {
onClickOrderHistory(value);
if (ownPartnerLicenseInfo.tier === 4) {
onClickViewDetails(value);
} else {
onClickOrderHistory(value);
}
}}
>
{t(
getTranslationID(
"partnerLicense.label.orderHistoryButton"
)
)}
{buttonLabel}
</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>
@ -612,8 +513,9 @@ const PartnerLicense: React.FC = (): JSX.Element => {
onClick={() => {
movePage(0);
}}
className={` ${!isLoading && currentPage !== 1 ? styles.isActive : ""
}`}
className={` ${
!isLoading && currentPage !== 1 ? styles.isActive : ""
}`}
>
«
</a>
@ -622,22 +524,25 @@ 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
: ""
}`}
}`}
>
</a>
@ -646,10 +551,11 @@ const PartnerLicense: React.FC = (): JSX.Element => {
onClick={() => {
movePage((totalPage - 1) * ACCOUNTS_VIEW_LIMIT);
}}
className={` ${!isLoading && currentPage < totalPage
className={` ${
!isLoading && currentPage < totalPage
? styles.isActive
: ""
}`}
}`}
>
»
</a>
@ -678,8 +584,4 @@ 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;

View File

@ -1,354 +0,0 @@
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>
);
};

View File

@ -1,123 +0,0 @@
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>
);
};

View File

@ -8,12 +8,7 @@ import Footer from "components/footer";
const SupportPage: React.FC = () => {
const { t } = useTranslation();
// OMDS_IS-381 Support画面で表示する内容を充実したいの対応 2024年8月7日
const userGuideDivStyles: React.CSSProperties = {
padding: "2rem 2rem 4rem 2rem",
};
const appDLDivStyles: React.CSSProperties = {
padding: "2rem 2rem 0rem 2rem",
};
const customDivStyles: React.CSSProperties = { padding: "2rem" };
const customUlStyles: React.CSSProperties = { marginBottom: "1rem" };
return (
@ -31,7 +26,7 @@ const SupportPage: React.FC = () => {
<div>
<h2>{t(getTranslationID("supportPage.label.howToUse"))}</h2>
<div className={styles.txContents} style={userGuideDivStyles}>
<div className={styles.txContents} style={customDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
@ -51,7 +46,7 @@ const SupportPage: React.FC = () => {
<h2>
{t(getTranslationID("supportPage.label.programDownload"))}
</h2>
<div className={styles.txContents} style={appDLDivStyles}>
<div className={styles.txContents} style={customDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
@ -76,7 +71,7 @@ const SupportPage: React.FC = () => {
)}
</p>
</div>
<div className={styles.txContents} style={appDLDivStyles}>
<div className={styles.txContents} style={customDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
@ -97,56 +92,6 @@ const SupportPage: React.FC = () => {
)}
</p>
</div>
<div className={styles.txContents} style={appDLDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
href="https://download.omsystem.com/pages/odms_download/odms_cloud_backup_extraction_tool/en/"
target="_blank"
className={styles.linkTx}
rel="noreferrer"
>
{t(
getTranslationID(
"supportPage.label.backupExtractionToolDownloadLink"
)
)}
</a>
</li>
</ul>
<p className={styles.txNormal}>
{t(
getTranslationID(
"supportPage.label.backupExtractionToolDownloadLinkDescription"
)
)}
</p>
</div>
<div className={styles.txContents} style={appDLDivStyles}>
<ul className={styles.listDocument} style={customUlStyles}>
<li>
<a
href="https://download.omsystem.com/pages/odms_download/odms_client_virtual_driver/en/"
target="_blank"
className={styles.linkTx}
rel="noreferrer"
>
{t(
getTranslationID(
"supportPage.label.virtualDriverDownloadLink"
)
)}
</a>
</li>
</ul>
<p className={styles.txNormal}>
{t(
getTranslationID(
"supportPage.label.virtualDriverDownloadLinkDescription"
)
)}
</p>
</div>
</div>
</section>
</div>

View File

@ -28,13 +28,12 @@ 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, clearUserSearchInputs } = props;
const { isOpen, onClose } = props;
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
@ -88,7 +87,6 @@ export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
if (meta.requestStatus === "fulfilled") {
closePopup();
clearUserSearchInputs();
dispatch(listUsersAsync());
}
}, [dispatch, closePopup, id, selectedlicenseId, hasErrorEmptyLicense]);
@ -221,7 +219,8 @@ export const AllocateLicensePopup: React.FC<AllocateLicensePopupProps> = (
value={selectedlicenseId ?? ""}
>
<option value="" hidden>
{`-- ${t(
{`--
${t(
getTranslationID(
"allocateLicensePopupPage.label.dropDownHeading"
)

View File

@ -10,7 +10,6 @@ import {
selectIsLoading,
deallocateLicenseAsync,
deleteUserAsync,
confirmUserForceAsync,
} from "features/user";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
@ -33,7 +32,6 @@ 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";
@ -50,8 +48,6 @@ 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);
@ -88,7 +84,6 @@ const UserListPage: React.FC = (): JSX.Element => {
const { meta } = await dispatch(deallocateLicenseAsync({ userId }));
if (meta.requestStatus === "fulfilled") {
clearUserSearchInputs();
dispatch(listUsersAsync());
}
},
@ -107,53 +102,12 @@ 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());
@ -172,21 +126,18 @@ 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}
@ -234,50 +185,6 @@ 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}`}>
@ -401,23 +308,6 @@ 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>

View File

@ -28,11 +28,10 @@ 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, clearUserSearchInputs } = props;
const { isOpen, onClose } = props;
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
const [isPasswordHide, setIsPasswordHide] = useState<boolean>(true);
@ -76,7 +75,6 @@ export const UserAddPopup: React.FC<UserAddPopupProps> = (props) => {
if (meta.requestStatus === "fulfilled") {
closePopup();
clearUserSearchInputs();
dispatch(listUsersAsync());
}
}, [

View File

@ -28,11 +28,10 @@ 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, clearUserSearchInputs } = props;
const { isOpen, onClose } = props;
const dispatch: AppDispatch = useDispatch();
const { t } = useTranslation();
const closePopup = useCallback(() => {
@ -80,7 +79,6 @@ export const UserUpdatePopup: React.FC<UserUpdatePopupProps> = (props) => {
if (meta.requestStatus === "fulfilled") {
closePopup();
clearUserSearchInputs();
dispatch(listUsersAsync());
}
}, [

View File

@ -454,6 +454,7 @@ h3 + .brCrumb .tlIcon {
.brCrumbLicense li a:hover {
color: #0084b2;
}
.buttonNormal {
display: inline-block;
width: 15rem;
@ -1698,9 +1699,9 @@ _:-ms-lang(x)::-ms-backdrop,
margin-left: 648px;
text-align: right;
}
.menuAction {
margin-bottom: 0.6rem;
position: relative;
}
.menuAction li {
display: inline-block;
@ -2058,9 +2059,6 @@ 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;
@ -2767,106 +2765,4 @@ 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 */

View File

@ -233,13 +233,5 @@ 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;

View File

@ -54,7 +54,7 @@
},
"text": {
"maintenanceNotificationTitle": "Hinweis auf geplante Wartungsarbeiten",
"maintenanceNotification": "Aufgrund von Systemwartungsarbeiten wird ODMS Cloud ab dem 27. Januar, 6:00 Uhr UTC-Zeit, etwa eine Stunde lang nicht verfügbar sein. Wir entschuldigen uns für etwaige Unannehmlichkeiten, die während der Wartung entstanden sind."
"maintenanceNotification": "Aufgrund von Systemwartungsarbeiten wird ODMS Cloud ab dem 17. Juni, 6:00 Uhr UTC-Zeit, etwa eine Stunde lang nicht verfügbar sein. Wir entschuldigen uns für etwaige Unannehmlichkeiten, die während der Wartung entstanden sind."
}
},
"signupPage": {
@ -146,9 +146,7 @@
"duplicateEmailError": "Die E-Mail-Adressen in den folgenden Zeilen werden in der CSV-Datei dupliziert.",
"duplicateAuthorIdError": "Die Autoren-ID in der folgenden Zeile wird in der CSV-Datei dupliziert.",
"overMaxUserError": "Durch die Benutzerregistrierung per CSV-Datei können bis zu 100 Benutzer gleichzeitig registriert werden.",
"invalidInputError": "Die Benutzerinformationen in der folgenden Zeile entsprechen nicht den Eingaberegeln.",
"forceEmailVerificationConfirm": "Möchten Sie die E-Mail dieses Benutzers zwangsweise verifizieren?",
"alreadyEmailVerifiedError": "Die Benutzer-E-Mail wurde bereits verifiziert."
"invalidInputError": "Die Benutzerinformationen in der folgenden Zeile entsprechen nicht den Eingaberegeln."
},
"label": {
"title": "Benutzer",
@ -195,9 +193,7 @@
"encryptionLabel": "Verschlüsselung",
"encryptionPasswordLabel": "Verschlüsselungspasswort",
"promptLabel": "Eingabeaufforderung",
"addUsers": "Benutzer hinzufügen",
"forceEmailVerification": "E-Mail-Verifizierung erzwingen",
"search": "Suche"
"addUsers": "Benutzer hinzufügen"
},
"text": {
"downloadExplain": "Bitte laden Sie die CSV-Beispieldatei herunter und geben Sie die erforderlichen Informationen gemäß den folgenden Regeln ein.",
@ -230,8 +226,7 @@
"storageAvailable": "Speicher nicht verfügbar (Menge überschritten)",
"licenseLabel": "Lizenz",
"storageLabel": "Lagerung",
"storageUnavailableCheckbox": "Beschränken Sie die Kontonutzung",
"issueTrialLicense": "Testlizenz ausstellen"
"storageUnavailableCheckbox": "Beschränken Sie die Kontonutzung"
},
"message": {
"storageUnavalableSwitchingConfirm": "Sind Sie sicher, dass Sie den Speichernutzungsstatus für dieses Konto ändern möchten?"
@ -317,8 +312,7 @@
"cancelDictation": "Transkription abbrechen",
"rawFileName": "Ursprünglicher Dateiname",
"fileNameSave": "Führen Sie eine Dateiumbenennung durch",
"reopenDictation": "Status auf „Ausstehend“ ändern",
"search": "Suche"
"reopenDictation": "Status auf „Ausstehend“ ändern"
}
},
"cardLicenseIssuePopupPage": {
@ -388,9 +382,7 @@
"issueRequesting": "Lizenzen auf Bestellung",
"viewDetails": "Details anzeigen",
"accounts": "konten",
"changeOwnerButton": "Change Owner",
"allocatedLicense": "Zugewiesene Lizenzen",
"search": "Suche"
"changeOwnerButton": "Change Owner"
}
},
"orderHistoriesPage": {
@ -408,8 +400,7 @@
"issue": "Ausgabe",
"issueCancel": "Lizenzen kündigen",
"orderCancel": "Bestellung stornieren",
"histories": "geschichten",
"licenseType": "Lizenztyp"
"histories": "geschichten"
},
"message": {
"notEnoughOfNumberOfLicense": "Lizenzen konnten aufgrund unzureichender Lizenzanzahl nicht ausgestellt werden. Bitte bestellen Sie zusätzliche Lizenzen.",
@ -633,11 +624,7 @@
"omdsDesktopAppDownloadLink": "ODMS Cloud Desktop App",
"omdsDesktopAppDownloadLinkDescription": "Klicken Sie hier, um zur Downloadseite der ODMS Cloud Desktop App zu gelangen.",
"dcpDownloadLink": "Device Configuration Program (DCP)",
"dcpDownloadLinkDescription": "Klicken Sie hier, um zur DCP-Downloadseite zu gelangen.",
"backupExtractionToolDownloadLink": "ODMS Cloud Backup Extraction Tool",
"backupExtractionToolDownloadLinkDescription": "Klicken Sie hier, um zur Downloadseite der ODMS Cloud Backup Extraction Tool zu gelangen.",
"virtualDriverDownloadLink": "ODMS Client Virtual Driver",
"virtualDriverDownloadLinkDescription": "Klicken Sie hier, um zur Downloadseite der ODMS Client Virtual Driver zu gelangen."
"dcpDownloadLinkDescription": "Klicken Sie hier, um zur DCP-Downloadseite zu gelangen."
},
"text": {
"notResolved": "Informationen zu den Funktionen der ODMS Cloud finden Sie im Benutzerhandbuch. Wenn Sie zusätzlichen Support benötigen, wenden Sie sich bitte an Ihren Administrator oder zertifizierten ODMS Cloud-Händler."
@ -673,22 +660,5 @@
"upperLayerId": "Upper Layer ID",
"lowerLayerId": "Lower Layer ID"
}
},
"trialLicenseIssuePopupPage": {
"label": {
"title": "Testlizenz ausstellen",
"subTitle": "Es werden Testlizenzen ausgestellt.",
"numberOfLicenses": "Anzahl der Lizenzen",
"expirationDate": "Verfallsdatum",
"issueButton": "Ausgabe"
},
"message": {
"accountNotSelected": "Konto ist nicht ausgewählt."
}
},
"searchPartnerAccountPopupPage": {
"label": {
"title": "Konto durchsuchen"
}
}
}

View File

@ -54,7 +54,7 @@
},
"text": {
"maintenanceNotificationTitle": "Notice of scheduled maintenance",
"maintenanceNotification": "Due to system maintenance, ODMS Cloud will be unavailable for approximately one hour starting from January 27th, 6:00AM UTC time. We apologize for any inconvenience caused during the maintenance."
"maintenanceNotification": "Due to system maintenance, ODMS Cloud will be unavailable for approximately one hour starting from June 17th, 6:00AM UTC time. We apologize for any inconvenience caused during the maintenance."
}
},
"signupPage": {
@ -146,9 +146,7 @@
"duplicateEmailError": "The email addresses in the following lines are duplicated in the CSV file.",
"duplicateAuthorIdError": "The Author ID in the following line is duplicated in the CSV file.",
"overMaxUserError": "Up to 100 users can be registered at one time by user registration via CSV file.",
"invalidInputError": "The user information in the following line does not comply with the input rules.",
"forceEmailVerificationConfirm": "Do you want to forcibly verify this user's email?",
"alreadyEmailVerifiedError": "This user's Email has already been verified."
"invalidInputError": "The user information in the following line does not comply with the input rules."
},
"label": {
"title": "User",
@ -195,9 +193,7 @@
"encryptionLabel": "Encryption",
"encryptionPasswordLabel": "Encryption Password",
"promptLabel": "Prompt",
"addUsers": "Add User",
"forceEmailVerification": "Force Email Verification",
"search": "Search"
"addUsers": "Add User"
},
"text": {
"downloadExplain": "Please download the sample CSV file and apply the required information according to the rules below.",
@ -230,8 +226,7 @@
"storageAvailable": "Storage Unavailable (Exceeded Amount)",
"licenseLabel": "License",
"storageLabel": "Storage",
"storageUnavailableCheckbox": "Restrict account usage",
"issueTrialLicense": "Issue Trial License"
"storageUnavailableCheckbox": "Restrict account usage"
},
"message": {
"storageUnavalableSwitchingConfirm": "Are you sure you would like to change the storage usage status for this account?"
@ -317,8 +312,7 @@
"cancelDictation": "Cancel Transcription",
"rawFileName": "Original File Name",
"fileNameSave": "Execute file rename",
"reopenDictation": "Change status to Pending",
"search": "Search"
"reopenDictation": "Change status to Pending"
}
},
"cardLicenseIssuePopupPage": {
@ -388,9 +382,7 @@
"issueRequesting": "Licenses on Order",
"viewDetails": "View Details",
"accounts": "accounts",
"changeOwnerButton": "Change Owner",
"allocatedLicense": "Allocated Licenses",
"search": "Search"
"changeOwnerButton": "Change Owner"
}
},
"orderHistoriesPage": {
@ -408,8 +400,7 @@
"issue": "Issue",
"issueCancel": "Cancel Licenses",
"orderCancel": "Cancel Order",
"histories": "histories",
"licenseType": "License Type"
"histories": "histories"
},
"message": {
"notEnoughOfNumberOfLicense": "Licenses could not be issued due to insufficient amount of licenses. Please order additional licenses.",
@ -633,11 +624,7 @@
"omdsDesktopAppDownloadLink": "ODMS Cloud Desktop App",
"omdsDesktopAppDownloadLinkDescription": "Click here to go to the ODMS Cloud Desktop App download page.",
"dcpDownloadLink": "Device Configuration Program (DCP)",
"dcpDownloadLinkDescription": "Click here to go to the DCP download page.",
"backupExtractionToolDownloadLink": "ODMS Cloud Backup Extraction Tool",
"backupExtractionToolDownloadLinkDescription": "Click here to go to the ODMS Cloud Backup Extraction Tool download page.",
"virtualDriverDownloadLink": "ODMS Client Virtual Driver",
"virtualDriverDownloadLinkDescription": "Click here to go to the ODMS Client Virtual Driver download page."
"dcpDownloadLinkDescription": "Click here to go to the DCP download page."
},
"text": {
"notResolved": "Please refer to the User Guide for information about the features of the ODMS Cloud. If you require additional support, please contact your administrator or certified ODMS Cloud reseller."
@ -673,22 +660,5 @@
"upperLayerId": "Upper Layer ID",
"lowerLayerId": "Lower Layer ID"
}
},
"trialLicenseIssuePopupPage": {
"label": {
"title": "Issue Trial License",
"subTitle": "Trial licenses will be issued.",
"numberOfLicenses": "Number of licenses",
"expirationDate": "Expiration Date",
"issueButton": "Issue"
},
"message": {
"accountNotSelected": "Account is not selected."
}
},
"searchPartnerAccountPopupPage": {
"label": {
"title": "Search Account"
}
}
}

View File

@ -54,7 +54,7 @@
},
"text": {
"maintenanceNotificationTitle": "Aviso de mantenimiento programado",
"maintenanceNotification": "Debido al mantenimiento del sistema, ODMS Cloud no estará disponible durante aproximadamente una hora a partir del 27 de enero a las 6:00 am, hora UTC. Pedimos disculpas por cualquier inconveniente causado durante el mantenimiento."
"maintenanceNotification": "Debido al mantenimiento del sistema, ODMS Cloud no estará disponible durante aproximadamente una hora a partir del 17 de junio a las 6:00 am, hora UTC. Pedimos disculpas por cualquier inconveniente causado durante el mantenimiento."
}
},
"signupPage": {
@ -146,9 +146,7 @@
"duplicateEmailError": "Las direcciones de correo electrónico de las siguientes líneas están duplicadas en el archivo CSV.",
"duplicateAuthorIdError": "El ID del autor en la siguiente línea está duplicado en el archivo CSV.",
"overMaxUserError": "Se pueden registrar hasta 100 usuarios a la vez mediante el registro de usuario mediante un archivo CSV.",
"invalidInputError": "La información del usuario en la siguiente línea no cumple con las reglas de entrada.",
"forceEmailVerificationConfirm": "¿Quieres verificar forzosamente el correo electrónico de este usuario?",
"alreadyEmailVerifiedError": "El correo electrónico de este usuario ya ha sido verificado."
"invalidInputError": "La información del usuario en la siguiente línea no cumple con las reglas de entrada."
},
"label": {
"title": "Usuario",
@ -195,9 +193,7 @@
"encryptionLabel": "Cifrado ",
"encryptionPasswordLabel": "Contraseña de cifrado",
"promptLabel": "Solicitar",
"addUsers": "Agregar usuario",
"forceEmailVerification": "Verificación forzada de correo electrónico",
"search": "Búsqueda"
"addUsers": "Agregar usuario"
},
"text": {
"downloadExplain": "Descargue el archivo CSV de muestra y aplique la información requerida de acuerdo con las reglas siguientes.",
@ -230,8 +226,7 @@
"storageAvailable": "Almacenamiento no disponible (cantidad excedida)",
"licenseLabel": "Licencia",
"storageLabel": "Almacenamiento",
"storageUnavailableCheckbox": "Restringir el uso de la cuenta",
"issueTrialLicense": "Emitir licencia de prueba"
"storageUnavailableCheckbox": "Restringir el uso de la cuenta"
},
"message": {
"storageUnavalableSwitchingConfirm": "¿Está seguro de que desea cambiar el estado de uso del almacenamiento de esta cuenta?"
@ -317,8 +312,7 @@
"cancelDictation": "Cancelar transcripción",
"rawFileName": "Nombre de archivo original",
"fileNameSave": "Ejecutar cambio de nombre de archivo",
"reopenDictation": "Cambiar el estado a Pendiente",
"search": "Búsqueda"
"reopenDictation": "Cambiar el estado a Pendiente"
}
},
"cardLicenseIssuePopupPage": {
@ -388,9 +382,7 @@
"issueRequesting": "Licencias en Pedido",
"viewDetails": "Ver detalles",
"accounts": "cuentas",
"changeOwnerButton": "Change Owner",
"allocatedLicense": "Licencias asignadas",
"search": "Búsqueda"
"changeOwnerButton": "Change Owner"
}
},
"orderHistoriesPage": {
@ -408,8 +400,7 @@
"issue": "Emitida",
"issueCancel": "Cancelar licencias",
"orderCancel": "Cancelar pedido",
"histories": "historias",
"licenseType": "Tipo de licencia"
"histories": "historias"
},
"message": {
"notEnoughOfNumberOfLicense": "No se pudieron emitir licencias debido a una cantidad insuficiente de licencias. Solicite licencias adicionales.",
@ -631,13 +622,9 @@
"supportPageLink": "Guía del usuario de la nube OMDS",
"programDownload": "Descarga del programa",
"omdsDesktopAppDownloadLink": "ODMS Cloud Desktop App",
"omdsDesktopAppDownloadLinkDescription": "Haga clic aquí para ir a la página de descarga de la ODMS Cloud Desktop App.",
"omdsDesktopAppDownloadLinkDescription": "Haga clic aquí para ir a la página de descarga de la aplicación de escritorio ODMS Cloud.",
"dcpDownloadLink": "Device Configuration Program (DCP)",
"dcpDownloadLinkDescription": "Haga clic aquí para ir a la página de descarga de DCP.",
"backupExtractionToolDownloadLink": "ODMS Cloud Backup Extraction Tool",
"backupExtractionToolDownloadLinkDescription": "Haga clic aquí para ir a la página de descarga de la ODMS Cloud Backup Extraction Tool.",
"virtualDriverDownloadLink": "ODMS Client Virtual Driver",
"virtualDriverDownloadLinkDescription": "Haga clic aquí para ir a la página de descarga de la ODMS Client Virtual Driver."
"dcpDownloadLinkDescription": "Haga clic aquí para ir a la página de descarga de DCP."
},
"text": {
"notResolved": "Consulte la Guía del usuario para obtener información sobre las funciones de ODMS Cloud. Si necesita soporte adicional, comuníquese con su administrador o revendedor certificado de ODMS Cloud."
@ -673,22 +660,5 @@
"upperLayerId": "Upper Layer ID",
"lowerLayerId": "Lower Layer ID"
}
},
"trialLicenseIssuePopupPage": {
"label": {
"title": "Emitir licencia de prueba",
"subTitle": "Se expedirán licencias de prueba.",
"numberOfLicenses": "Número de licencias",
"expirationDate": "Fecha de caducidad",
"issueButton": "Emitida"
},
"message": {
"accountNotSelected": "La cuenta no está seleccionada."
}
},
"searchPartnerAccountPopupPage": {
"label": {
"title": "Buscar cuenta"
}
}
}

View File

@ -54,7 +54,7 @@
},
"text": {
"maintenanceNotificationTitle": "Avis de maintenance programmée",
"maintenanceNotification": "En raison de la maintenance du système, ODMS Cloud sera indisponible pendant environ une heure à partir du 27 janvier à 6h00, heure UTC. Nous nous excusons pour tout inconvénient causé lors de la maintenance."
"maintenanceNotification": "En raison de la maintenance du système, ODMS Cloud sera indisponible pendant environ une heure à partir du 17 juin à 6h00, heure UTC. Nous nous excusons pour tout inconvénient causé lors de la maintenance."
}
},
"signupPage": {
@ -146,9 +146,7 @@
"duplicateEmailError": "Les adresses email des lignes suivantes sont dupliquées dans le fichier CSV.",
"duplicateAuthorIdError": "L'ID d'auteur dans la ligne suivante est dupliqué dans le fichier CSV.",
"overMaxUserError": "Jusqu'à 100 utilisateurs peuvent être enregistrés en même temps par enregistrement d'utilisateur via un fichier CSV.",
"invalidInputError": "Les informations utilisateur de la ligne suivante ne sont pas conformes aux règles de saisie.",
"forceEmailVerificationConfirm": "Voulez-vous vérifier de force l'e-mail de cet utilisateur ?",
"alreadyEmailVerifiedError": "L'e-mail de cet utilisateur a déjà été vérifié."
"invalidInputError": "Les informations utilisateur de la ligne suivante ne sont pas conformes aux règles de saisie."
},
"label": {
"title": "Utilisateur",
@ -195,9 +193,7 @@
"encryptionLabel": "Chiffrement",
"encryptionPasswordLabel": "Mot de passe de chiffrement",
"promptLabel": "Invite",
"addUsers": "Ajouter un utilisateur",
"forceEmailVerification": "Forcer la vérification de l'e-mail",
"search": "Recherche"
"addUsers": "Ajouter un utilisateur"
},
"text": {
"downloadExplain": "Veuillez télécharger l'exemple de fichier CSV et appliquer les informations requises conformément aux règles ci-dessous.",
@ -230,8 +226,7 @@
"storageAvailable": "Stockage indisponible (montant dépassée)",
"licenseLabel": "Licence",
"storageLabel": "Stockage",
"storageUnavailableCheckbox": "Restreindre l'utilisation du compte",
"issueTrialLicense": "Délivrer licence d'essai"
"storageUnavailableCheckbox": "Restreindre l'utilisation du compte"
},
"message": {
"storageUnavalableSwitchingConfirm": "Êtes-vous sûr de vouloir modifier l'état d'utilisation du stockage pour ce compte ?"
@ -317,8 +312,7 @@
"cancelDictation": "Annuler la transcription",
"rawFileName": "Nom du fichier d'origine",
"fileNameSave": "Exécuter le changement de nom du fichier",
"reopenDictation": "Changer le statut en Suspendu",
"search": "Recherche"
"reopenDictation": "Changer le statut en Suspendu"
}
},
"cardLicenseIssuePopupPage": {
@ -388,9 +382,7 @@
"issueRequesting": "Licences en commande",
"viewDetails": "Voir les détails",
"accounts": "comptes",
"changeOwnerButton": "Change Owner",
"allocatedLicense": "Licences attribuées",
"search": "Recherche"
"changeOwnerButton": "Change Owner"
}
},
"orderHistoriesPage": {
@ -398,18 +390,17 @@
"title": "Licence",
"orderHistory": "Historique des commandes",
"orderDate": "Date de commande",
"issueDate": "Date de délivrance",
"issueDate": "Date de émission",
"numberOfOrder": "Nombre de licences commandées",
"poNumber": "Numéro de bon de commande",
"status": "État",
"issueRequesting": "Licences en commande",
"issued": "Licence délivrée",
"issued": "Licence issued",
"orderCanceled": "Commande annulée",
"issue": "Délivrer",
"issue": "Problème",
"issueCancel": "Annuler les licences",
"orderCancel": "Annuler commande",
"histories": "histoires",
"licenseType": "Type de licence"
"histories": "histoires"
},
"message": {
"notEnoughOfNumberOfLicense": "Les licences n'ont pas pu être délivrées en raison d'un nombre insuffisant de licences. Veuillez commander des licences supplémentaires.",
@ -631,13 +622,9 @@
"supportPageLink": "Guide de l'utilisateur du cloud OMDS",
"programDownload": "Télécharger le programme",
"omdsDesktopAppDownloadLink": "ODMS Cloud Desktop App",
"omdsDesktopAppDownloadLinkDescription": "Cliquez ici pour accéder à la page de téléchargement de ODMS Cloud Desktop App.",
"omdsDesktopAppDownloadLinkDescription": "Cliquez ici pour accéder à la page de téléchargement de l'application ODMS Cloud Desktop.",
"dcpDownloadLink": "Device Configuration Program (DCP)",
"dcpDownloadLinkDescription": "Cliquez ici pour accéder à la page de téléchargement du DCP.",
"backupExtractionToolDownloadLink": "ODMS Cloud Backup Extraction Tool",
"backupExtractionToolDownloadLinkDescription": "Cliquez ici pour accéder à la page de téléchargement de ODMS Cloud Backup Extraction Tool.",
"virtualDriverDownloadLink": "ODMS Client Virtual Driver",
"virtualDriverDownloadLinkDescription": "Cliquez ici pour accéder à la page de téléchargement de ODMS Client Virtual Driver."
"dcpDownloadLinkDescription": "Cliquez ici pour accéder à la page de téléchargement du DCP."
},
"text": {
"notResolved": "Veuillez vous référer au Guide de l'utilisateur pour plus d'informations sur les fonctionnalités d'ODMS Cloud. Si vous avez besoin d'une assistance supplémentaire, veuillez contacter votre administrateur ou votre revendeur certifié ODMS Cloud."
@ -673,22 +660,5 @@
"upperLayerId": "Upper Layer ID",
"lowerLayerId": "Lower Layer ID"
}
},
"trialLicenseIssuePopupPage": {
"label": {
"title": "Délivrer licence d'essai",
"subTitle": "Des licences dessai seront délivrées.",
"numberOfLicenses": "Nombre de licences",
"expirationDate": "Date d'expiration",
"issueButton": "Délivrer"
},
"message": {
"accountNotSelected": "Le compte n'est pas sélectionné."
}
},
"searchPartnerAccountPopupPage": {
"label": {
"title": "Rechercher un compte"
}
}
}

View File

@ -39,6 +39,9 @@ RUN mkdir -p /tmp/gotools \
&& mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ \
&& rm -rf /tmp/gotools
# Update NPM
RUN npm install -g npm
# 以下 ユーザー権限で実施
USER $USERNAME
# copy init-script

View File

@ -636,15 +636,15 @@ export async function sendMailWithU108(
"utf-8"
);
html = templateU108NoParentHtml
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
text = templateU108NoParentText
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
} else {
const templateU108Html = readFileSync(
path.resolve(__dirname, `../templates/template_U_108.html`),
@ -655,17 +655,17 @@ export async function sendMailWithU108(
"utf-8"
);
html = templateU108Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
text = templateU108Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
}
const uniqueCustomerAdminMails = [...new Set(customerAdminMails)];
const ccMails = uniqueCustomerAdminMails.includes(userMail) ? [] : [userMail];
@ -694,10 +694,3 @@ class autoAllocationList {
accountId: number;
userIds: number[];
}
/**
* $ $$
* @param str -
* @returns
*/
export const escapeDollar = (str: string): string => str.replace(/\$/g, "$$$$");

View File

@ -37,6 +37,9 @@ RUN mkdir -p /tmp/gotools \
&& mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ \
&& rm -rf /tmp/gotools
# Update NPM
RUN npm install -g npm
# Install NestJS
RUN npm i -g @nestjs/cli

View File

@ -1,5 +0,0 @@
-- +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`;

View File

@ -1,16 +0,0 @@
-- +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`;

View File

@ -1,9 +0,0 @@
-- +migrate Up
INSERT INTO task_filters (user_id)
SELECT
id AS user_id
FROM
users;
-- +migrate Down
TRUNCATE TABLE task_filters;

View File

@ -27,9 +27,7 @@
"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: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"
"migrate:up:test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test"
},
"dependencies": {
"@azure/identity": "^3.1.3",
@ -107,7 +105,6 @@
"json",
"ts"
],
"testTimeout": 120000,
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {

View File

@ -1417,119 +1417,6 @@
"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",
@ -2081,76 +1968,11 @@
"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": [
{
"name": "userName",
"required": false,
"in": "query",
"schema": { "type": "string" }
},
{
"name": "email",
"required": false,
"in": "query",
"schema": { "type": "string" }
}
],
"parameters": [],
"responses": {
"200": {
"description": "成功時のレスポンス",
@ -2362,106 +2184,6 @@
"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",
@ -3274,20 +2996,6 @@
"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": {
@ -4071,62 +3779,6 @@
"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",
@ -4881,10 +4533,6 @@
"type": "number",
"description": "不足数({Stock license} - {Issue Requested}"
},
"allocatedLicense": {
"type": "number",
"description": "有効期限内の割り当て済み総ライセンス数"
},
"issueRequesting": {
"type": "number",
"description": "未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数"
@ -4897,7 +4545,6 @@
"stockLicense",
"issuedRequested",
"shortage",
"allocatedLicense",
"issueRequesting"
]
},
@ -4931,10 +4578,9 @@
"issueDate": { "type": "string", "description": "発行日付" },
"numberOfOrder": { "type": "number", "description": "注文数" },
"poNumber": { "type": "string", "description": "POナンバー" },
"status": { "type": "string", "description": "注文状態" },
"type": { "type": "string", "description": "ライセンス種別" }
"status": { "type": "string", "description": "注文状態" }
},
"required": ["orderDate", "numberOfOrder", "poNumber", "status", "type"]
"required": ["orderDate", "numberOfOrder", "poNumber", "status"]
},
"GetOrderHistoriesResponse": {
"type": "object",
@ -5147,60 +4793,6 @@
},
"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": {
@ -5366,11 +4958,6 @@
"required": ["token"]
},
"ConfirmResponse": { "type": "object", "properties": {} },
"ConfirmForceRequest": {
"type": "object",
"properties": { "userId": { "type": "number" } },
"required": ["userId"]
},
"User": {
"type": "object",
"properties": {
@ -5538,34 +5125,6 @@
},
"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": {
@ -6056,12 +5615,6 @@
},
"required": ["allocatableLicenses"]
},
"IssueTrialLicenseRequest": {
"type": "object",
"properties": { "issuedAccount": { "type": "number" } },
"required": ["issuedAccount"]
},
"IssueTrialLicenseResponse": { "type": "object", "properties": {} },
"CancelOrderRequest": {
"type": "object",
"properties": { "poNumber": { "type": "string" } },

View File

@ -41,7 +41,6 @@ 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';
@ -139,7 +138,6 @@ import { JobNumberRepositoryModule } from './repositories/job_number/job_number.
AuthGuardsModule,
SystemAccessGuardsModule,
SortCriteriaRepositoryModule,
TaskFiltersRepositoryModule,
WorktypesRepositoryModule,
TermsModule,
RedisModule,

View File

@ -1,8 +0,0 @@
/**
* LIKE句で使用する文字列のエスケープ
* @param value
* @returns
*/
export function escapeLikeString(value: string): string {
return value.replace(/[%_]/g, (match) => `\\${match}`);
}

View File

@ -23,7 +23,6 @@ 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';
@ -79,7 +78,6 @@ export const makeTestingModule = async (
BlobstorageModule,
AuthGuardsModule,
SortCriteriaRepositoryModule,
TaskFiltersRepositoryModule,
WorktypesRepositoryModule,
TermsRepositoryModule,
JobNumberRepositoryModule,

View File

@ -10,7 +10,6 @@ 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';
// ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ###
@ -280,14 +279,6 @@ 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メンバ変数の参照を取得

View File

@ -12,7 +12,6 @@ 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[] }[];
@ -244,9 +243,6 @@ 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');
@ -337,10 +333,6 @@ 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;
};
@ -531,46 +523,3 @@ 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;
};

View File

@ -344,9 +344,3 @@ 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;

View File

@ -85,10 +85,6 @@ 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';
@ -1889,187 +1885,6 @@ 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

View File

@ -38,10 +38,6 @@ import {
Partner,
GetCompanyNameResponse,
PartnerUser,
SearchPartnersResponse,
GetPartnerHierarchyResponse,
SearchPartner,
PartnerHierarchy,
} from './types/types';
import {
DateWithZeroTime,
@ -985,18 +981,13 @@ export class AccountsService {
// 各子アカウントのShortageを算出してreturn用の変数にマージする
const childrenPartnerLicenses: PartnerLicenseInfo[] = [];
for (const childPartnerLicenseFromRepository of getPartnerLicenseResult.childPartnerLicensesFromRepository) {
const {
allocatableLicenseWithMargin,
expiringSoonLicense,
allocatedLicense,
} = childPartnerLicenseFromRepository;
const { allocatableLicenseWithMargin, expiringSoonLicense } =
childPartnerLicenseFromRepository;
let childShortage = 0;
if (childPartnerLicenseFromRepository.tier === TIERS.TIER5) {
if (
allocatableLicenseWithMargin === undefined ||
expiringSoonLicense === undefined ||
allocatedLicense === undefined
expiringSoonLicense === undefined
) {
throw new Error(
`Tier5 account has no allocatableLicenseWithMargin or expiringSoonLicense. accountId: ${accountId}`,
@ -1017,9 +1008,6 @@ export class AccountsService {
{
shortage: childShortage,
},
{
allocatedLicense: allocatedLicense,
},
);
childrenPartnerLicenses.push(childPartnerLicense);
@ -1084,7 +1072,6 @@ export class AccountsService {
orderDate: new Date(licenseOrder.ordered_at).toISOString(),
poNumber: licenseOrder.po_number,
status: licenseOrder.status,
type: licenseOrder.type,
};
orderHistories.push(returnLicenseOrder);
}
@ -2172,166 +2159,6 @@ 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

View File

@ -463,7 +463,6 @@ export const makeDefaultLicensesRepositoryMockValue =
numberOfOrder: 10,
poNumber: 'PO001',
status: 'Issued',
type: 'NORMAL',
},
],
},

View File

@ -4,7 +4,6 @@ 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';
@ -22,15 +21,6 @@ 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,

View File

@ -1,4 +1,4 @@
import { ApiProperty, OmitType, PickType } from '@nestjs/swagger';
import { ApiProperty } from '@nestjs/swagger';
import {
IsEmail,
IsInt,
@ -320,21 +320,6 @@ 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)
@ -607,9 +592,6 @@ export class PartnerLicenseInfo {
@ApiProperty({ description: '不足数({Stock license} - {Issue Requested}' })
shortage: number;
@ApiProperty({ description: '有効期限内の割り当て済み総ライセンス数' })
allocatedLicense?: number;
@ApiProperty({
description:
'未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数',
@ -636,8 +618,6 @@ export class LicenseOrder {
poNumber: string;
@ApiProperty({ description: '注文状態' })
status: string;
@ApiProperty({ description: 'ライセンス種別' })
type: string;
}
export class GetOrderHistoriesResponse {
@ -726,13 +706,6 @@ 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;
@ -740,16 +713,6 @@ 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 {}
@ -849,10 +812,3 @@ export type PartnerInfoFromDb = {
primaryAccountExternalId: string;
dealerManagement: boolean;
};
// パートナー検索にて、RepositoryからPartnerLicenseInfoに関する情報を取得する際の型
// dealerManagementを除外
export type SearchPartnerInfoFromDb = Omit<
PartnerInfoFromDb,
'dealerManagement'
>;

View File

@ -27,8 +27,6 @@ import {
GetAllocatableLicensesResponse,
CancelOrderRequest,
CancelOrderResponse,
IssueTrialLicenseResponse,
IssueTrialLicenseRequest,
} from './types/types';
import { Request } from 'express';
import { retrieveAuthorizationToken } from '../../common/http/helper';
@ -349,90 +347,6 @@ 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,

View File

@ -1,7 +1,4 @@
import {
NewAllocatedLicenseExpirationDate,
NewTrialLicenseExpirationDate,
} from './types/types';
import { NewAllocatedLicenseExpirationDate } from './types/types';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { HttpException, HttpStatus } from '@nestjs/common';
import { LicensesService } from './licenses.service';
@ -18,14 +15,12 @@ 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 {
@ -41,7 +36,6 @@ 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;
@ -109,8 +103,6 @@ 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 () => {
@ -744,7 +736,7 @@ describe('ライセンス割り当て', () => {
);
const service = module.get<UsersService>(UsersService);
let _subject = '';
let _subject: string = '';
let _url: string | undefined = '';
overrideAdB2cService(service, {
getUser: async (context, externalId) => {
@ -2017,385 +2009,3 @@ 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();
}
}
});
});

View File

@ -14,19 +14,12 @@ 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 {
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';
import { LICENSE_ISSUE_STATUS } from '../../constants';
@Injectable()
export class LicensesService {
@ -459,106 +452,6 @@ 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

View File

@ -1,4 +1,4 @@
import { DataSource, IsNull } from 'typeorm';
import { DataSource } from 'typeorm';
import {
License,
CardLicense,
@ -214,45 +214,3 @@ 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,
};
};

View File

@ -59,16 +59,6 @@ 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]+$/)

View File

@ -54,8 +54,9 @@ export class NotificationService {
}
try {
// installationIdにuserIdを設定し、ユーザー端末の登録にする
const installationId = `odms-user-${userId}`;
// TODO: 登録毎に新規登録する想定でUUIDを付与している
// もしデバイスごとに登録を上書きするようであればUUID部分にデバイス識別子を設定
const installationId = `${pns}_${userId}_${uuidv4()}`;
this.logger.log(`[${context.getTrackingId()}] ${installationId}`);
await this.notificationhubService.register(

View File

@ -132,12 +132,6 @@ 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,
@ -149,8 +143,6 @@ export class TasksController {
status?.split(','),
paramName,
direction,
filterConditionAuthorId,
filterConditionFileName,
);
return { tasks, total, limit, offset };
}
@ -869,7 +861,7 @@ export class TasksController {
@ApiOperation({
operationId: 'reopen',
description:
'了した文字起こしタスクを再開しますステータスをPendingにします',
'了した文字起こしタスクを再開しますステータスをPendingにします',
})
@UseGuards(AuthGuard)
@UseGuards(

File diff suppressed because it is too large Load Diff

View File

@ -75,8 +75,6 @@ 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: { ` +
@ -86,10 +84,7 @@ export class TasksService {
`limit: ${limit}, ` +
`status: ${status}, ` +
`paramName: ${paramName}, ` +
`direction: ${direction}, ` +
`filterConditionAuthorId: ${filterConditionAuthorId},` +
`filterConditionFileName: ${filterConditionFileName}
};`,
`direction: ${direction} };`,
);
// パラメータが省略された場合のデフォルト値: 保存するソート条件の値の初期値と揃える
@ -111,8 +106,6 @@ export class TasksService {
paramName ?? defaultParamName,
direction ?? defaultDirection,
status ?? defaultStatus,
filterConditionAuthorId,
filterConditionFileName,
);
// B2Cからユーザー名を取得する
@ -141,8 +134,6 @@ export class TasksService {
paramName ?? defaultParamName,
direction ?? defaultDirection,
status ?? defaultStatus,
filterConditionAuthorId,
filterConditionFileName,
);
// B2Cからユーザー名を取得する
@ -165,8 +156,6 @@ export class TasksService {
paramName ?? defaultParamName,
direction ?? defaultDirection,
status ?? defaultStatus,
filterConditionAuthorId,
filterConditionFileName,
);
// B2Cからユーザー名を取得する
const b2cUsers = await this.getB2cUsers(

View File

@ -27,7 +27,6 @@ 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';
@ -75,7 +74,6 @@ export const makeTaskTestingModuleWithNotificaiton = async (
BlobstorageModule,
AuthGuardsModule,
SortCriteriaRepositoryModule,
TaskFiltersRepositoryModule,
],
providers: [
AuthService,
@ -112,9 +110,8 @@ export const createTask = async (
priority: string,
jobNumber: string,
status: string,
typist_user_id?: number,
is_job_number_enabled?: boolean,
file_name?: string,
typist_user_id?: number | undefined,
is_job_number_enabled?: boolean | undefined,
): Promise<{ taskId: number; audioFileId: number }> => {
const { identifiers: audioFileIdentifiers } = await datasource
.getRepository(AudioFile)
@ -122,8 +119,8 @@ export const createTask = async (
account_id: account_id,
owner_user_id: owner_user_id,
url: '',
file_name: file_name ?? 'x.zip',
raw_file_name: file_name ?? 'y.zip',
file_name: 'x.zip',
raw_file_name: 'y.zip',
author_id: author_id,
work_type_id: work_type_id,
started_at: new Date(),

View File

@ -6,7 +6,6 @@ import {
IsIn,
IsInt,
IsOptional,
IsString,
Min,
ValidateNested,
} from 'class-validator';
@ -66,22 +65,6 @@ 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に使用される箇所のみバリデータでチェックが行われる状態になっている

View File

@ -12,8 +12,6 @@ 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,
@ -28,11 +26,6 @@ export type SortCriteriaRepositoryMockValue = {
getSortCriteria: SortCriteria | Error;
};
export type TaskFiltersRepositoryMockValue = {
updateTaskFilter: TaskFilters | Error;
getTaskFilter: TaskFilters | Error;
};
export type UsersRepositoryMockValue = {
updateUserVerified: undefined | Error;
findUserById: User | Error;
@ -70,7 +63,6 @@ export const makeUsersServiceMock = async (
sendGridMockValue: SendGridMockValue,
configMockValue: ConfigMockValue,
sortCriteriaRepositoryMockValue: SortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue: TaskFiltersRepositoryMockValue,
): Promise<UsersService> => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
@ -98,8 +90,6 @@ export const makeUsersServiceMock = async (
return makeSortCriteriaRepositoryMock(
sortCriteriaRepositoryMockValue,
);
case TaskFiltersRepositoryService:
return makeTaskFiltersRepositoryMock(taskFiltersRepositoryMockValue);
case BlobstorageService:
return {};
}
@ -138,29 +128,6 @@ 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 {
@ -324,21 +291,6 @@ 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: {

View File

@ -35,15 +35,6 @@ export class ConfirmRequest {
export class ConfirmResponse {}
export class ConfirmForceRequest {
@ApiProperty()
@IsInt()
userId: number;
}
export class ConfirmForceResponse {}
export class User {
@ApiProperty()
id: number;
@ -93,17 +84,6 @@ 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[];
@ -243,30 +223,6 @@ 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)

View File

@ -32,10 +32,6 @@ import {
PostSortCriteriaResponse,
GetSortCriteriaRequest,
GetSortCriteriaResponse,
PostTaskFiltersRequest,
PostTaskFiltersResponse,
GetTaskFiltersRequest,
GetTaskFiltersResponse,
PostUpdateUserRequest,
PostUpdateUserResponse,
AllocateLicenseResponse,
@ -51,9 +47,6 @@ import {
PostMultipleImportsResponse,
PostMultipleImportsCompleteRequest,
PostMultipleImportsCompleteResponse,
ConfirmForceRequest,
ConfirmForceResponse,
GetUsersRequest,
} from './types/types';
import { UsersService } from './users.service';
import { AuthService } from '../auth/auth.service';
@ -164,85 +157,6 @@ 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,
@ -265,14 +179,7 @@ export class UsersController {
RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }),
)
@Get()
async getUsers(
@Req() req: Request,
@Query() query: GetUsersRequest,
): Promise<GetUsersResponse> {
const userName = query.userName?.trimStart();
const email = query.email?.trimStart();
async getUsers(@Req() req: Request): Promise<GetUsersResponse> {
const accessToken = retrieveAuthorizationToken(req);
if (!accessToken) {
@ -309,12 +216,7 @@ export class UsersController {
const context = makeContext(userId, requestId);
this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);
const users = await this.usersService.getUsers(
context,
userId,
userName,
email,
);
const users = await this.usersService.getUsers(context, userId);
return { users };
}
@ -633,163 +535,6 @@ 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,

View File

@ -3,7 +3,6 @@ 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';
@ -18,7 +17,6 @@ import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module
UsersRepositoryModule,
LicensesRepositoryModule,
SortCriteriaRepositoryModule,
TaskFiltersRepositoryModule,
AdB2cModule,
SendGridModule,
ConfigModule,

View File

@ -5,7 +5,6 @@ import {
makeDefaultConfigValue,
makeDefaultSendGridlValue,
makeDefaultSortCriteriaRepositoryMockValue,
makeDefaultTaskFiltersRepositoryMockValue,
makeDefaultUsersRepositoryMockValue,
makeUsersServiceMock,
} from './test/users.service.mock';
@ -61,7 +60,6 @@ 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;
@ -125,7 +123,7 @@ describe('UsersService.confirmUser', () => {
});
const service = module.get<UsersService>(UsersService);
let _subject = '';
let _subject: string = '';
let _url: string | undefined = '';
overrideSendgridService(service, {
sendMail: async (
@ -322,7 +320,7 @@ describe('UsersService.confirmUserAndInitPassword', () => {
};
},
});
let _subject = '';
let _subject: string = '';
overrideSendgridService(service, {
sendMail: async (
context: Context,
@ -894,7 +892,7 @@ describe('UsersService.createUser', () => {
};
},
});
let _subject = '';
let _subject: string = '';
let _url: string | undefined = '';
overrideSendgridService(service, {
sendMail: async (
@ -2051,331 +2049,6 @@ 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();
@ -2439,8 +2112,6 @@ describe('UsersService.updateSortCriteria', () => {
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
@ -2448,7 +2119,6 @@ describe('UsersService.updateSortCriteria', () => {
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
@ -2470,8 +2140,6 @@ describe('UsersService.updateSortCriteria', () => {
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
usersRepositoryMockValue.findUserByExternalId = new Error('user not found');
@ -2482,7 +2150,6 @@ describe('UsersService.updateSortCriteria', () => {
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
@ -2504,8 +2171,6 @@ describe('UsersService.updateSortCriteria', () => {
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
sortCriteriaRepositoryMockValue.updateSortCriteria = new Error(
'sort criteria not found',
);
@ -2517,7 +2182,6 @@ describe('UsersService.updateSortCriteria', () => {
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
@ -2541,8 +2205,6 @@ describe('UsersService.getSortCriteria', () => {
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
const service = await makeUsersServiceMock(
usersRepositoryMockValue,
licensesRepositoryMockValue,
@ -2550,7 +2212,6 @@ describe('UsersService.getSortCriteria', () => {
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
@ -2568,8 +2229,6 @@ describe('UsersService.getSortCriteria', () => {
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
sortCriteriaRepositoryMockValue.getSortCriteria = new Error(
'sort criteria not found',
@ -2582,7 +2241,6 @@ describe('UsersService.getSortCriteria', () => {
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
@ -2604,8 +2262,6 @@ describe('UsersService.getSortCriteria', () => {
const configMockValue = makeDefaultConfigValue();
const sortCriteriaRepositoryMockValue =
makeDefaultSortCriteriaRepositoryMockValue();
const taskFiltersRepositoryMockValue =
makeDefaultTaskFiltersRepositoryMockValue();
sortCriteriaRepositoryMockValue.getSortCriteria = {
id: 1,
direction: 'AAA',
@ -2620,7 +2276,6 @@ describe('UsersService.getSortCriteria', () => {
sendgridMockValue,
configMockValue,
sortCriteriaRepositoryMockValue,
taskFiltersRepositoryMockValue,
);
const context = makeContext(`uuidv4`, 'requestId');
@ -2635,182 +2290,6 @@ 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 () => {
@ -5665,295 +5144,3 @@ 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();
}
}
});
});

View File

@ -17,7 +17,6 @@ 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,
@ -27,7 +26,6 @@ import { LicensesRepositoryService } from '../../repositories/licenses/licenses.
import {
MultipleImportUser,
GetRelationsResponse,
GetTaskFiltersResponse,
MultipleImportErrors,
User,
} from './types/types';
@ -75,7 +73,6 @@ 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,
@ -606,18 +603,10 @@ export class UsersService {
/**
* Get Users
* @param context
* @param externalId
* @param userInputUserName
* @param userInputEmail
* @param accessToken
* @returns users
*/
async getUsers(
context: Context,
externalId: string,
userInputUserName?: string,
userInputEmail?: string,
): Promise<User[]> {
async getUsers(context: Context, externalId: string): Promise<User[]> {
this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`);
try {
@ -628,7 +617,7 @@ export class UsersService {
);
// DBから取得したユーザーの外部IDをもとにADB2Cからユーザーを取得する
const externalIds = dbUsers.map((user) => user.external_id);
const externalIds = dbUsers.map((x) => x.external_id);
const adb2cUsers = await this.adB2cService.getUsers(context, externalIds);
// DBから取得した各ユーザーをもとにADB2C情報をマージしライセンス情報を算出
@ -714,22 +703,7 @@ 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;
return users;
} catch (e) {
this.logger.error(`[${context.getTrackingId()}] error=${e}`);
throw new HttpException(
@ -857,116 +831,6 @@ 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
@ -1876,99 +1740,6 @@ 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

View File

@ -520,8 +520,8 @@ export class BlobstorageService {
this.getContainerClient.name
} | params: { ` + `accountId: ${accountId} };`,
);
const containerName = `account-${accountId}`;
const containerName = `account-${accountId}`;
if (BLOB_STORAGE_REGION_US.includes(country)) {
return this.blobServiceClientUS.getContainerClient(containerName);
} else if (BLOB_STORAGE_REGION_AU.includes(country)) {

View File

@ -33,8 +33,6 @@ 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';
@ -102,10 +100,6 @@ 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');
@ -363,22 +357,6 @@ 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',
);
}
}
@ -401,11 +379,11 @@ export class SendGridService {
const subject = 'Account Registered Notification [U-101]';
const url = new URL(this.appDomain).href;
const html = this.templateU101Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(TOP_URL, url);
const text = this.templateU101Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(TOP_URL, url);
await this.sendMail(
context,
@ -456,8 +434,8 @@ export class SendGridService {
const verifyUrl = `${url}?verify=${token}`;
const subject = 'User Registration Notification [U-102]';
const html = this.templateU102Html.replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
const text = this.templateU102Text.replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
const html = this.templateU102Html.replaceAll(VERIFY_LINK, verifyUrl);
const text = this.templateU102Text.replaceAll(VERIFY_LINK, verifyUrl);
await this.sendMail(
context,
@ -503,15 +481,15 @@ export class SendGridService {
// メールの本文を作成する
const html = this.templateU105Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PO_NUMBER, poNumber)
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
const text = this.templateU105Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PO_NUMBER, poNumber)
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
// メールを送信する
await this.sendMail(
@ -558,15 +536,15 @@ export class SendGridService {
// メールの本文を作成する
const html = this.templateU106Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PO_NUMBER, poNumber)
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
const text = this.templateU106Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PO_NUMBER, poNumber)
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
// メールを送信する
await this.sendMail(
@ -613,15 +591,15 @@ export class SendGridService {
// メールの本文を作成する
const html = this.templateU107Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY,escapeDollar(`${lisenceCount}`));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PO_NUMBER, poNumber)
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
const text = this.templateU107Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY,escapeDollar(`${lisenceCount}`));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PO_NUMBER, poNumber)
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
// メールを送信する
await this.sendMail(
@ -670,28 +648,28 @@ export class SendGridService {
if (dealerAccountName === null) {
html = this.templateU108NoParentHtml
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
text = this.templateU108NoParentText
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
} else {
html = this.templateU108Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
text = this.templateU108Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(USER_EMAIL, escapeDollar(userMail))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(USER_NAME, userName)
.replaceAll(USER_EMAIL, userMail)
.replaceAll(TOP_URL, url);
}
const ccAddress = customerAdminMails.includes(userMail) ? [] : [userMail];
@ -741,15 +719,15 @@ export class SendGridService {
// メールの本文を作成する
const html = this.templateU109Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PO_NUMBER, poNumber)
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
const text = this.templateU109Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PO_NUMBER, escapeDollar(poNumber))
.replaceAll(LICENSE_QUANTITY, escapeDollar(`${lisenceCount}`));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PO_NUMBER, poNumber)
.replaceAll(LICENSE_QUANTITY, `${lisenceCount}`);
// メールを送信する
await this.sendMail(
@ -791,14 +769,14 @@ export class SendGridService {
// メールの本文を作成する
const html = this.templateU111Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, url);
const text = this.templateU111Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, url);
// メールを送信する
await this.sendMail(
@ -847,24 +825,24 @@ export class SendGridService {
if (dealerAccountName === null) {
// メールの本文を作成する
html = this.templateU112NoParentHtml
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, url);
text = this.templateU112NoParentText
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, url);
} else {
html = this.templateU112Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, url);
text = this.templateU112Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TOP_URL, url);
}
// メールを送信する
@ -906,11 +884,11 @@ export class SendGridService {
// メールの本文を作成する
const html = this.templateU113Html
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TEMPORARY_PASSWORD, escapeDollar(temporaryPassword));
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TEMPORARY_PASSWORD, temporaryPassword);
const text = this.templateU113Text
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(TEMPORARY_PASSWORD, escapeDollar(temporaryPassword));
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(TEMPORARY_PASSWORD, temporaryPassword);
// メールを送信する
await this.sendMail(
@ -969,11 +947,11 @@ export class SendGridService {
// メールの本文を作成する
const html = this.templateU114Html
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(VERIFY_LINK, verifyUrl);
const text = this.templateU114Text
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName))
.replaceAll(VERIFY_LINK, escapeDollar(verifyUrl));
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName)
.replaceAll(VERIFY_LINK, verifyUrl);
// メールを送信する
await this.sendMail(
@ -1016,11 +994,11 @@ export class SendGridService {
// メールの本文を作成する
const html = this.templateU115Html
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
.replaceAll(USER_NAME, userName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName);
const text = this.templateU115Text
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
.replaceAll(USER_NAME, userName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName);
// 管理者ユーザーの情報を変更した場合にはTOに管理者のメールアドレスを設定するので、CCには管理者のメールアドレスを設定しない
const ccAdminMails = adminMails.filter((x) => x !== userMail);
@ -1066,11 +1044,11 @@ export class SendGridService {
// メールの本文を作成する
const html = this.templateU116Html
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
.replaceAll(USER_NAME, userName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName);
const text = this.templateU116Text
.replaceAll(USER_NAME, escapeDollar(userName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(primaryAdminName));
.replaceAll(USER_NAME, userName)
.replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName);
// メールを送信する
await this.sendMail(
@ -1116,15 +1094,15 @@ export class SendGridService {
// メールの本文を作成する
const html = this.templateU117Html
.replaceAll(AUTHOR_NAME, escapeDollar(authorName))
.replaceAll(FILE_NAME, escapeDollar(fileName))
.replaceAll(TYPIST_NAME, escapeDollar(typistName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(adminName));
.replaceAll(AUTHOR_NAME, authorName)
.replaceAll(FILE_NAME, fileName)
.replaceAll(TYPIST_NAME, typistName)
.replaceAll(PRIMARY_ADMIN_NAME, adminName);
const text = this.templateU117Text
.replaceAll(AUTHOR_NAME, escapeDollar(authorName))
.replaceAll(FILE_NAME, escapeDollar(fileName))
.replaceAll(TYPIST_NAME, escapeDollar(typistName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(adminName));
.replaceAll(AUTHOR_NAME, authorName)
.replaceAll(FILE_NAME, fileName)
.replaceAll(TYPIST_NAME, typistName)
.replaceAll(PRIMARY_ADMIN_NAME, adminName);
// OMDS_IS-380 Dictation Workflow完了通知 [U-117]  をTypistには送信しないようにしたいの対応のため送信先からtypistEmailを削除 2024年8月7日
const to = [authorEmail].filter((x): x is string => x !== null);
@ -1166,19 +1144,20 @@ export class SendGridService {
if (!dealerAccountName) {
html = this.templateU118NoParentHtml.replaceAll(
CUSTOMER_NAME,escapeDollar( customerAccountName),
CUSTOMER_NAME,
customerAccountName,
);
text = this.templateU118NoParentText.replaceAll(
CUSTOMER_NAME,
escapeDollar(customerAccountName),
customerAccountName,
);
} else {
html = this.templateU118Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName);
text = this.templateU118Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName);
}
// メールを送信する
@ -1226,19 +1205,19 @@ export class SendGridService {
if (!dealerAccountName) {
html = this.templateU119NoParentHtml.replaceAll(
CUSTOMER_NAME,
escapeDollar(customerAccountName),
customerAccountName,
);
text = this.templateU119NoParentText.replaceAll(
CUSTOMER_NAME,
escapeDollar(customerAccountName),
customerAccountName,
);
} else {
html = this.templateU119Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName);
text = this.templateU119Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName);
}
// メールを送信する
@ -1287,24 +1266,24 @@ export class SendGridService {
if (!dealerAccountName) {
html = this.templateU120NoParentHtml
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, escapeDollar(fileName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
text = this.templateU120NoParentText
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, escapeDollar(fileName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
} else {
html = this.templateU120Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, escapeDollar(fileName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
text = this.templateU120Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, escapeDollar(fileName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
}
// メールを送信する
@ -1353,24 +1332,24 @@ export class SendGridService {
if (!dealerAccountName) {
html = this.templateU121NoParentHtml
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, escapeDollar(fileName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
text = this.templateU121NoParentText
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, escapeDollar(fileName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
} else {
html = this.templateU121Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, escapeDollar(fileName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
text = this.templateU121Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(REQUEST_TIME, escapeDollar(requestTime))
.replaceAll(FILE_NAME, escapeDollar(fileName));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(REQUEST_TIME, requestTime)
.replaceAll(FILE_NAME, fileName);
}
// メールを送信する
@ -1453,52 +1432,52 @@ export class SendGridService {
if (!dealerAccountName) {
html = this.templateU122NoParentHtml
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
.replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
.replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
.replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
.replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
.replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
.replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
.replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
.replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn)
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe)
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr)
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn)
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe)
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr)
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn)
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe)
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr);
text = this.templateU122NoParentText
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
.replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
.replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
.replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
.replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
.replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
.replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
.replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
.replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn)
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe)
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr)
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn)
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe)
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr)
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn)
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe)
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr);
} else {
html = this.templateU122Html
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
.replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
.replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
.replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
.replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
.replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
.replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
.replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
.replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn)
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe)
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr)
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn)
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe)
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr)
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn)
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe)
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr);
text = this.templateU122Text
.replaceAll(CUSTOMER_NAME, escapeDollar(customerAccountName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(EMAIL_DUPLICATION_EN, escapeDollar(duplicateEmailsMsgEn))
.replaceAll(EMAIL_DUPLICATION_DE, escapeDollar(duplicateEmailsMsgDe))
.replaceAll(EMAIL_DUPLICATION_FR, escapeDollar(duplicateEmailsMsgFr))
.replaceAll(AUTHOR_ID_DUPLICATION_EN, escapeDollar(duplicateAuthorIdsMsgEn))
.replaceAll(AUTHOR_ID_DUPLICATION_DE, escapeDollar(duplicateAuthorIdsMsgDe))
.replaceAll(AUTHOR_ID_DUPLICATION_FR, escapeDollar(duplicateAuthorIdsMsgFr))
.replaceAll(UNEXPECTED_ERROR_EN, escapeDollar(otherErrorsMsgEn))
.replaceAll(UNEXPECTED_ERROR_DE, escapeDollar(otherErrorsMsgDe))
.replaceAll(UNEXPECTED_ERROR_FR, escapeDollar(otherErrorsMsgFr));
.replaceAll(CUSTOMER_NAME, customerAccountName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn)
.replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe)
.replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr)
.replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn)
.replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe)
.replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr)
.replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn)
.replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe)
.replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr);
}
// メールを送信する
@ -1543,13 +1522,13 @@ export class SendGridService {
const subject = 'Partner Account Deleted Notification [U-123]';
const html = this.templateU123Html
.replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
.replaceAll(CUSTOMER_NAME, partnerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName)
.replaceAll(DEALER_NAME, dealerAccountName);
const text = this.templateU123Text
.replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName));
.replaceAll(CUSTOMER_NAME, partnerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName)
.replaceAll(DEALER_NAME, dealerAccountName);
// メールを送信する
await this.sendMail(
@ -1594,15 +1573,15 @@ export class SendGridService {
const url = new URL(this.appDomain).href;
const html = this.templateU124Html
.replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, partnerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(TOP_URL, url);
const text = this.templateU124Text
.replaceAll(CUSTOMER_NAME, escapeDollar(partnerAccountName))
.replaceAll(PRIMARY_ADMIN_NAME, escapeDollar(partnerPrimaryName))
.replaceAll(DEALER_NAME, escapeDollar(dealerAccountName))
.replaceAll(TOP_URL, escapeDollar(url));
.replaceAll(CUSTOMER_NAME, partnerAccountName)
.replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName)
.replaceAll(DEALER_NAME, dealerAccountName)
.replaceAll(TOP_URL, url);
// メールを送信する
await this.sendMail(
@ -1621,104 +1600,6 @@ 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
@ -1776,9 +1657,3 @@ export class SendGridService {
}
}
}
/**
* $ $$
* @param str -
* @returns
*/
export const escapeDollar = (str: string): string => str.replace(/\$/g, "$$$$");

View File

@ -1,5 +1,5 @@
import sendgrid from '@sendgrid/mail';
import { escapeDollar, SendGridService } from './sendgrid.service';
import { SendGridService } from './sendgrid.service';
import { Context, makeContext } from '../../common/log';
// sendgridのsend関数をモック化
@ -131,111 +131,3 @@ describe('SendGridService', () => {
).rejects.toThrow('Send failed');
});
});
describe('escapeDollar', () => {
test('文字列に$が含まれていない場合はそのまま返す', () => {
expect(escapeDollar('Hello World')).toBe('Hello World');
expect(escapeDollar('No dollars here!')).toBe('No dollars here!');
expect(escapeDollar('')).toBe('');
});
test('$が文字列の先頭にある場合、エスケープされる', () => {
expect(escapeDollar('$Hello')).toBe('$$Hello');
});
test('$が文字列の末尾にある場合、エスケープされる', () => {
expect(escapeDollar('World$')).toBe('World$$');
});
test('$が文字列の途中にある場合、エスケープされる', () => {
expect(escapeDollar('Hello$World')).toBe('Hello$$World');
});
test('複数の$が文字列に含まれる場合、すべてエスケープされる', () => {
expect(escapeDollar('$$Hello$$World$$')).toBe('$$$$Hello$$$$World$$$$');
expect(escapeDollar('$1$2$3')).toBe('$$1$$2$$3');
});
test('特殊文字と$が混在する場合でも$が正しくエスケープされる', () => {
expect(escapeDollar('Price: $5, Tax: $0.50')).toBe(
'Price: $$5, Tax: $$0.50',
);
expect(escapeDollar('Special.$World$^$')).toBe('Special.$$World$$^$$');
});
test('文字列が$だけの場合、すべてエスケープされる', () => {
expect(escapeDollar('$')).toBe('$$');
expect(escapeDollar('$$$')).toBe('$$$$$$');
});
test('すでにエスケープされた$が含まれる場合、さらにエスケープされる', () => {
expect(escapeDollar('$$Already$$Escaped$$')).toBe(
'$$$$Already$$$$Escaped$$$$',
);
});
test('空白や改行、タブ文字を含む文字列でも$がエスケープされる', () => {
expect(escapeDollar(' $Leading$Trailing$ ')).toBe(
' $$Leading$$Trailing$$ ',
);
expect(escapeDollar('Line1$\nLine2$\tEnd$')).toBe(
'Line1$$\nLine2$$\tEnd$$',
);
});
});
describe('特殊文字を含む置き換え処理でescapeDollarを使用するテスト', () => {
test('escapeDollarを使わない場合と使った場合の置き換え結果を比較', () => {
const input = 'This is $TEMPORARY_PASSWORD$ for you.';
const replacement = 'T$&AS$123$';
// escapeDollarを使わない場合
const resultWithoutEscape = input.replaceAll(
'$TEMPORARY_PASSWORD$',
replacement,
);
// escapeDollarを使った場合
const safeReplacement = escapeDollar(replacement);
const resultWithEscape = input.replaceAll(
'$TEMPORARY_PASSWORD$',
safeReplacement,
);
// 意図しない結果になることを確認
expect(resultWithoutEscape).not.toBe('This is T$&AS$123$ for you.');
expect(resultWithoutEscape).toContain('$TEMPORARY_PASSWORD$'); // 意図通りに置換されていない場合が含まれる
// escapeDollarを使用した結果が意図通りであることを確認
expect(resultWithEscape).toBe('This is T$&AS$123$ for you.');
});
test("特殊文字 ($1, $`, $') を含む場合の置換処理を確認", () => {
const input = 'This is $TEMPORARY_PASSWORD$ for you.';
const replacementWithGroup = 'Group-$1-Value';
const replacementWithBacktick = 'Backtick-$`-Value';
const replacementWithSingleQuote = "Single-$'-Value";
// $1: キャプチャグループ
const resultWithGroup = input.replaceAll(
'$TEMPORARY_PASSWORD$',
escapeDollar(replacementWithGroup),
);
expect(resultWithGroup).toBe('This is Group-$1-Value for you.');
// $`: 置換対象文字列の先頭部分
const resultWithBacktick = input.replaceAll(
'$TEMPORARY_PASSWORD$',
escapeDollar(replacementWithBacktick),
);
expect(resultWithBacktick).toBe('This is Backtick-$`-Value for you.');
// $': 置換対象文字列の後続部分
const resultWithSingleQuote = input.replaceAll(
'$TEMPORARY_PASSWORD$',
escapeDollar(replacementWithSingleQuote),
);
expect(resultWithSingleQuote).toBe("This is Single-$'-Value for you.");
});
});

View File

@ -1,4 +1,3 @@
import assert from 'node:assert';
import { Injectable } from '@nestjs/common';
import {
Between,
@ -10,8 +9,6 @@ import {
Not,
UpdateResult,
EntityManager,
FindOptionsWhere,
Like,
} from 'typeorm';
import { User, UserArchive } from '../users/entity/user.entity';
import { Account } from './entity/account.entity';
@ -24,8 +21,6 @@ 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,
@ -71,7 +66,6 @@ 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';
@ -248,21 +242,6 @@ 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 };
});
}
@ -281,7 +260,6 @@ 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(
@ -297,13 +275,6 @@ export class AccountsRepositoryService {
this.isCommentOut,
context,
);
// タスク検索条件を削除
await deleteEntity(
taskFiltersRepo,
{ user_id: userId },
this.isCommentOut,
context,
);
// プライマリ管理者を削除
await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context);
// アカウントを削除
@ -440,35 +411,6 @@ 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;
}
/**
*
*
@ -555,12 +497,14 @@ export class AccountsRepositoryService {
});
// 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する
const allocatedLicense = await this.getAllocatedLicense(
context,
entityManager,
id,
currentDate,
);
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 reusableLicense = await license.count({
@ -805,7 +749,6 @@ 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,
@ -821,12 +764,6 @@ export class AccountsRepositoryService {
childAccount.id,
expiringSoonDate,
);
allocatedLicense = await this.getAllocatedLicense(
context,
entityManager,
childAccount.id,
currentDate,
);
}
// 戻り値用の値を設定
@ -840,7 +777,6 @@ export class AccountsRepositoryService {
issueRequesting: childLicenseOrderStatus.issueRequesting,
expiringSoonLicense: expiringSoonLicense,
allocatableLicenseWithMargin: allocatableLicenseWithMargin,
allocatedLicense: allocatedLicense,
};
childPartnerLicensesFromRepository.push(
@ -906,15 +842,10 @@ export class AccountsRepositoryService {
if (!account) {
break;
}
// 第一階層はアカウント階層の最上位であるため、到達したらループを抜ける
if (account.tier === TIERS.TIER1) {
break;
if (!account.parent_account_id) {
throw new Error("Parent account doesn't exist.");
}
// 第二〜第五のアカウントには親アカウントIDがかならず設定されている
assert(
account.parent_account_id,
new Error("Parent account doesn't exist."),
);
parentAccountIds.push(account.parent_account_id);
currentAccountId = account.parent_account_id;
}
@ -1510,16 +1441,6 @@ 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;
});
}
@ -1736,15 +1657,6 @@ 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(
@ -1969,209 +1881,4 @@ 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;
}
}

View File

@ -35,9 +35,6 @@ export class LicenseOrder {
@Column({ nullable: true, type: 'datetime' })
issued_at: Date | null;
@Column()
type: string;
@Column()
quantity: number;

View File

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, In, MoreThanOrEqual } from 'typeorm';
import { DataSource, In, IsNull, MoreThanOrEqual, Not } from 'typeorm';
import {
LicenseOrder,
License,
@ -9,7 +9,6 @@ import {
} from './entity/license.entity';
import {
CARD_LICENSE_LENGTH,
ISSUED_BY_UPPER_TIER_TRIAL_LICENSE_QUANTITY,
LICENSE_ALLOCATED_STATUS,
LICENSE_ISSUE_STATUS,
LICENSE_TYPE,
@ -33,7 +32,6 @@ import {
import {
AllocatableLicenseInfo,
DateWithZeroTime,
NewTrialLicenseExpirationDate,
} from '../../features/licenses/types/types';
import { NewAllocatedLicenseExpirationDate } from '../../features/licenses/types/types';
import {
@ -516,66 +514,6 @@ 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

View File

@ -1,16 +0,0 @@
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;
}

View File

@ -1,11 +0,0 @@
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 {}

View File

@ -1,129 +0,0 @@
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;
}
}

View File

@ -6,7 +6,6 @@ import {
FindOptionsOrderValue,
In,
IsNull,
Like,
Not,
Repository,
} from 'typeorm';
@ -22,7 +21,6 @@ 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,
@ -47,7 +45,6 @@ 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 {
@ -632,8 +629,6 @@ 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(
@ -644,8 +639,6 @@ 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[];
@ -661,14 +654,6 @@ 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()}`,
});
@ -683,14 +668,6 @@ 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,
@ -726,8 +703,6 @@ 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(
@ -739,8 +714,6 @@ 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[];
@ -749,75 +722,31 @@ export class TasksRepositoryService {
const order = makeOrder(sort_criteria, direction);
const value = await this.dataSource.transaction(async (entityManager) => {
const taskRepo = entityManager.getRepository(Task);
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 count = await taskRepo.count({
where: {
account_id: account_id,
status: In(status),
file: { author_id: author_id },
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
tasksQueryBuilder.comment(
`${context.getTrackingId()}_${new Date().toUTCString()}`,
);
const tasks = await tasksQueryBuilder.take(limit).skip(offset).getMany();
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()}`,
});
const checkoutRepo = entityManager.getRepository(CheckoutPermission);
const taskIds = tasks.map((x) => x.id);
@ -844,8 +773,6 @@ 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[];
@ -910,27 +837,11 @@ 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()}`,
@ -950,27 +861,11 @@ 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に使用するパラメータを変更
@ -1282,9 +1177,7 @@ 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()}`,
@ -1295,17 +1188,6 @@ 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 (
@ -1317,14 +1199,6 @@ 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: {
@ -1373,9 +1247,6 @@ export class TasksRepositoryService {
// 引数の音声ファイルIDで指定したタスクとユーザが着手可能なタスクの一覧を取得
const tasks = await taskRepo.find({
relations: {
file: true,
},
where: [
{
account_id: accountId,
@ -1386,14 +1257,6 @@ 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,

View File

@ -8,7 +8,6 @@ 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,
@ -136,21 +135,6 @@ 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;
},
);
@ -852,7 +836,6 @@ export class UsersRepositoryService {
this.isCommentOut,
context,
);
// 期限切れライセンスが割り当てられていた場合、ユーザーを削除する前にライセンスを割り当て解除する
// ※この処理時点で有効期限外ライセンスであることは確定であるため、期限切れ判定をここでは行わない
if (allocatedLicense != null) {
@ -886,7 +869,6 @@ export class UsersRepositoryService {
context,
);
}
// ユーザテーブルのレコードを削除する
await deleteEntity(
userRepo,
@ -894,7 +876,6 @@ export class UsersRepositoryService {
this.isCommentOut,
context,
);
// ソート条件のテーブルのレコードを削除する
const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
await deleteEntity(
@ -903,16 +884,6 @@ export class UsersRepositoryService {
this.isCommentOut,
context,
);
// タスク検索条件テーブルのレコードを削除する
const taskFiltersRepo = entityManager.getRepository(TaskFilters);
await deleteEntity(
taskFiltersRepo,
{ user_id: target.id },
this.isCommentOut,
context,
);
return { isSuccess: true };
});
}
@ -933,16 +904,6 @@ 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);
});

View File

@ -12,9 +12,6 @@ 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$`;

View File

@ -1,108 +0,0 @@
<html>
<head>
<title>Issued Trial License Notification [U-125]</title>
</head>
<body>
<div>
<h3>&lt;English&gt;</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>&lt;Deutsch&gt;</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>&lt;Français&gt;</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>

View File

@ -1,59 +0,0 @@
<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.

View File

@ -1,92 +0,0 @@
<html>
<head>
<title>Forced Email Verification Notification [U-126]</title>
</head>
<body>
<div>
<h3>&lt;English&gt;</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>&lt;Deutsch&gt;</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>&lt;Français&gt;</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>

View File

@ -1,47 +0,0 @@
<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.

Some files were not shown because too many files have changed in this diff Show More