From da40e8f09c0f18d869f43e064d1df3f582b21aa9 Mon Sep 17 00:00:00 2001 From: "makabe.t" Date: Wed, 6 Mar 2024 01:58:32 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20800:=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=EF=BC=88=E4=B8=80=E6=8B=AC=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=83=9C=E3=82=BF=E3=83=B3=EF=BC=86=E3=83=9D=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=82=A2=E3=83=83=E3=83=97=E7=94=BB=E9=9D=A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3753: 画面実装(一括追加ボタン&ポップアップ画面)](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3753) - ユーザー一括登録画面を実装しました。 - 一括登録ポップアップ - テンプレートCSVダウンロード - ファイルインポート - エラー行表示 ## レビューポイント - 行エラーの条件、内容は認識通りでしょうか? - 画面の表示内容は認識通りでしょうか? - CSV変換時にworkerを有効にしているとエラーとなりうまくいかないのでOFFにしてしまいましたが問題ないでしょうか? - @<湯本 開> さん ## UIの変更 - [Task3753](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task3753?csf=1&web=1&e=x5M6hr) ## 動作確認状況 - ローカルで確認 - ファイルチェックするところまで --- dictation_client/src/api/api.ts | 286 ++++++++++++++++++ dictation_client/src/assets/images/upload.svg | 1 + dictation_client/src/common/parser.ts | 2 +- .../src/features/user/operations.ts | 72 +++++ .../src/features/user/selectors.ts | 139 +++++++++ dictation_client/src/features/user/state.ts | 9 +- .../src/features/user/userSlice.ts | 26 ++ .../src/pages/UserListPage/importPopup.tsx | 254 ++++++++++++++++ .../src/pages/UserListPage/index.tsx | 22 ++ dictation_client/src/styles/app.module.scss | 50 +++ .../src/styles/app.module.scss.d.ts | 4 +- dictation_client/src/translation/de.json | 55 +++- dictation_client/src/translation/en.json | 47 ++- dictation_client/src/translation/es.json | 47 ++- dictation_client/src/translation/fr.json | 47 ++- 15 files changed, 1026 insertions(+), 35 deletions(-) create mode 100644 dictation_client/src/assets/images/upload.svg create mode 100644 dictation_client/src/pages/UserListPage/importPopup.tsx diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index e166e6f..b68f729 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -1344,6 +1344,92 @@ export interface LicenseOrder { */ 'status': string; } +/** + * + * @export + * @interface MultipleImportErrors + */ +export interface MultipleImportErrors { + /** + * ユーザー名 + * @type {string} + * @memberof MultipleImportErrors + */ + 'name': string; + /** + * エラー発生行数 + * @type {number} + * @memberof MultipleImportErrors + */ + 'line': number; + /** + * エラーコード + * @type {string} + * @memberof MultipleImportErrors + */ + 'errorCode': string; +} +/** + * + * @export + * @interface MultipleImportUser + */ +export interface MultipleImportUser { + /** + * ユーザー名 + * @type {string} + * @memberof MultipleImportUser + */ + 'name': string; + /** + * メールアドレス + * @type {string} + * @memberof MultipleImportUser + */ + 'email': string; + /** + * 0(none)/1(author)/2(typist) + * @type {number} + * @memberof MultipleImportUser + */ + 'role': number; + /** + * + * @type {string} + * @memberof MultipleImportUser + */ + 'authorId'?: string; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'autoRenew': number; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'notification': number; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'encryption'?: number; + /** + * + * @type {string} + * @memberof MultipleImportUser + */ + 'encryptionPassword'?: string; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'prompt'?: number; +} /** * * @export @@ -1525,6 +1611,56 @@ export interface PostDeleteUserRequest { */ 'userId': number; } +/** + * + * @export + * @interface PostMultipleImportsCompleteRequest + */ +export interface PostMultipleImportsCompleteRequest { + /** + * アカウントID + * @type {number} + * @memberof PostMultipleImportsCompleteRequest + */ + 'accountId': number; + /** + * CSVファイル名 + * @type {string} + * @memberof PostMultipleImportsCompleteRequest + */ + 'filename': string; + /** + * 一括登録受付時刻(UNIXTIME/ミリ秒) + * @type {number} + * @memberof PostMultipleImportsCompleteRequest + */ + 'requestTime': number; + /** + * + * @type {Array} + * @memberof PostMultipleImportsCompleteRequest + */ + 'errors': Array; +} +/** + * + * @export + * @interface PostMultipleImportsRequest + */ +export interface PostMultipleImportsRequest { + /** + * CSVファイル名 + * @type {string} + * @memberof PostMultipleImportsRequest + */ + 'filename': string; + /** + * + * @type {Array} + * @memberof PostMultipleImportsRequest + */ + 'users': Array; +} /** * * @export @@ -7431,6 +7567,86 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImports: async (postMultipleImportsRequest: PostMultipleImportsRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postMultipleImportsRequest' is not null or undefined + assertParamExists('multipleImports', 'postMultipleImportsRequest', postMultipleImportsRequest) + const localVarPath = `/users/multiple-imports`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImportsComplate: async (postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postMultipleImportsCompleteRequest' is not null or undefined + assertParamExists('multipleImportsComplate', 'postMultipleImportsCompleteRequest', postMultipleImportsCompleteRequest) + const localVarPath = `/users/multiple-imports/complete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsCompleteRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -7710,6 +7926,32 @@ export const UsersApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['UsersApi.getUsers']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImports(postMultipleImportsRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.multipleImports']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImportsComplate(postMultipleImportsCompleteRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.multipleImportsComplate']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -7858,6 +8100,26 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath getUsers(options?: any): AxiosPromise { return localVarFp.getUsers(options).then((request) => request(axios, basePath)); }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: any): AxiosPromise { + return localVarFp.multipleImports(postMultipleImportsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: any): AxiosPromise { + return localVarFp.multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -8012,6 +8274,30 @@ export class UsersApi extends BaseAPI { return UsersApiFp(this.configuration).getUsers(options).then((request) => request(this.axios, this.basePath)); } + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).multipleImports(postMultipleImportsRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary diff --git a/dictation_client/src/assets/images/upload.svg b/dictation_client/src/assets/images/upload.svg new file mode 100644 index 0000000..2425a97 --- /dev/null +++ b/dictation_client/src/assets/images/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dictation_client/src/common/parser.ts b/dictation_client/src/common/parser.ts index c2950c3..15fe85b 100644 --- a/dictation_client/src/common/parser.ts +++ b/dictation_client/src/common/parser.ts @@ -40,7 +40,7 @@ export const parseCSV = async (csvString: string): Promise => new Promise((resolve, reject) => { Papa.parse(csvString, { download: false, - worker: true, + worker: false, // XXX: workerを使うとエラーが発生するためfalseに設定 header: true, dynamicTyping: true, complete: (results: ParseResult) => { diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index f9e2663..8b9c782 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -9,6 +9,7 @@ import { UsersApi, LicensesApi, GetAllocatableLicensesResponse, + MultipleImportUser, } from "../../api/api"; import { Configuration } from "../../api/configuration"; import { ErrorObject, createErrorObject } from "../../common/errors"; @@ -498,3 +499,74 @@ export const deleteUserAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const importUsersAsync = createAsyncThunk< + // 正常時の戻り値の型 + { + /* Empty Object */ + }, + // 引数 + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("users/importUsersAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const { importFileName, importUsers } = state.user.apps; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const usersApi = new UsersApi(config); + + try { + if (importFileName === undefined) { + throw new Error("importFileName is undefined"); + } + + // CSVデータをAPIに送信するためのデータに変換 + const users: MultipleImportUser[] = importUsers.map((user) => ({ + name: user.name ?? "", + email: user.email ?? "", + role: user.role ?? 0, + authorId: user.author_id ?? undefined, + autoRenew: user.auto_renew ?? 0, + notification: user.notification ?? 0, + encryption: user.encryption ?? undefined, + encryptionPassword: user.encryption_password ?? undefined, + prompt: user.prompt ?? undefined, + })); + + await usersApi.multipleImports( + { + filename: importFileName, + users, + }, + { headers: { authorization: `Bearer ${accessToken}` } } + ); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("userListPage.message.importSuccess"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換 + const error = createErrorObject(e); + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/user/selectors.ts b/dictation_client/src/features/user/selectors.ts index 29c41fb..dc25752 100644 --- a/dictation_client/src/features/user/selectors.ts +++ b/dictation_client/src/features/user/selectors.ts @@ -382,3 +382,142 @@ const convertValueBasedOnLicenseStatus = ( remaining: undefined, }; }; + +export const selectImportFileName = (state: RootState) => + state.user.apps.importFileName; + +export const selectImportValidationErrors = (state: RootState) => { + const csvUsers = state.user.apps.importUsers; + + let rowNumber = 0; + const invalidInput: number[] = []; + + const duplicatedEmailsMap = new Map(); + const duplicatedAuthorIdsMap = new Map(); + const overMaxRow = csvUsers.length > 100; + + // eslint-disable-next-line no-restricted-syntax + for (const csvUser of csvUsers) { + rowNumber += 1; + + // メールアドレスの重複がある場合、エラーとしてその行番号を追加する + const duplicatedEmailUser = csvUsers.filter( + (x) => x.email === csvUser.email + ); + if (duplicatedEmailUser.length > 1) { + if (csvUser.email !== null && !duplicatedEmailsMap.has(csvUser.email)) { + duplicatedEmailsMap.set(csvUser.email, rowNumber); + } + } + + // AuthorIDの重複がある場合、エラーとしてその行番号を追加する + const duplicatedAuthorIdUser = csvUsers.filter( + (x) => x.author_id === csvUser.author_id + ); + if (duplicatedAuthorIdUser.length > 1) { + if ( + csvUser.author_id !== null && + !duplicatedAuthorIdsMap.has(csvUser.author_id) + ) { + duplicatedAuthorIdsMap.set(csvUser.author_id, rowNumber); + } + } + + // name + if (csvUser.name === null || csvUser.name.length > 225) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // email + const emailPattern = + /^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/; + if ( + csvUser.name === null || + csvUser.name.length > 225 || + !emailPattern.test(csvUser.email ?? "") + ) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // role + if (csvUser.role === null || ![0, 1, 2].includes(csvUser.role)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // role=1(Author) + if (csvUser.role === 1) { + // author_id + if (csvUser.author_id === null || csvUser.author_id.length > 16) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + // 半角英数字と_の組み合わせで16文字まで + const charaTypePattern = /^[A-Z0-9_]{1,16}$/; + const charaType = new RegExp(charaTypePattern).test(csvUser.author_id); + if (!charaType) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // encryption + if (csvUser.encryption === null || ![0, 1].includes(csvUser.encryption)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + if (csvUser.encryption === 1) { + // encryption_password + if (csvUser.encryption === 1) { + const regex = /^[!-~]{4,16}$/; + if (!regex.test(csvUser.encryption_password ?? "")) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + } + } + + // prompt + if (csvUser.prompt === null || ![0, 1].includes(csvUser.prompt)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + } + + // auto_renew + if (csvUser.auto_renew === null || ![0, 1].includes(csvUser.auto_renew)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // notification + if ( + csvUser.notification === null || + ![0, 1].includes(csvUser.notification) + ) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + } + + const duplicatedEmails = Array.from(duplicatedEmailsMap.values()); + const duplicatedAuthorIds = Array.from(duplicatedAuthorIdsMap.values()); + + return { + invalidInput, + duplicatedEmails, + duplicatedAuthorIds, + overMaxRow, + }; +}; diff --git a/dictation_client/src/features/user/state.ts b/dictation_client/src/features/user/state.ts index f25dd94..5d619e1 100644 --- a/dictation_client/src/features/user/state.ts +++ b/dictation_client/src/features/user/state.ts @@ -1,4 +1,9 @@ -import { User, AllocatableLicenseInfo } from "../../api/api"; +import { CSVType } from "common/parser"; +import { + User, + AllocatableLicenseInfo, + MultipleImportUser, +} from "../../api/api"; import { AddUser, UpdateUser, LicenseAllocateUser } from "./types"; export interface UsersState { @@ -19,4 +24,6 @@ export interface Apps { selectedlicenseId: number; hasPasswordMask: boolean; isLoading: boolean; + importFileName: string | undefined; + importUsers: CSVType[]; } diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index 42baced..b0fcf5c 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -1,5 +1,6 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { USER_ROLES } from "components/auth/constants"; +import { CSVType } from "common/parser"; import { UsersState } from "./state"; import { addUserAsync, @@ -8,6 +9,7 @@ import { getAllocatableLicensesAsync, deallocateLicenseAsync, deleteUserAsync, + importUsersAsync, } from "./operations"; import { RoleType, UserView } from "./types"; @@ -61,6 +63,8 @@ const initialState: UsersState = { selectedlicenseId: 0, hasPasswordMask: false, isLoading: false, + importFileName: undefined, + importUsers: [], }, }; @@ -242,6 +246,17 @@ export const userSlice = createSlice({ state.apps.licenseAllocateUser = initialState.apps.licenseAllocateUser; state.apps.selectedlicenseId = initialState.apps.selectedlicenseId; }, + changeImportFileName: ( + state, + action: PayloadAction<{ fileName: string }> + ) => { + const { fileName } = action.payload; + state.apps.importFileName = fileName; + }, + changeImportCsv: (state, action: PayloadAction<{ users: CSVType[] }>) => { + const { users } = action.payload; + state.apps.importUsers = users; + }, }, extraReducers: (builder) => { builder.addCase(listUsersAsync.pending, (state) => { @@ -300,6 +315,15 @@ export const userSlice = createSlice({ builder.addCase(deleteUserAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(importUsersAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(importUsersAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(importUsersAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); @@ -327,6 +351,8 @@ export const { changeLicenseAllocateUser, changeSelectedlicenseId, cleanupLicenseAllocateInfo, + changeImportFileName, + changeImportCsv, } = userSlice.actions; export default userSlice.reducer; diff --git a/dictation_client/src/pages/UserListPage/importPopup.tsx b/dictation_client/src/pages/UserListPage/importPopup.tsx new file mode 100644 index 0000000..d01d930 --- /dev/null +++ b/dictation_client/src/pages/UserListPage/importPopup.tsx @@ -0,0 +1,254 @@ +import { AppDispatch } from "app/store"; +import React, { useState, useCallback } from "react"; +import styles from "styles/app.module.scss"; +import { useDispatch, useSelector } from "react-redux"; +import { getTranslationID } from "translation"; +import { useTranslation } from "react-i18next"; +import { + selectIsLoading, + importUsersAsync, + changeImportFileName, + changeImportCsv, + selectImportFileName, + selectImportValidationErrors, +} from "features/user"; +import { parseCSV } from "common/parser"; +import close from "../../assets/images/close.svg"; +import download from "../../assets/images/download.svg"; +import upload from "../../assets/images/upload.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +interface UserAddPopupProps { + isOpen: boolean; + onClose: () => void; +} + +export const ImportPopup: React.FC = (props) => { + const { isOpen, onClose } = props; + const dispatch: AppDispatch = useDispatch(); + const { t } = useTranslation(); + // AddUserの情報を取得 + + const closePopup = useCallback(() => { + setIsPushImportButton(false); + onClose(); + }, [onClose]); + + const [isPushImportButton, setIsPushImportButton] = useState(false); + const isLoading = useSelector(selectIsLoading); + + const importFileName = useSelector(selectImportFileName); + const { invalidInput, duplicatedEmails, duplicatedAuthorIds, overMaxRow } = + useSelector(selectImportValidationErrors); + + const onDownloadCsv = useCallback(() => { + // csvファイルダウンロード処理 + const filename = `import_users.csv`; + + const importCsvHeader = [ + "name", + "email", + "role", + "author_id", + "auto_renew", + "notification", + "encryption", + "encryption_password", + "prompt", + ].toString(); + + const blob = new Blob([importCsvHeader], { + type: "mime", + }); + const blobURL = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = blobURL; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.parentNode?.removeChild(a); + }, []); + + // ファイルが選択されたときの処理 + const handleFileChange = useCallback( + async (event: React.ChangeEvent) => { + // 選択されたファイルを取得(複数選択されても先頭を取得) + const file = event.target.files?.[0]; + + // ファイルが選択されていれば、storeに保存 + if (file) { + const text = await file.text(); + const users = await parseCSV(text.trimEnd()); + + dispatch(changeImportCsv({ users })); + dispatch(changeImportFileName({ fileName: file.name })); + } + // 同名のファイルを選択した場合、onChangeが発火しないため、valueをクリアする + event.target.value = ""; + }, + [dispatch] + ); + + const onImportUsers = useCallback(async () => { + setIsPushImportButton(true); + if ( + invalidInput.length > 0 || + duplicatedEmails.length > 0 || + duplicatedAuthorIds.length > 0 || + overMaxRow + ) { + return; + } + + await dispatch(importUsersAsync()); + setIsPushImportButton(false); + }, [ + dispatch, + invalidInput, + duplicatedEmails, + duplicatedAuthorIds, + overMaxRow, + ]); + + return ( +
+
+

