diff --git a/dictation_client/src/pages/AccountPage/index.tsx b/dictation_client/src/pages/AccountPage/index.tsx index 6fd0877..8e3a149 100644 --- a/dictation_client/src/pages/AccountPage/index.tsx +++ b/dictation_client/src/pages/AccountPage/index.tsx @@ -191,23 +191,32 @@ const AccountPage: React.FC = (): JSX.Element => { )} {isTier5 && ( -
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - -
+ <> +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
+
+ {t( + getTranslationID( + "accountPage.text.dealerManagementAnnotation" + ) + )} +
+ )} {!isTier5 &&
-
} @@ -374,6 +383,15 @@ const AccountPage: React.FC = (): JSX.Element => { className={styles.icLoading} alt="Loading" /> + {isTier5 && ( +

+ {t( + getTranslationID( + "accountPage.text.dealerManagementAnnotation" + ) + )} +

+ )} {isTier5 && ( diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 2435346..52f8553 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -1632,8 +1632,31 @@ _:-ms-lang(x)::-ms-backdrop, .account .listVertical dd .formInput { max-width: 100%; } +.account .listVertical dd.full { + width: 100%; + padding-top: 0; + background: none; +} +.account .listVertical dd.full.odd { + background: #f5f5f5; +} +.account .listVertical dd.formComment { + text-align: left; + font-size: 0.9rem; + word-break: break-word; +} +.account .box100 .formComment { + display: block; + width: 600px; + text-align: left; +} .account .box100.alignRight { width: calc(1200px + 3rem); + text-align: right; +} +.account .box100.alignRight .formComment { + margin-left: 648px; + text-align: right; } .menuAction { @@ -2306,7 +2329,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input + label:hover, .formChange ul.holdMember li input + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center; + background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left + center; background-size: 1.3rem; } .formChange ul.chooseMember li input:checked + label, @@ -2317,8 +2341,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input:checked + label:hover, .formChange ul.holdMember li input:checked + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right - center; + background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat + right center; background-size: 1.3rem; } .formChange > p { @@ -2471,7 +2495,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input + label:hover, .formChange ul.holdMember li input + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left center; + background: #e6e6e6 url(../assets/images/arrow_circle_left.svg) no-repeat left + center; background-size: 1.3rem; } .formChange ul.chooseMember li input:checked + label, @@ -2482,8 +2507,8 @@ tr.isSelected .menuInTable li a.isDisable { } .formChange ul.chooseMember li input:checked + label:hover, .formChange ul.holdMember li input:checked + label:hover { - background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat right - center; + background: #e6e6e6 url(../assets/images/arrow_circle_right.svg) no-repeat + right center; background-size: 1.3rem; } .formChange > p { diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts index c4b1492..2dde822 100644 --- a/dictation_client/src/styles/app.module.scss.d.ts +++ b/dictation_client/src/styles/app.module.scss.d.ts @@ -107,6 +107,7 @@ declare const classNames: { readonly clm0: "clm0"; readonly menuInTable: "menuInTable"; readonly isSelected: "isSelected"; + readonly odd: "odd"; readonly alignRight: "alignRight"; readonly menuAction: "menuAction"; readonly inTable: "inTable"; diff --git a/dictation_client/src/translation/de.json b/dictation_client/src/translation/de.json index f756410..c729c87 100644 --- a/dictation_client/src/translation/de.json +++ b/dictation_client/src/translation/de.json @@ -571,4 +571,4 @@ "close": "Schließen" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/en.json b/dictation_client/src/translation/en.json index 1ac4bf5..dca78c7 100644 --- a/dictation_client/src/translation/en.json +++ b/dictation_client/src/translation/en.json @@ -571,4 +571,4 @@ "close": "Close" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/es.json b/dictation_client/src/translation/es.json index 70f8bf8..84e0cc9 100644 --- a/dictation_client/src/translation/es.json +++ b/dictation_client/src/translation/es.json @@ -571,4 +571,4 @@ "close": "Cerrar" } } -} \ No newline at end of file +} diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 3717a7e..c90ad0b 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -571,4 +571,4 @@ "close": "Fermer" } } -} \ No newline at end of file +} diff --git a/dictation_server/db/migrations/056-add_tasks_index.sql b/dictation_server/db/migrations/056-add_tasks_index.sql new file mode 100644 index 0000000..983ea67 --- /dev/null +++ b/dictation_server/db/migrations/056-add_tasks_index.sql @@ -0,0 +1,27 @@ +-- +migrate Up +ALTER TABLE `tasks` ADD INDEX `idx_tasks_audio_file_id` (audio_file_id); +ALTER TABLE `tasks` ADD INDEX `idx_tasks_status` (status); +ALTER TABLE `tasks` ADD INDEX `idx_tasks_typist_user_id` (typist_user_id); +ALTER TABLE `tasks` ADD INDEX `idx_tasks_is_job_number_enabled` (is_job_number_enabled); +ALTER TABLE `checkout_permission` ADD INDEX `idx_checkout_permission_task_id` (task_id); +ALTER TABLE `checkout_permission` ADD INDEX `idx_checkout_permission_user_group_id` (user_group_id); +ALTER TABLE `checkout_permission` ADD INDEX `idx_checkout_permission_user_id` (user_id); +ALTER TABLE `users` ADD INDEX `idx_users_role` (role); +ALTER TABLE `users` ADD INDEX `idx_users_author_id` (author_id); +ALTER TABLE `users` ADD INDEX `idx_users_deleted_at` (deleted_at); +ALTER TABLE `worktypes` ADD INDEX `idx_worktypes_custom_worktype_id` (custom_worktype_id); +ALTER TABLE `workflows` ADD INDEX `idx_workflows_account_id` (account_id); + +-- +migrate Down +ALTER TABLE `tasks` DROP INDEX `idx_tasks_audio_file_id`; +ALTER TABLE `tasks` DROP INDEX `idx_tasks_status`; +ALTER TABLE `tasks` DROP INDEX `idx_tasks_typist_user_id`; +ALTER TABLE `tasks` DROP INDEX `idx_tasks_is_job_number_enabled`; +ALTER TABLE `checkout_permission` DROP INDEX `idx_checkout_permission_task_id`; +ALTER TABLE `checkout_permission` DROP INDEX `idx_checkout_permission_user_group_id`; +ALTER TABLE `checkout_permission` DROP INDEX `idx_checkout_permission_user_id`; +ALTER TABLE `users` DROP INDEX `idx_users_role`; +ALTER TABLE `users` DROP INDEX `idx_users_author_id`; +ALTER TABLE `users` DROP INDEX `idx_users_deleted_at`; +ALTER TABLE `worktypes` DROP INDEX `idx_worktypes_custom_worktype_id`; +ALTER TABLE `workflows` DROP INDEX `idx_workflows_account_id`; \ No newline at end of file diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 50038f9..d712597 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -194,6 +194,7 @@ export class TasksRepositoryService { audio_file_id: audio_file_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!task) { throw new TasksNotFoundError( @@ -209,6 +210,7 @@ export class TasksRepositoryService { typist_user_id: user_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (tasks.length > 0) { @@ -247,6 +249,7 @@ export class TasksRepositoryService { }, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ユーザーの所属するすべてのグループIDを列挙 const groupIds = groups.map((member) => member.user_group_id); @@ -269,6 +272,7 @@ export class TasksRepositoryService { }, ], comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); //チェックアウト権限がなければエラー @@ -339,6 +343,7 @@ export class TasksRepositoryService { audio_file_id: audio_file_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!task) { throw new TasksNotFoundError( @@ -392,6 +397,7 @@ export class TasksRepositoryService { audio_file_id: audio_file_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!task) { throw new TasksNotFoundError( @@ -467,6 +473,7 @@ export class TasksRepositoryService { audio_file_id: audio_file_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!task) { throw new TasksNotFoundError( @@ -518,6 +525,7 @@ export class TasksRepositoryService { audio_file_id: audio_file_id, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!task) { throw new TasksNotFoundError( @@ -901,6 +909,7 @@ export class TasksRepositoryService { where: { account_id: account_id, is_job_number_enabled: true }, order: { created_at: 'DESC', job_number: 'DESC' }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); let newJobNumber = '00000001'; @@ -963,30 +972,6 @@ export class TasksRepositoryService { assignees: Assignee[], ): Promise { await this.dataSource.transaction(async (entityManager) => { - // UserGroupの取得/存在確認 - const userGroupIds = assignees - .filter((x) => x.typistGroupId !== undefined) - .map((y) => { - return y.typistGroupId; - }); - const groupRepo = entityManager.getRepository(UserGroup); - const groupRecords = await groupRepo.find({ - where: { - id: In(userGroupIds), - account_id: account_id, - deleted_at: IsNull(), - }, - comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, - }); - // idはユニークであるため取得件数の一致でグループの存在を確認 - if (userGroupIds.length !== groupRecords.length) { - throw new TypistUserGroupNotFoundError( - `Group not exists Error. reqUserGroupId:${userGroupIds}; resUserGroupId:${groupRecords.map( - (x) => x.id, - )}`, - ); - } - // Userの取得/存在確認 const typistUserIds = assignees .filter((x) => x.typistUserId !== undefined) @@ -1014,6 +999,31 @@ export class TasksRepositoryService { ); } + // UserGroupの取得/存在確認 + const userGroupIds = assignees + .filter((x) => x.typistGroupId !== undefined) + .map((y) => { + return y.typistGroupId; + }); + const groupRepo = entityManager.getRepository(UserGroup); + const groupRecords = await groupRepo.find({ + where: { + id: In(userGroupIds), + account_id: account_id, + deleted_at: IsNull(), + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + // idはユニークであるため取得件数の一致でグループの存在を確認 + if (userGroupIds.length !== groupRecords.length) { + throw new TypistUserGroupNotFoundError( + `Group not exists Error. reqUserGroupId:${userGroupIds}; resUserGroupId:${groupRecords.map( + (x) => x.id, + )}`, + ); + } + // 引数audioFileIdを使ってTaskレコードを特定し、そのステータスを取得/存在確認 const taskRepo = entityManager.getRepository(Task); @@ -1029,6 +1039,7 @@ export class TasksRepositoryService { }, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); //タスクが存在しない or ステータスがUploadedでなければエラー if (!taskRecord) { @@ -1192,39 +1203,51 @@ export class TasksRepositoryService { return await this.dataSource.transaction(async (entityManager) => { // 音声ファイルを取得 const audioFileRepo = entityManager.getRepository(AudioFile); - const audioFile = await audioFileRepo.findOne({ - relations: { - task: true, - }, + const audio = await audioFileRepo.findOne({ where: { id: audioFileId, account_id: accountId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); - if (!audioFile) { + if (!audio) { throw new Error( `audio file not found. audio_file_id:${audioFileId}, accountId:${accountId}`, ); } - const { task } = audioFile; - - if (!task) { - throw new Error( - `task not found. audio_file_id:${audioFileId}, accountId:${accountId}`, - ); - } // authorIdをもとにユーザーを取得 const userRepo = entityManager.getRepository(User); const authorUser = await userRepo.findOne({ where: { - author_id: audioFile.author_id, + author_id: audio.author_id, account_id: accountId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); + // TaskとFileを取得 + const taskRepo = entityManager.getRepository(Task); + const task = await taskRepo.findOne({ + relations: { + file: true, + }, + where: { + account_id: accountId, + audio_file_id: audioFileId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + const audioFile = task?.file; + if (!audioFile) { + throw new Error( + `audio file not found. audio_file_id:${audioFileId}, accountId:${accountId}`, + ); + } + // 音声ファイル上のworktypeIdをもとにworktypeを取得 const worktypeRepo = entityManager.getRepository(Worktype); const worktypeRecord = await worktypeRepo.findOne({ @@ -1233,6 +1256,7 @@ export class TasksRepositoryService { account_id: accountId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // 音声ファイル上のworktypeIdが設定されているが、一致するworktypeが存在しない場合はエラーを出して終了 @@ -1254,6 +1278,7 @@ export class TasksRepositoryService { worktype_id: worktypeRecord?.id ?? IsNull(), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // Workflow(ルーティングルール)があればタスクのチェックアウト権限を設定する @@ -1281,6 +1306,7 @@ export class TasksRepositoryService { account_id: accountId, }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (!myAuthorUser) { throw new Error( @@ -1297,6 +1323,7 @@ export class TasksRepositoryService { worktype_id: worktypeRecord?.id ?? IsNull(), }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // API実行者のAuthorIdと音声ファイルのWorktypeをもとにルーティングルールを取得できない場合はエラーを出して終了 @@ -1478,6 +1505,7 @@ export class TasksRepositoryService { const typistUsers = await userRepo.find({ where: { account_id: accountId, id: In(typistIds) }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (typistUsers.length !== typistIds.length) { throw new Error(`typist not found. ids: ${typistIds}`); @@ -1491,6 +1519,7 @@ export class TasksRepositoryService { const typistGroups = await userGroupRepo.find({ where: { account_id: accountId, id: In(groupIds) }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); if (typistGroups.length !== groupIds.length) { throw new Error(`typist group not found. ids: ${groupIds}`); diff --git a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts index 07e1704..8ca02c9 100644 --- a/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts +++ b/dictation_server/src/repositories/worktypes/worktypes.repository.service.ts @@ -220,6 +220,7 @@ export class WorktypesRepositoryService { const worktype = await worktypeRepo.findOne({ where: { account_id: accountId, id: id }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, }); // ワークタイプが存在しない場合はエラー if (!worktype) {