+ {t(getTranslationID("userListPage.label.bulkImport"))} + +

+
+
+
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + + + {t(getTranslationID("userListPage.label.downloadCsv"))} + + {t(getTranslationID("userListPage.text.downloadExplain"))} +
+
+ +
+
Input rules
+
{t(getTranslationID("userListPage.label.nameLabel"))}
+
{t(getTranslationID("userListPage.text.nameRule"))}
+
+ {t(getTranslationID("userListPage.label.emailAddressLabel"))} +
+
{t(getTranslationID("userListPage.text.emailAddressRule"))}
+
{t(getTranslationID("userListPage.label.roleLabel"))}
+
{t(getTranslationID("userListPage.text.roleRule"))}
+
{t(getTranslationID("userListPage.label.authorIdLabel"))}
+
{t(getTranslationID("userListPage.text.authorIdRule"))}
+
{t(getTranslationID("userListPage.label.autoRenewLabel"))}
+
{t(getTranslationID("userListPage.text.autoRenewRule"))}
+
+ {t(getTranslationID("userListPage.label.notificationLabel"))} +
+
{t(getTranslationID("userListPage.text.notificationRule"))}
+
{t(getTranslationID("userListPage.label.encryptionLabel"))}
+
{t(getTranslationID("userListPage.text.encryptionRule"))}
+
+ {t( + getTranslationID("userListPage.label.encryptionPasswordLabel") + )} +
+
+ {t(getTranslationID("userListPage.text.encryptionPasswordRule"))} +
+
{t(getTranslationID("userListPage.label.promptLabel"))}
+
{t(getTranslationID("userListPage.text.promptRule"))}
+
+ {isPushImportButton && overMaxRow && ( + + {t(getTranslationID("userListPage.message.overMaxUserError"))} + + )} + {isPushImportButton && invalidInput.length > 0 && ( + <> + + {t( + getTranslationID("userListPage.message.invalidInputError") + )} + + + {invalidInput.map((row) => `L${row}`).join(", ")} + + + )} + {isPushImportButton && duplicatedEmails.length > 0 && ( + <> + + {t( + getTranslationID( + "userListPage.message.duplicateEmailError" + ) + )} + + + {duplicatedEmails.map((row) => `L${row}`).join(", ")} + + + )} + {isPushImportButton && duplicatedAuthorIds.length > 0 && ( + <> + + {t( + getTranslationID( + "userListPage.message.duplicateAuthorIdError" + ) + )} + + + {duplicatedAuthorIds.map((row) => `L${row}`).join(", ")} + + + )} +
+
+ + Loading +
+
+
+
+
+ ); +}; diff --git a/dictation_client/src/pages/UserListPage/index.tsx b/dictation_client/src/pages/UserListPage/index.tsx index 92ecef0..c8a2ba4 100644 --- a/dictation_client/src/pages/UserListPage/index.tsx +++ b/dictation_client/src/pages/UserListPage/index.tsx @@ -32,9 +32,11 @@ import personAdd from "../../assets/images/person_add.svg"; 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 { UserAddPopup } from "./popup"; import { UserUpdatePopup } from "./updatePopup"; import { AllocateLicensePopup } from "./allocateLicensePopup"; +import { ImportPopup } from "./importPopup"; const UserListPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -46,6 +48,7 @@ const UserListPage: React.FC = (): JSX.Element => { const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false); const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] = useState(false); + const [isImportPopupOpen, setIsImportPopupOpen] = useState(false); const onOpen = useCallback(() => { setIsPopupOpen(true); @@ -66,6 +69,9 @@ const UserListPage: React.FC = (): JSX.Element => { }, [setIsAllocateLicensePopupOpen, dispatch] ); + const onImportPopupOpen = useCallback(() => { + setIsImportPopupOpen(true); + }, [setIsImportPopupOpen]); const onLicenseDeallocation = useCallback( async (userId: number) => { @@ -134,6 +140,12 @@ const UserListPage: React.FC = (): JSX.Element => { setIsAllocateLicensePopupOpen(false); }} /> + { + setIsImportPopupOpen(false); + }} + />
{ {t(getTranslationID("userListPage.label.addUser"))} +
  • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + + + {t(getTranslationID("userListPage.label.bulkImport"))} + +
  • diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 8e77dfa..c3a2d1e 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -1630,6 +1630,43 @@ _:-ms-lang(x)::-ms-backdrop, margin-bottom: 5rem; } +.formList.userImport .formTitle { + padding: 1rem 4% 0; + line-height: 1.2; +} +.formList.userImport dt:not(.formTitle) { + width: 30%; + padding: 0 4% 0 4%; + font-size: 0.9rem; +} +.formList.userImport dt:not(.formTitle):nth-of-type(odd) { + background: #f0f0f0; +} +.formList.userImport dt:not(.formTitle):nth-of-type(odd) + dd { + background: #f0f0f0; +} +.formList.userImport dd { + width: 58%; + padding: 0.2rem 4% 0.2rem 0; + margin-bottom: 0; + white-space: pre-line; + word-wrap: break-word; + font-size: 0.9rem; + line-height: 1.2; +} +.formList.userImport dd.full { + width: 100%; + padding: 0.2rem 4% 0.2rem 4%; +} +.formList.userImport dd.full .buttonText { + padding: 0 0 0.8rem; +} +.formList.userImport dd .menuLink { + display: inline-block; + margin-bottom: 0.6rem; + padding: 0.5rem 1.5rem 0.5rem 1.3rem; +} + .account .listVertical { margin-bottom: 3rem; } @@ -2284,6 +2321,9 @@ tr.isSelected .menuInTable li a.isDisable { .formList.property dt:not(.formTitle):nth-of-type(odd) + dd { background: #f0f0f0; } +.formList.property dt:has(+ dd.hasInput) { + padding-top: 0.4rem; +} .formList.property dd { width: 58%; padding: 0.2rem 4% 0.2rem 0; @@ -2295,6 +2335,16 @@ tr.isSelected .menuInTable li a.isDisable { .formList.property dd img { height: 1.1rem; } +.formList.property dd .formInput.short { + width: 250px; + padding: 0.3rem 0.3rem 0.1rem; +} +.formList.property dd .formSubmit { + min-width: auto; + padding: 0.2rem 0.5rem; + position: absolute; + right: 0.5rem; +} .formList.property dd.full { width: 100%; padding: 0.2rem 4% 0.2rem 4%; diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts index 656d36a..e82c3c7 100644 --- a/dictation_client/src/styles/app.module.scss.d.ts +++ b/dictation_client/src/styles/app.module.scss.d.ts @@ -108,11 +108,12 @@ declare const classNames: { readonly clm0: "clm0"; readonly menuInTable: "menuInTable"; readonly isSelected: "isSelected"; + readonly userImport: "userImport"; + readonly menuLink: "menuLink"; readonly odd: "odd"; readonly alignRight: "alignRight"; readonly menuAction: "menuAction"; readonly inTable: "inTable"; - readonly menuLink: "menuLink"; readonly menuIcon: "menuIcon"; readonly colorLink: "colorLink"; readonly isDisable: "isDisable"; @@ -193,6 +194,7 @@ declare const classNames: { readonly hideO10: "hideO10"; readonly op10: "op10"; readonly property: "property"; + readonly hasInput: "hasInput"; readonly formChange: "formChange"; readonly chooseMember: "chooseMember"; readonly holdMember: "holdMember"; diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index 9730841..7ed2025 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -64,7 +64,7 @@ "countryExplanation": "Wählen Sie das Land aus, in dem Sie sich befinden. Wenn Ihr Land nicht aufgeführt ist, wählen Sie bitte das nächstgelegene Land aus.", "dealerExplanation": "Bitte wählen Sie den Händler aus, bei dem Sie die Lizenz erwerben möchten.", "adminInfoTitle": "Registrieren Sie die Informationen des primären Administrators", - "passwordTerms": "Bitte legen Sie ein Passwort fest. Das Passwort muss 8–25 Zeichen lang sein und Buchstaben, Zahlen und Symbole enthalten. (Sollte ein kompatibles Symbol auflisten und angeben, ob ein Großbuchstabe erforderlich ist.)" + "passwordTerms": "Bitte legen Sie ein Passwort fest. Das Passwort muss 8–64 Zeichen lang sein und Buchstaben, Zahlen und Symbole enthalten." }, "label": { "company": "Name der Firma", @@ -135,7 +135,12 @@ "typistUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", "authorUserDeletionTranscriptionTaskError": "(de)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", "typistUserDeletionTranscriptionistGroupError": "(de)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "authorDeletionRoutingRuleError": "(de)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", + "importSuccess": "(de)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", + "duplicateEmailError": "(de)以下の行のメールアドレスがCSV中で重複しています。", + "duplicateAuthorIdError": "(de)以下の行のAuthorIDがCSV中で重複しています。", + "overMaxUserError": "(de)一度に追加できるユーザーは100件までです。", + "invalidInputError": "(de)以下の行のユーザー情報が入力ルールに準拠していません。" }, "label": { "title": "Benutzer", @@ -168,7 +173,33 @@ "deleteUser": "Benutzer löschen", "none": "Keiner", "encryptionPassword": "Passwort", - "encryptionPasswordTerm": "Bitte legen Sie Ihr Passwort mit 4 bis 16 alphanumerischen Zeichen und Symbolen fest." + "encryptionPasswordTerm": "Bitte legen Sie Ihr Passwort mit 4 bis 16 alphanumerischen Zeichen und Symbolen fest.", + "bulkImport": "(de)Bulk import", + "downloadCsv": "(de)Download CSV", + "importCsv": "(de)Import CSV", + "inputRules": "(de)Input rules", + "nameLabel": "(de)Name", + "emailAddressLabel": "(de)Email Address", + "roleLabel": "(de)Role", + "authorIdLabel": "(de)Author ID", + "autoRenewLabel": "(de)Auto Renew", + "notificationLabel": "(de)Notification", + "encryptionLabel": "(de)Encryption", + "encryptionPasswordLabel": "(de)Encryption Password", + "promptLabel": "(de)Prompt", + "addUsers": "(de)Add users" + }, + "text": { + "downloadExplain": "(de)Download the csv format and enter it according to the rules below.", + "nameRule": "(de)Maximum 225 characters", + "emailAddressRule": "(de)Maximum 225 characters\nCannot use an email address that is already in use.", + "roleRule": "(de)None : 0\nAuthor : 1\nTranscriptionist : 2", + "authorIdRule": "(de)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", + "autoRenewRule": "(de)0 or 1", + "notificationRule": "(de)0 or 1", + "encryptionRule": "(de)Required only when Role=Author(1)\n0 or 1", + "encryptionPasswordRule": "(de)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", + "promptRule": "(de)Required only when Role=Author(1)\n0 or 1" } }, "LicenseSummaryPage": { @@ -198,16 +229,16 @@ "licenseOrderPage": { "message": { "inputEmptyError": "Pflichtfeld", - "poNumberIncorrectError": "Das Format der Bestellnummer ist ungültig. Für die Bestellnummer können nur alphanumerische Zeichen eingegeben werden.", + "poNumberIncorrectError": "Das Format der PO-Nummer ist ungültig. Für die PO-Nummer können nur alphanumerische Zeichen eingegeben werden.", "newOrderIncorrectError": "Bitte geben Sie für die neue Bestellung eine Zahl größer oder gleich 1 ein.", "confirmOrder": "Möchten Sie eine Bestellung aufgeben?", - "poNumberConflictError": "Die eingegebene Bestellnummer existiert bereits. Bitte geben Sie eine andere Bestellnummer ein.", - "dealerNotFoundError": "(de)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" + "poNumberConflictError": "Die eingegebene PO-Nummer existiert bereits. Bitte geben Sie eine andere PO-Nummer ein.", + "dealerNotFoundError": "Um eine Lizenz zu bestellen, müssen Sie den Händler angeben, bei dem Sie die Lizenz erwerben möchten. Melden Sie sich bei ODMS Cloud an und richten Sie „Händler“ auf der Registerkarte „Konto“ ein." }, "label": { "title": "Lizenz bestellen", "licenses": "Lizenz-Typ", - "poNumber": "Bestellnummer", + "poNumber": "PO-Nummer", "newOrder": "Anzahl der Lizenzen", "orderButton": "Bestellen", "licenseTypeText": "Ein Jahr" @@ -220,7 +251,9 @@ "taskNotEditable": "Der Transkriptionist kann nicht geändert werden, da die Transkription bereits ausgeführt wird oder die Datei nicht vorhanden ist. Bitte aktualisieren Sie den Bildschirm und prüfen Sie den aktuellen Status.", "backupFailedError": "Der Prozess „Dateisicherung“ ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal. Wenn der Fehler weiterhin besteht, wenden Sie sich an Ihren Systemadministrator.", "cancelFailedError": "Die Diktate konnten nicht gelöscht werden. Bitte aktualisieren Sie Ihren Bildschirm und versuchen Sie es erneut.", - "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" + "deleteFailedError": "(de)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "licenseNotAssignedError": "Das Diktat kann nicht hochgeladen werden, da keine gültige Lizenz zugewiesen ist. Bitte fragen Sie Ihren Administrator.", + "licenseExpiredError": "Die Transkription ist nicht möglich, da Ihre Lizenz abgelaufen ist. Bitte bitten Sie Ihren Administrator, Ihnen eine gültige Lizenz zuzuweisen." }, "label": { "title": "Diktate", @@ -262,7 +295,7 @@ "changeTranscriptionist": "Transkriptionist ändern", "deleteDictation": "Diktat löschen", "selectedTranscriptionist": "Ausgewählter transkriptionist", - "poolTranscriptionist": "Liste der Transkriptionisten", + "poolTranscriptionist": "Transkriptionsliste", "fileBackup": "Dateisicherung", "downloadForBackup": "Zur Sicherung herunterladen", "applications": "Desktopanwendung", @@ -345,7 +378,7 @@ "orderDate": "Auftragsdatum", "issueDate": "Ausgabetag", "numberOfOrder": "Anzahl der bestellten Lizenzen", - "poNumber": "Bestellnummer", + "poNumber": "PO-Nummer", "status": "Status", "issueRequesting": "Lizenzen auf Bestellung", "issued": "Lizenz ausgestellt", @@ -429,7 +462,7 @@ "message": { "selectedTypistEmptyError": "Um eine Transkriptionsgruppe zu speichern, müssen ein oder mehrere Transkriptionisten ausgewählt werden.", "groupSaveFailedError": "Die Transkriptionistengruppe konnte nicht gespeichert werden. Die angezeigten Informationen sind möglicherweise veraltet. Aktualisieren Sie daher bitte den Bildschirm, um den neuesten Status anzuzeigen.", - "GroupNameAlreadyExistError": "(de)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。", + "GroupNameAlreadyExistError": "Der Name dieser Transkriptionistengruppe ist bereits registriert. Bitte registrieren Sie sich mit einem anderen Namen der Transkriptionistengruppe.", "deleteFailedWorkflowAssigned": "(de)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", "deleteFailedCheckoutPermissionExisted": "(de)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 959ea1d..e3186d5 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -64,7 +64,7 @@ "countryExplanation": "Select the country where you are located. If your country isn't listed, please select the nearest country.", "dealerExplanation": "Please select the dealer you would like to purchase the license from.", "adminInfoTitle": "Register primary administrator's information", - "passwordTerms": "Please set a password. The password must be 8-25 characters must contain letters, numbers, and symbols. (Should list compatible symbol and state if capital letter is needed)." + "passwordTerms": "Please set a password. The password must be 8-64 characters must contain letters, numbers, and symbols." }, "label": { "company": "Company Name", @@ -135,7 +135,12 @@ "typistUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", "authorUserDeletionTranscriptionTaskError": "ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", "typistUserDeletionTranscriptionistGroupError": "ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "authorDeletionRoutingRuleError": "ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", + "importSuccess": "ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", + "duplicateEmailError": "以下の行のメールアドレスがCSV中で重複しています。", + "duplicateAuthorIdError": "以下の行のAuthorIDがCSV中で重複しています。", + "overMaxUserError": "一度に追加できるユーザーは100件までです。", + "invalidInputError": "以下の行のユーザー情報が入力ルールに準拠していません。" }, "label": { "title": "User", @@ -168,7 +173,33 @@ "deleteUser": "Delete User", "none": "None", "encryptionPassword": "Password", - "encryptionPasswordTerm": "Please set your password using 4 to 16 alphanumeric and symbols." + "encryptionPasswordTerm": "Please set your password using 4 to 16 alphanumeric and symbols.", + "bulkImport": "Bulk import", + "downloadCsv": "Download CSV", + "importCsv": "Import CSV", + "inputRules": "Input rules", + "nameLabel": "Name", + "emailAddressLabel": "Email Address", + "roleLabel": "Role", + "authorIdLabel": "Author ID", + "autoRenewLabel": "Auto Renew", + "notificationLabel": "Notification", + "encryptionLabel": "Encryption", + "encryptionPasswordLabel": "Encryption Password", + "promptLabel": "Prompt", + "addUsers": "Add users" + }, + "text": { + "downloadExplain": "Download the csv format and enter it according to the rules below.", + "nameRule": "Maximum 225 characters", + "emailAddressRule": "Maximum 225 characters\nCannot use an email address that is already in use.", + "roleRule": "None : 0\nAuthor : 1\nTranscriptionist : 2", + "authorIdRule": "Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", + "autoRenewRule": "0 or 1", + "notificationRule": "0 or 1", + "encryptionRule": "Required only when Role=Author(1)\n0 or 1", + "encryptionPasswordRule": "Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", + "promptRule": "Required only when Role=Author(1)\n0 or 1" } }, "LicenseSummaryPage": { @@ -202,7 +233,7 @@ "newOrderIncorrectError": "Please enter a number greater than or equal to 1 for the New Order.", "confirmOrder": "Would you like to place an order?", "poNumberConflictError": "PO Number entered already exists. Please enter a different PO Number.", - "dealerNotFoundError": "ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" + "dealerNotFoundError": "In order to order a license, you need to set up the dealer where you want to purchase it. Sign in to ODMS Cloud and set up \"Dealer\" in the \"Account\" tab." }, "label": { "title": "Order License", @@ -220,7 +251,9 @@ "taskNotEditable": "The transcriptionist cannot be changed because the transcription is already in progress or the file does not exist. Please refresh the screen and check the latest status.", "backupFailedError": "The \"File Backup\" process has failed. Please try again later. If the error continues, contact your system administrator.", "cancelFailedError": "Failed to delete the dictations. Please refresh your screen and try again.", - "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" + "deleteFailedError": "タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "licenseNotAssignedError": "Dictation cannot be uploaded because a vaild license is not assigned. Please ask your administrator.", + "licenseExpiredError": "Transcription is not possible because your license is expired. Please ask your administrator to assign a valid license." }, "label": { "title": "Dictations", @@ -262,7 +295,7 @@ "changeTranscriptionist": "Change Transcriptionist", "deleteDictation": "Delete Dictation", "selectedTranscriptionist": "Selected Transcriptionist", - "poolTranscriptionist": "Transcriptionist List", + "poolTranscriptionist": "Transcription List", "fileBackup": "File Backup", "downloadForBackup": "Download for backup", "applications": "Desktop Application", @@ -429,7 +462,7 @@ "message": { "selectedTypistEmptyError": "One or more transcriptonist must be selected to save a transcrption group.", "groupSaveFailedError": "Transcriptionist Group could not be saved. The displayed information may be outdated, so please refresh the screen to see the latest status.", - "GroupNameAlreadyExistError": "このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。", + "GroupNameAlreadyExistError": "This Transcriptionist Group name is already registered. Please register with another Transcriptionist Group name.", "deleteFailedWorkflowAssigned": "TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", "deleteFailedCheckoutPermissionExisted": "TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index f89d531..ea17dd8 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -64,7 +64,7 @@ "countryExplanation": "Seleccione el país donde se encuentra. Si su país no aparece en la lista, seleccione el país más cercano.", "dealerExplanation": "Seleccione el distribuidor al que le gustaría comprar la licencia.", "adminInfoTitle": "Registre la información del administrador principal", - "passwordTerms": "Establezca una contraseña. La contraseña debe tener entre 8 y 25 caracteres y debe contener letras, números y símbolos. (Debe enumerar el símbolo compatible e indicar si se necesita una letra mayúscula)." + "passwordTerms": "Establezca una contraseña. La contraseña debe tener entre 8 y 64 caracteres y debe contener letras, números y símbolos." }, "label": { "company": "Nombre de empresa", @@ -135,7 +135,12 @@ "typistUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", "authorUserDeletionTranscriptionTaskError": "(es)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", "typistUserDeletionTranscriptionistGroupError": "(es)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "authorDeletionRoutingRuleError": "(es)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", + "importSuccess": "(es)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", + "duplicateEmailError": "(es)以下の行のメールアドレスがCSV中で重複しています。", + "duplicateAuthorIdError": "(es)以下の行のAuthorIDがCSV中で重複しています。", + "overMaxUserError": "(es)一度に追加できるユーザーは100件までです。", + "invalidInputError": "(es)以下の行のユーザー情報が入力ルールに準拠していません。" }, "label": { "title": "Usuario", @@ -168,7 +173,33 @@ "deleteUser": "Borrar usuario", "none": "Ninguno", "encryptionPassword": "Contraseña", - "encryptionPasswordTerm": "Configure su contraseña utilizando de 4 a 16 símbolos alfanuméricos y." + "encryptionPasswordTerm": "Configure su contraseña utilizando de 4 a 16 símbolos alfanuméricos y.", + "bulkImport": "(es)Bulk import", + "downloadCsv": "(es)Download CSV", + "importCsv": "(es)Import CSV", + "inputRules": "(es)Input rules", + "nameLabel": "(es)Name", + "emailAddressLabel": "(es)Email Address", + "roleLabel": "(es)Role", + "authorIdLabel": "(es)Author ID", + "autoRenewLabel": "(es)Auto Renew", + "notificationLabel": "(es)Notification", + "encryptionLabel": "(es)Encryption", + "encryptionPasswordLabel": "(es)Encryption Password", + "promptLabel": "(es)Prompt", + "addUsers": "(es)Add users" + }, + "text": { + "downloadExplain": "(es)Download the csv format and enter it according to the rules below.", + "nameRule": "(es)Maximum 225 characters", + "emailAddressRule": "(es)Maximum 225 characters\nCannot use an email address that is already in use.", + "roleRule": "(es)None : 0\nAuthor : 1\nTranscriptionist : 2", + "authorIdRule": "(es)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", + "autoRenewRule": "(es)0 or 1", + "notificationRule": "(es)0 or 1", + "encryptionRule": "(es)Required only when Role=Author(1)\n0 or 1", + "encryptionPasswordRule": "(es)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", + "promptRule": "(es)Required only when Role=Author(1)\n0 or 1" } }, "LicenseSummaryPage": { @@ -202,7 +233,7 @@ "newOrderIncorrectError": "Ingrese un número mayor o igual a 1 para el Nuevo Pedido.", "confirmOrder": "¿Quieres hacer un pedido?", "poNumberConflictError": "El número de orden de compra ingresado ya existe. Ingrese un número de orden de compra diferente.", - "dealerNotFoundError": "(es)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" + "dealerNotFoundError": "Para solicitar una licencia, debe configurar el distribuidor donde desea comprarla. Inicie sesión en ODMS Cloud y configure \"Distribuidor\" en la pestaña \"Cuenta\"." }, "label": { "title": "Licencia de pedido", @@ -220,7 +251,9 @@ "taskNotEditable": "No se puede cambiar el transcriptor porque la transcripción ya está en curso o el archivo no existe. Actualice la pantalla y verifique el estado más reciente.", "backupFailedError": "El proceso de \"Copia de seguridad de archivos\" ha fallado. Por favor, inténtelo de nuevo más tarde. Si el error continúa, comuníquese con el administrador del sistema.", "cancelFailedError": "No se pudieron eliminar los dictados. Actualice su pantalla e inténtelo nuevamente.", - "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" + "deleteFailedError": "(es)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "licenseNotAssignedError": "No se puede cargar el dictado porque no se ha asignado una licencia válida. Consulte a su administrador.", + "licenseExpiredError": "La transcripción no es posible porque su licencia ha caducado. Solicite a su administrador que le asigne una licencia válida." }, "label": { "title": "Dictado", @@ -262,7 +295,7 @@ "changeTranscriptionist": "Cambiar transcriptor", "deleteDictation": "Borrar dictado", "selectedTranscriptionist": "Transcriptor seleccionado", - "poolTranscriptionist": "Lista de transcriptores", + "poolTranscriptionist": "Lista de transcriptor", "fileBackup": "Copia de seguridad de archivos", "downloadForBackup": "Descargar para respaldo", "applications": "Aplicación de escritorio", @@ -429,7 +462,7 @@ "message": { "selectedTypistEmptyError": "Se deben seleccionar uno o más transcriptores para guardar un grupo de transcripción.", "groupSaveFailedError": "El grupo transcriptor no se pudo salvar. La información mostrada puede estar desactualizada. Así que actualice la pantalla para ver el estado más reciente.", - "GroupNameAlreadyExistError": "(es)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。", + "GroupNameAlreadyExistError": "El nombre de este grupo transcriptor ya está registrado. Regístrese con otro nombre de grupo transcriptor.", "deleteFailedWorkflowAssigned": "(es)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", "deleteFailedCheckoutPermissionExisted": "(es)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" } diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index b313ce4..1b473e6 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -64,7 +64,7 @@ "countryExplanation": "Sélectionnez le pays où vous vous trouvez. Si votre pays ne figure pas dans la liste, veuillez sélectionner le pays le plus proche.", "dealerExplanation": "Veuillez sélectionner le revendeur auprès duquel vous souhaitez acheter la licence.", "adminInfoTitle": "Enregistrer les informations de l'administrateur principal", - "passwordTerms": "Veuillez définir un mot de passe. Le mot de passe doit être composé de 8 à 25 caractères et doit contenir des lettres, des chiffres et des symboles. (Devrait lister les symboles compatibles et indiquer si une majuscule est nécessaire)." + "passwordTerms": "Veuillez définir un mot de passe. Le mot de passe doit être composé de 8 à 64 caractères et doit contenir des lettres, des chiffres et des symboles." }, "label": { "company": "Nom de l'entreprise", @@ -135,7 +135,12 @@ "typistUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面でタスクのルーティングから対象Transcriptionistを外してください。", "authorUserDeletionTranscriptionTaskError": "(fr)ユーザーの削除に失敗しました。Dictation画面で対象AuthorのAuthorIDが設定されているタスクの中で、文字起こしが未完了のタスクを削除またはFinishedにしてください。", "typistUserDeletionTranscriptionistGroupError": "(fr)ユーザーの削除に失敗しました。Workflow画面でTranscriptionistGroupから対象Transcriptionistを外してください。", - "authorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。" + "authorDeletionRoutingRuleError": "(fr)ユーザーの削除に失敗しました。Workflow画面でルーティングルールから対象AuthorのAuthorIDを外してください。", + "importSuccess": "(fr)ユーザー一括追加を受け付けました。登録処理が完了次第メールが届きますのでご確認ください。", + "duplicateEmailError": "(fr)以下の行のメールアドレスがCSV中で重複しています。", + "duplicateAuthorIdError": "(fr)以下の行のAuthorIDがCSV中で重複しています。", + "overMaxUserError": "(fr)一度に追加できるユーザーは100件までです。", + "invalidInputError": "(fr)以下の行のユーザー情報が入力ルールに準拠していません。" }, "label": { "title": "Utilisateur", @@ -168,7 +173,33 @@ "deleteUser": "Supprimer l'utilisateur", "none": "Aucun", "encryptionPassword": "Mot de passe", - "encryptionPasswordTerm": "Veuillez définir votre mot de passe en utilisant 4 à 16 caractères alphanumériques et symboles." + "encryptionPasswordTerm": "Veuillez définir votre mot de passe en utilisant 4 à 16 caractères alphanumériques et symboles.", + "bulkImport": "(fr)Bulk import", + "downloadCsv": "(fr)Download CSV", + "importCsv": "(fr)Import CSV", + "inputRules": "(fr)Input rules", + "nameLabel": "(fr)Name", + "emailAddressLabel": "(fr)Email Address", + "roleLabel": "(fr)Role", + "authorIdLabel": "(fr)Author ID", + "autoRenewLabel": "(fr)Auto Renew", + "notificationLabel": "(fr)Notification", + "encryptionLabel": "(fr)Encryption", + "encryptionPasswordLabel": "(fr)Encryption Password", + "promptLabel": "(fr)Prompt", + "addUsers": "(fr)Add users" + }, + "text": { + "downloadExplain": "(fr)Download the csv format and enter it according to the rules below.", + "nameRule": "(fr)Maximum 225 characters", + "emailAddressRule": "(fr)Maximum 225 characters\nCannot use an email address that is already in use.", + "roleRule": "(fr)None : 0\nAuthor : 1\nTranscriptionist : 2", + "authorIdRule": "(fr)Required only when Role=Author(1)\nMaximum 16 characters\nOnly uppercase alphanumeric characters and \"_\" can be entered.\nCannot use an Author ID that is already in use.", + "autoRenewRule": "(fr)0 or 1", + "notificationRule": "(fr)0 or 1", + "encryptionRule": "(fr)Required only when Role=Author(1)\n0 or 1", + "encryptionPasswordRule": "(fr)Required only when Role=Author(1) and Encryption=ON(1)\nOnly 4 to 16 letters, numbers, and symbols can be entered.", + "promptRule": "(fr)Required only when Role=Author(1)\n0 or 1" } }, "LicenseSummaryPage": { @@ -202,7 +233,7 @@ "newOrderIncorrectError": "Veuillez saisir un nombre supérieur ou égal à 1 pour la nouvelle commande.", "confirmOrder": "Voulez-vous passer commande?", "poNumberConflictError": "Le numéro de bon de commande saisi existe déjà. Veuillez saisir un autre numéro de bon de commande.", - "dealerNotFoundError": "(fr)ディーラーが設定されていないため、ライセンスを注文できません。アカウント画面でディーラーを指定してください。" + "dealerNotFoundError": "Pour commander une licence, vous devez identifier le revendeur où vous souhaitez l'acheter. Connectez-vous à ODMS Cloud et configurez « Revendeur » dans l'onglet « Compte »." }, "label": { "title": "Commander licence", @@ -220,7 +251,9 @@ "taskNotEditable": "Le transcripteur ne peut pas être changé car la transcription est déjà en cours ou le fichier n'existe pas. Veuillez actualiser l'écran et vérifier le dernier statut.", "backupFailedError": "Le processus de « Sauvegarde de fichier » a échoué. Veuillez réessayer plus tard. Si l'erreur persiste, contactez votre administrateur système.", "cancelFailedError": "Échec de la suppression des dictées. Veuillez actualiser votre écran et réessayer.", - "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。" + "deleteFailedError": "(fr)タスクの削除に失敗しました。画面を更新し、再度ご確認ください。", + "licenseNotAssignedError": "La dictée ne peut pas être téléchargée parce qu'une licence valable n'est pas attribuée. Veuillez vous adresser à votre administrateur.", + "licenseExpiredError": "La transcription n'est pas possible car votre licence est expirée. Veuillez demander à votre administrateur de vous attribuer une licence valide." }, "label": { "title": "Dictées", @@ -262,7 +295,7 @@ "changeTranscriptionist": "Changer de transcriptionniste ", "deleteDictation": "Supprimer la dictée", "selectedTranscriptionist": "Transcriptionniste sélectionné", - "poolTranscriptionist": "Liste des transcripteurs", + "poolTranscriptionist": "Liste de transcriptionniste", "fileBackup": "Sauvegarde de fichiers", "downloadForBackup": "Télécharger pour sauvegarde", "applications": "Application de bureau", @@ -429,7 +462,7 @@ "message": { "selectedTypistEmptyError": "Un ou plusieurs transcripteurs doivent être sélectionnés pour enregistrer un groupe de transcription.", "groupSaveFailedError": "Le groupe de transcriptionniste n'a pas pu être enregistré. Les informations affichées peuvent être obsolètes, veuillez donc actualiser l'écran pour voir le dernier statut.", - "GroupNameAlreadyExistError": "(fr)このTranscriptionistGroup名は既に登録されています。他のTranscriptionistGroup名で登録してください。", + "GroupNameAlreadyExistError": "Ce nom de groupe transcripteur est déjà enregistré. Veuillez vous inscrire avec un autre nom de groupe transcripteur.", "deleteFailedWorkflowAssigned": "(fr)TranscriptionistGroupの削除に失敗しました。Workflow画面でルーティングルールから対象TranscriptionistGroupを外してください。", "deleteFailedCheckoutPermissionExisted": "(fr)TranscriptionistGroupの削除に失敗しました。Dictation画面でタスクのルーティングから対象TranscriptionistGroupを外してください。" }